Ga naar inhoud

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 POST met een JSON-body naar webhook_url.
  • Headers: Authorization: <webhook_auth> (letterlijk; weggelaten indien leeg) en Accept: 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. Factureerbaarprice is 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 | true wanneer de site is geblokkeerd achter een onbetaalde factuur. Let op: subscription.status kan nog steeds active zijn 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 een site-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 null wanneer 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 null voor lifecycle-only-events.
      • amount_cents: Integer
      • currency: String | Drieletterige ISO-code (de plan-valuta), bijv. usd.
      • term: String | Bijv. yearly (voor restored, 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.