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
Authorizationrequest 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
POSTwith a JSON body towebhook_url. - Headers:
Authorization: <webhook_auth>(verbatim; omitted if blank) andAccept: 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. Billable —
priceis 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 |
truewhen the site is locked behind an unpaid invoice. Notesubscription.statuscan still readactivewhile 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 asitepayload). - 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
nullwhen 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
nullfor lifecycle-only events.- amount_cents: Integer
- currency: String | Three-letter ISO code (the plan currency), e.g.
usd. - term: String | e.g.
yearly(forrestored, 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.