Billing webhooks
Billing webhooks zijn uitgaande notificaties die SuperSpace naar jouw
factuursysteem stuurt wanneer er iets gebeurt met een dienst op een facturatieplan. Ze laten een
externe biller — een plan dat klanten factureert buiten Stripe om
(payment_method: none) — lifecycle-wijzigingen in zijn eigen boekhouding spiegelen zonder
de API te pollen.
Twee resourcetypes versturen billing webhooks: sites (subscription-lifecycle) en
domeinregistraties (registry-lifecycle). Beide gaan naar hetzelfde plan-endpoint
en delen dezelfde envelope en hetzelfde leveringsgedrag; ze verschillen alleen in het
payload-blok (site vs domain).
Configuratie
Billing webhooks worden per facturatieplan geconfigureerd door een account- of reseller- beheerder in de SuperSpace admin-UI. Er is geen publiek API-endpoint om facturatieplannen of hun webhookinstellingen te beheren.
Drie velden op het plan sturen de levering aan:
Webhookinstellingen van het plan
- webhook_enabled: Boolean | Hoofd aan/uit-schakelaar (standaard
false). - webhook_url: String | Het HTTPS-endpoint dat de POST ontvangt.
- webhook_auth: String | Wordt letterlijk verstuurd als de waarde van de
Authorization-requestheader. Geef de volledige waarde op die je wilt — bijv.Bearer <secret>,Token <secret>, of een ruwe token. Wordt volledig weggelaten als hij leeg is.
Een webhook wordt alleen afgevuurd wanneer zowel webhook_enabled true is als
webhook_url aanwezig is. Anders wordt het event stilzwijgend genegeerd — geen retry, geen
fout.
Leveringssemantiek
- Methode / body: HTTP
POSTmet een JSON-body naarwebhook_url. - Headers:
Authorization: <webhook_auth>(letterlijk; weggelaten indien leeg) enAccept: application/json. - Timeout: 30 seconden per poging.
- Succes: elke HTTP
2xx. Al het andere — waaronder een verbindingsfout of timeout — wordt behandeld als een mislukte levering. - Retries: mislukte leveringen worden opnieuw geprobeerd met backoff — elke 2 minuten gedurende de eerste 5 minuten, daarna elke 5 minuten tot 10 minuten oud, daarna elke 15 minuten — en SuperSpace geeft het 4 uur na de eerste poging op.
- Asynchroon: de levering gebeurt buiten de band, nadat het oorspronkelijke werk is afgerond. Het maakt geen deel uit van het HTTP-antwoord van de triggerende API-aanroep.
Maak je ontvanger idempotent
Omdat mislukte leveringen opnieuw worden geprobeerd, kan hetzelfde event meer dan
één keer worden geleverd. Dedupliceer op de payload (bijv. resource-id + action, plus
subscription.updated_at voor sites of expires_at voor domeinen). Domein-
status-/vervalevents (registered, transferred_in, renewed, expired)
worden intern bovendien single-winner geclaimd, zodat gelijktijdige registry-
syncs elk logisch event maar één keer versturen — maar retries gelden nog steeds.
Events
Elke levering draagt de eventnaam in het action-veld op het hoogste niveau, en het
resource-blok (site of domain) vertelt je welk soort event het is.
Site-subscription-events
- created: Een nieuw bestelde site is klaar met provisioning en de subscription is actief geworden.
- resized: Het plan of de resources van een site zijn gewijzigd (een resize / planwijziging is afgerond).
- owner_change: Een site is overgedragen aan een ander eigenaarsaccount.
- deleted: Een site is verwijderd.
Domeinregistratie-events dragen een domain-blok. Factureerbare events bevatten een
gevulde price; lifecycle-only-events hebben price: null.
Domeinregistratie-events
- registered: Een nieuwe registratie is afgerond. Factureerbaar (registratieprijs).
- transferred_in: Een inkomende verhuizing is afgerond. Factureerbaar (verhuisprijs).
- renewed: De registry-vervaldatum is opgeschoven (auto-renew, handmatige verlenging of herstelverlenging). Factureerbaar (verlengingsprijs).
- restored: Een redemption-herstel is afgerond. Factureerbaar —
priceis het gecombineerde herstel- + verlengingsbedrag (één herstelfactuur dekt beide). - registrant_change: Een houderwijziging (eigenaarscontact) is afgerond. Factureerbaar (prijs voor houderwijziging).
- privacy_enabled: WHOIS-privacy is ingeschakeld. Factureerbaar (privacyprijs; kan
null/$0 zijn bij sommige TLD's). - local_contact_added: Een vereist lokaal/registry-contact is toegevoegd. Factureerbaar (prijs voor lokaal contact).
- transferred_out: Het domein is verhuisd naar een andere registrar. Lifecycle-only (
price: null). - expired: Het domein is over zijn vervaldatum heen (OK → EXPIRED). Lifecycle-only.
- redemption: Het domein is in de redemption- / herstelfase terechtgekomen. Lifecycle-only.
- purged: Een vervallen domein is gepurged (verwijderd bij de registry). Lifecycle-only.
- privacy_disabled: WHOIS-privacy is uitgeschakeld. Lifecycle-only.
- local_contact_removed: Een lokaal/registry-contact is verwijderd. Lifecycle-only.
- auto_renew_enabled: Auto-renew is ingeschakeld (door de klant, of een wijziging bij de registry die teruggesynchroniseerd is). Lifecycle-only.
- auto_renew_disabled: Auto-renew is uitgeschakeld (door de klant, of automatisch wanneer het domein wegens niet-betaling wordt vergrendeld). Lifecycle-only.
Hetzelfde endpoint, beide resourcetypes
Site- en domeinwebhooks delen één plan-endpoint. Route op het resource-blok
— een site-key (geen object-veld) versus een domain-key met
"object": "domain_registration".
Payload
De POST-body is een JSON-object met twee keys op het hoogste niveau: action (de eventnaam)
en een resource-blok — site voor site-events, domain voor domein-events.
Site-payload
Voor site-events is het resource-blok site (dezelfde shape die wordt teruggegeven door
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-velden
- action: String | Een van
created,resized,owner_change,deleted. - site: Object
- id: String | Site-GUID.
- name: String | Interne sitenaam.
- primary_domain: String | Huidig primair domein.
- created_at: DateTime
- updated_at: DateTime
- location: String | Korte naam van de locatie (bijv.
pdx). - region: String | Korte naam van de regio (bijv.
pdx01). - bunny_id: String | Bunny pull-zone-ID.
- bunny_cname: String | Bunny CDN-CNAME.
- package: String | Korte naam van het product / pakket.
- php_version: String | Actieve PHP-versie.
- domain_cname: String | CDN-CNAME-doel (zelfde waarde als
bunny_cname). - sftp_base_path: String | SFTP-basispad.
- dunning_suspended: Boolean |
truewanneer de site is geblokkeerd achter een onbetaalde factuur. Let op:subscription.statuskan nog steedsactivezijn terwijl de site geblokkeerd is. - account: Object | Het bezittende account, tenzij overschreven voor sub-accounts (zie hieronder).
- 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
Domein-payload
Voor domein-events is het resource-blok domain. Het draagt
"object": "domain_registration" zodat ontvangers kunnen routeren, en een price-blok
dat gevuld is voor factureerbare events en anders null is.
{
"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"
}
}
}
Domein-payload-velden
- action: String | Een van de hierboven genoemde domein-events.
- domain: Object
- id: String | Domeinregistratie-GUID.
- object: String | Altijd
domain_registration(gebruik dit om onderscheid te maken met eensite-payload). - name: String | De domeinnaam.
- status: String | Registry-status (bijv.
OK,EXPIRED,PENDING_TRANSFER). - expires_at: DateTime | Huidige registry-vervaldatum.
- auto_renew: Boolean | Of auto-renew is ingeschakeld.
- privacy_protect: Boolean | Of WHOIS-privacy actief is.
- tld: String | De TLD (bijv.
com). - recovery_phase: String | Redemption-/herstelfase, of
nullwanneer niet in herstel. - account: Object | Het bezittende account, tenzij overschreven voor sub-accounts (zie hieronder).
- id: String
- name: String
- price: Object | Het bedrag dat voor dit event in rekening is gebracht, of
nullvoor lifecycle-only-events.- amount_cents: Integer
- currency: String | Drieletterige ISO-code (de plan-valuta), bijv.
usd. - term: String | Bijv.
yearly(voorrestored, de verlengingstermijn).
Sub-accountfacturatie
Wanneer het bezittende account een sub-account van een ander facturatieaccount is,
identificeert de payload beide: account wordt ingesteld op het facturatie- (parent-)account en
een extra sub_account-blok (binnen het resource-blok) benoemt de werkelijke eigenaar.
Dit geldt voor zowel site- als 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": { "...": "..." }
}
}
Wat geen webhook triggert
Billing webhooks worden afgevuurd voor de hierboven genoemde site- en domein-events. Het volgende verstuurt geen billing webhook — zelfs niet wanneer het betaald is:
- Back-ups, restores, cache, Shield en andere per-site taakoperaties. Volg deze via task polling of een per-request completion-callback.
- Order- / factuurmechaniek (charges, refunds, betaalstatus). Billing webhooks beschrijven de service-lifecycle, niet de factuurstatus; gebruik voor detail op orderniveau de Orders-API en de registrar-processes.
Billing webhooks vs. task completion callbacks
SuperSpace heeft twee verschillende push-mechanismen. Verwar ze niet:
| Billing webhook | Task completion callback | |
|---|---|---|
| Geconfigureerd op | Het facturatieplan (admin-UI) | Per request, in het callback-object op POST /api/orders |
| Scope | Alle site- en domein-lifecycle-events onder dat plan | De enkele taak / order waaraan het is gekoppeld |
| Vuurt voor | Site-events (created, resized, …) en domein-events (registered, renewed, …) |
Afronding van die specifieke async-taak |
| Body | { "action", "site" } of { "action", "domain" } |
{ "timestamp", "success", "data" } (het taakresultaat) |
Zie Callbacks voor het per-request-callbackcontract.