Webhooks
Webhooks deliver real-time HTTP notifications when domain events happen in Kontorion - invoice finalized, subscription transitioned, credit note applied. Each delivery is HMAC-SHA256-signed, idempotency-keyed, and retried on a fixed schedule until it succeeds or exhausts. Endpoints filter on event type, share secrets via the rotation surface, and have an operator-driven replay path for individual deliveries.
For the catalog of every event type and payload shape, see Webhook Events.
How delivery works
Code
One originating event fans out to every endpoint subscribed to its type for the org. Each endpoint gets its own delivery row keyed on (endpoint_id, idempotency_key) - the dedupe scope means a broker resend or dispatcher retry collapses to a single HTTP call per endpoint.
Setting up an endpoint
Endpoints are configured in two steps: create a signing secret, then register the endpoint that references it.
1. Create a signing secret
Code
The response carries the plaintext signing key - only at creation time:
Code
Store signing_key in your application's secret manager. After this response only key_prefix is ever returned for inspection - the full key is unrecoverable. To rotate, use Rotation.
2. Register the endpoint
Code
event_types is required and must contain at least one entry. Events outside that set are not delivered to this endpoint - wildcard subscriptions don't exist; list every event type explicitly.
secret_id is optional. An endpoint without a secret won't carry an X-Webhook-Signature header - useful for development tunnels but never recommended for production.
Endpoint statuses: active (default) or disabled (skipped by the dispatcher). Update the URL or event-type set by deleting + recreating the endpoint; mid-life mutation isn't supported.
Envelope shape
Every POST body has the same envelope around the typed payload:
Code
| Field | Notes |
|---|---|
id | Per-delivery UUID. Unique to this endpoint's copy of the event - different endpoints subscribed to the same event get different ids. Use idempotency_key (not id) for cross-endpoint dedup |
type | The event type - see Webhook Events |
schema | Payload shape version. Currently "v1". Bumped only for non-additive changes - additive fields are added without a schema bump |
idempotency_key | UUID of the originating action. Stable across broker resends, dispatcher retries, and your endpoint's HTTP retries. The field to deduplicate on |
created_at | ISO 8601 UTC timestamp of the originating event |
payload | Typed body - shape documented per event type |
Verifying signatures
Signed deliveries carry:
Code
Where <hex> is the lowercase hex of HMAC-SHA256(signing_key, raw_body).
Code
Code
Use a constant-time comparison (timingSafeEqual / compare_digest) to avoid timing leaks. Verify against the raw body bytes - JSON re-serialisation will change the hash even when the parsed object is equivalent.
Retry schedule
If your endpoint returns a non-2xx status (or the connection fails), Kontorion retries on a fixed schedule measured from the previous attempt:
| After attempt | Next retry |
|---|---|
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 12 hours |
After 6 total attempts the delivery is marked exhausted and stops retrying. The default cap can be tightened (not raised - the schedule is fixed) per organization via the webhook_max_retries setting; once a delivery has the cap stamped on it, lowering the org default does not retroactively shorten in-flight retries.
Delivery statuses: pending → terminal delivered / exhausted, with failed as the transient state between retries.
Idempotency on your side
Your endpoint may receive the same event more than once - broker resends, dispatcher retries, and your own HTTP retries are all part of the at-least-once guarantee. Deduplicate on idempotency_key, not id. id is per-delivery and changes between retries to the same endpoint; idempotency_key is stable for the originating action.
Code
Return 2xx as soon as you've persisted the dedupe row; do downstream work asynchronously. The dispatcher considers your endpoint a failure on a non-2xx response or a hung connection - keep the synchronous path short.
Operator replay
Individual deliveries can be replayed by ID:
Code
This re-POSTs the original envelope with a fresh id (the new delivery row) but the same idempotency_key - your endpoint's dedupe layer treats it correctly. Use it after fixing a downstream bug, never as a fan-out trigger.
GET /v1/webhook-endpoints/{id}/deliveries lists deliveries for an endpoint with their status and last error.
Rotation
To rotate a signing key without dropped deliveries:
POST /v1/webhook-secrets/{id}/rotate- generates a fresh key against the same secret row and returns the new plaintext exactly once- Configure your endpoint to verify against both the old and new key
- Wait for the longest retry window (12h) so any in-flight retries from before the rotation drain
- Drop the old key from your verifier
Deleting a secret (DELETE /v1/webhook-secrets/{id}) takes effect immediately - any endpoint still pointing at it stops getting signed deliveries until you reassign.
Inbound webhooks (other direction)
The reverse path - Kontorion receiving webhooks from external systems - runs through dedicated unauthenticated endpoints with their own signature schemes:
| Endpoint | Source |
|---|---|
POST /v1/webhooks/stripe | Stripe events (signed with Stripe-Signature against the gateway's stored webhook_secret) - see Payments |
POST /v1/webhooks/einvoicing | Peppol forwarder lifecycle events (HMAC-authed) |
POST /v1/webhooks/{entity_group}/{id} | Generic inbound channel for org-side integrations (X-Webhook-Signature: v1=<hex>) |
These are inbound only - they don't appear in the outbound delivery surface and have separate auth.
Endpoints
Webhook Endpoints, Webhook Secrets, Webhook Deliveries.
Related
- Webhook Events - every event type and its payload shape
- Authentication - Bearer-token setup
- Errors - non-2xx response shape your endpoint should handle