Skip to content

Billing Webhooks

Billing webhooks are outbound notifications that SuperSpace sends to your billing system when something happens to a service on a billing plan. They let an external biller — a plan that invoices customers outside Stripe (payment_method: none) — mirror lifecycle changes into its own books without polling the API.

Two resource types emit billing webhooks: sites (subscription lifecycle) and domain registrations (registry lifecycle). Both go to the same plan endpoint and share the same envelope and delivery behavior; they differ only in the payload block (site vs domain).


Configuration

Billing webhooks are configured per billing plan by an account or reseller administrator in the SuperSpace admin UI. There is no public API endpoint to manage billing plans or their webhook settings.

Three fields on the plan control delivery:

Plan Webhook Settings
  • webhook_enabled: Boolean | Master on/off switch (default false).
  • webhook_url: String | The HTTPS endpoint that receives the POST.
  • webhook_auth: String | Sent verbatim as the Authorization request header value. Supply the full value you want — e.g. Bearer <secret>, Token <secret>, or a raw token. Omitted entirely if blank.

A webhook fires only when both webhook_enabled is true and webhook_url is present. Otherwise the event is silently dropped — no retry, no error.


Delivery Semantics

  • Method / body: HTTP POST with a JSON body to webhook_url.
  • Headers: Authorization: <webhook_auth> (verbatim; omitted if blank) and Accept: application/json.
  • Timeout: 30 seconds per attempt.
  • Success: any HTTP 2xx. Anything else — including a connection failure or timeout — is treated as a failed delivery.
  • Retries: failed deliveries are retried with backoff — every 2 minutes for the first 5 minutes, then every 5 minutes up to 10 minutes old, then every 15 minutes — and SuperSpace gives up 4 hours after the first attempt.
  • Asynchronous: delivery happens out of band, after the originating work completes. It is not part of the triggering API call's HTTP response.

Make your receiver idempotent

Because failed deliveries are retried, the same event may be delivered more than once. De-duplicate on the payload (e.g. resource id + action, plus subscription.updated_at for sites or expires_at for domains). Domain status/expiry events (registered, transferred_in, renewed, expired) are additionally claimed single-winner internally, so concurrent registry syncs emit each logical event only once — but retries still apply.


Events

Each delivery carries the event name in the top-level action field, and the resource block (site or domain) tells you which kind of event it is.

Site Subscription Events
  • created: A newly ordered site finished provisioning and its subscription became active.
  • resized: A site's plan or resources changed (a resize / plan-change completed).
  • owner_change: A site was transferred to a different owning account.
  • deleted: A site was deleted.

Domain-registration events carry a domain block. Billable events include a populated price; lifecycle-only events have price: null.

Domain Registration Events
  • registered: A new registration completed. Billable (registration price).
  • transferred_in: An inbound transfer completed. Billable (transfer price).
  • renewed: The registry expiry advanced (auto-renew, manual renew, or recovery renew). Billable (renewal price).
  • restored: A redemption recovery completed. Billableprice is the combined restore + renewal amount (one recovery invoice covers both).
  • registrant_change: A registrant (owner contact) change completed. Billable (registrant-change price).
  • privacy_enabled: WHOIS privacy was turned on. Billable (privacy price; may be null/$0 on some TLDs).
  • local_contact_added: A required local/registry contact was added. Billable (local-contact price).
  • transferred_out: The domain was transferred away to another registrar. Lifecycle-only (price: null).
  • expired: The domain passed its expiry (OK → EXPIRED). Lifecycle-only.
  • redemption: The domain entered the redemption / recovery phase. Lifecycle-only.
  • purged: A lapsed domain was purged (deleted at the registry). Lifecycle-only.
  • privacy_disabled: WHOIS privacy was turned off. Lifecycle-only.
  • local_contact_removed: A local/registry contact was removed. Lifecycle-only.
  • auto_renew_enabled: Auto-renew was turned on (by the customer, or a change made at the registry and synced back). Lifecycle-only.
  • auto_renew_disabled: Auto-renew was turned off (by the customer, or automatically when the domain is locked for non-payment). Lifecycle-only.

Same endpoint, both resource types

Site and domain webhooks share one plan endpoint. Route on the resource block — a site key (no object field) versus a domain key carrying "object": "domain_registration".


Payload

The POST body is a JSON object with two top-level keys: action (the event name) and a resource block — site for site events, domain for domain events.

Site payload

For site events the resource block is site (the same shape returned by GET /api/sites/{site-id}).

