Overview

kurnl sends HTTP POST requests to your webhook_url after significant events. The webhook_url lives on your provider integration settings (tenant-config); the signing secret is the Shared webhook secret from the dashboard Checkout page.
Webhooks are inert until webhook_url is configured — events are silently skipped, with no error. And if no webhook secret is set, deliveries arrive unsigned (no X-Webhook-Signature header): configure a secret and reject unsigned requests.
All events share a common envelope structure with event-specific fields merged in at the top level.

Event reference

On every event: subscription_id, external_subscription_id (your own identifier; null when the line has none) and is_anonymous. They are additive: nothing you already read has moved.What is not emitted: there is no subscription.suspended event, and a restore you request through the status call sends no new subscription.activated event. Also mind the payload variants: subscription.activated from a provider switch carries kind, activated_at and plan_version_id like every activation, plus started_at and via: "provider_switch", but no location_hash or subscriber_id, so don’t key on those unconditionally.
subscription.activated used to be silent on the home-drop and confirm-install paths. It now fires there too, with kind: "first". A line you cancelled and kurnl later brings back sends it again with kind: "reactivated". An example of the new visit event:

Backup e-mail for anonymous lines

For an anonymous line, kurnl can also send every one of these events to an e-mail address of your choice, next to the webhook. It is a safety net, for the day your endpoint is down or a delivery is dropped. Set the address under Backup notification e-mail on the dashboard Checkout page; the Send notification e-mails switch turns it off. It works without a webhook_url too. The mails come from kurnl’s status address. Lines of named subscribers send no backup mail, and neither do port offline or port restored.

Payload structure

All payloads include event, provider_document_id, and timestamp. Event-specific fields are merged in alongside them.

Verifying signatures

When you have a webhook_secret configured, kurnl signs every request body with HMAC-SHA256 and sends the signature in the X-Webhook-Signature header as sha256=<hex_digest>. Always verify this signature before processing any event.

Delivery behaviour

  • kurnl first attempts delivery up to 3 times inline, with exponential back-off (2s, then up to 30s between retries) and a 15 second timeout per attempt
  • If all inline attempts fail, the event moves to a dead-letter queue: a scheduler (every 10 minutes) redelivers it with growing back-off until 5 total attempts have been made, then the event is dropped and kurnl operations is alerted
  • Every delivery carries an X-Webhook-Event header with the event name; DLQ redeliveries additionally carry X-Webhook-Retry: true — use it for dedup awareness
  • Network errors and 5xx responses are retried; 4xx responses are not retried (a 4xx during DLQ redelivery ends retrying immediately)
  • Return any 2xx status to acknowledge — kurnl does not inspect the response body

Idempotency

kurnl may deliver the same event more than once after transient failures. Use job_id or subscription_id as an idempotency key to deduplicate in your handler:

Testing webhooks locally

During development you can use a tunnelling tool to expose your local server to kurnl:
Alternatively, use webhook.site for a no-setup temporary receiver to inspect payloads without running any code.

Debugging delivery failures

The sandbox has its own webhook event feed — but it is a different pipeline with different event names than the production events on this page. See Sandbox for how to configure, list, and replay sandbox webhook events. For production issues, remember the delivery behaviour above: failed events are redelivered from the dead-letter queue with X-Webhook-Retry: true, and events that exhaust all attempts alert kurnl operations.