{
  "action": "created",
  "site": {
    "id": "83426e2f-58f4-4e7e-99bc-b76b07a31574",
    "name": "adminsacc-dbe11dadb7eb9f9e",
    "primary_domain": "youthful-buck49235.example.com",
    "created_at": "2024-09-25T22:29:21.612Z",
    "updated_at": "2024-09-25T22:33:13.589Z",
    "location": "pdx",
    "region": "pdx01",
    "bunny_id": "1234567",
    "bunny_cname": "youthful-buck49235.b-cdn.net",
    "package": "pro",
    "php_version": "8.3",
    "domain_cname": "youthful-buck49235.b-cdn.net",
    "sftp_base_path": "/home/sites/83426e2f",
    "dunning_suspended": false,
    "account": {
      "id": "16512906-c2b0-4c98-ac20-b1389838aa06",
      "name": "acme2"
    },
    "subscription": {
      "id": "6d8bf084-ed11-4378-8531-ef584da08439",
      "status": "active",
      "created_at": "2024-09-25T22:33:13.581Z",
      "updated_at": "2024-09-25T22:33:14.333Z",
      "price": { "amount_cents": 5000, "term": "monthly" },
      "product": { "id": "pro", "name": "Pro" }
    }
  }
}
Payload Fields
  • action: String | One of created, resized, owner_change, deleted.
  • site: Object
    • id: String | Site GUID.
    • name: String | Internal site name.
    • primary_domain: String | Current primary domain.
    • created_at: DateTime
    • updated_at: DateTime
    • location: String | Location short name (e.g. pdx).
    • region: String | Region short name (e.g. pdx01).
    • bunny_id: String | Bunny pull-zone ID.
    • bunny_cname: String | Bunny CDN CNAME.
    • package: String | Product / package short name.
    • php_version: String | Active PHP version.
    • domain_cname: String | CDN CNAME target (same value as bunny_cname).
    • sftp_base_path: String | SFTP base path.
    • dunning_suspended: Boolean | true when the site is locked behind an unpaid invoice. Note subscription.status can still read active while suspended.
    • account: Object | The owning account, unless overridden for sub-accounts (see below).
      • id: String
      • name: String
    • subscription: Object
      • id: String
      • status: String
      • created_at: DateTime
      • updated_at: DateTime
      • price: Object
        • amount_cents: Integer
        • term: String
      • product: Object
        • id: String
        • name: String

Domain payload

For domain events the resource block is domain. It carries "object": "domain_registration" so receivers can route, and a price block that is populated for billable events and null otherwise.

{
  "action": "registered",
  "domain": {
    "id": "b2c3d4e5-6789-4abc-9def-0123456789ab",
    "object": "domain_registration",
    "name": "example.com",
    "status": "OK",
    "expires_at": "2026-09-25T00:00:00.000Z",
    "auto_renew": true,
    "privacy_protect": false,
    "tld": "com",
    "recovery_phase": null,
    "account": {
      "id": "16512906-c2b0-4c98-ac20-b1389838aa06",
      "name": "acme2"
    },
    "price": {
      "amount_cents": 1200,
      "currency": "usd",
      "term": "yearly"
    }
  }
}
Domain Payload Fields
  • action: String | One of the domain events listed above.
  • domain: Object
    • id: String | Domain registration GUID.
    • object: String | Always domain_registration (use to distinguish from a site payload).
    • name: String | The domain name.
    • status: String | Registry status (e.g. OK, EXPIRED, PENDING_TRANSFER).
    • expires_at: DateTime | Current registry expiry.
    • auto_renew: Boolean | Whether auto-renew is enabled.
    • privacy_protect: Boolean | Whether WHOIS privacy is active.
    • tld: String | The TLD (e.g. com).
    • recovery_phase: String | Redemption/recovery phase, or null when not in recovery.
    • account: Object | The owning account, unless overridden for sub-accounts (see below).
      • id: String
      • name: String
    • price: Object | The amount billed for this event, or null for lifecycle-only events.
      • amount_cents: Integer
      • currency: String | Three-letter ISO code (the plan currency), e.g. usd.
      • term: String | e.g. yearly (for restored, the renewal term).

Sub-account billing

When the owning account is a sub-account of a different billing account, the payload identifies both: account is set to the billing (parent) account and an extra sub_account block (inside the resource block) names the actual owner. This applies to both site and domain payloads.

{
  "action": "resized",
  "site": {
    "id": "83426e2f-58f4-4e7e-99bc-b76b07a31574",
    "account":     { "id": "<billing-account-guid>", "name": "Parent Co" },
    "sub_account": { "id": "<owner-account-guid>",   "name": "Child Team" },
    "subscription": { "...": "..." }
  }
}

What does not trigger a webhook

Billing webhooks fire for the site and domain events listed above. The following do not emit a billing webhook — even when paid:

  • Backups, restores, cache, Shield, and other per-site task operations. Track these via task polling or a per-request completion callback.
  • Order / invoice mechanics (charges, refunds, payment status). Billing webhooks describe service lifecycle, not invoice state; for order-level detail use the Orders API and the registrar processes.

Billing webhooks vs. task completion callbacks

SuperSpace has two distinct push mechanisms. Don't confuse them:

Billing webhook Task completion callback
Configured on The billing plan (admin UI) Per request, in the callback object on POST /api/orders
Scope All site and domain lifecycle events under that plan The single task / order it was attached to
Fires for Site events (created, resized, …) and domain events (registered, renewed, …) Completion of that specific async task
Body { "action", "site" } or { "action", "domain" } { "timestamp", "success", "data" } (the task result)

See Callbacks for the per-request callback contract.