A production webhook endpoint must authenticate the sender, preserve each delivery until it can be processed, and remain safe when the same delivery arrives more than once.
Request contract#
Webhooks sends an HTTP POST with a JSON body and the following headers:
| Header | Description |
|---|---|
formance-webhook-id | Identifier for one delivery chain. It remains stable across automatic retries and, where replay is available, manual replay. |
formance-webhook-timestamp | Unix timestamp generated for this HTTP attempt. Each retry receives a new timestamp. |
formance-webhook-signature | One or more versioned signatures. The current format is v1,<base64-HMAC>. |
formance-webhook-idempotency-key | Source idempotency key, when the module that produced the event supplied one. Do not assume it is always present. |
formance-webhook-test | true for a request sent by the configuration test endpoint, otherwise false. |
Content-Type | application/json. |
Webhooks signs the raw request body, including whitespace and field ordering.
See the event reference for the Stack v3.2 envelope and payload catalog.
Verify the signature#
For signature version v1, Webhooks computes HMAC-SHA256 over:
The result is base64-encoded and sent as v1,<signature>. The verification helper compares signatures in constant time, but it does not enforce timestamp freshness. Apply your own tolerance after parsing the timestamp.
Do not parse and re-serialize the JSON before verification. Any byte-level change produces a different signature. Keep your server clock synchronized so a legitimate request is not rejected by the timestamp check.
Acknowledge after durable acceptance#
Webhooks treats every 2xx response as success. Return success after the event is stored in a system that survives process failure, such as a database-backed inbox or durable queue. If you respond before storing the event and then crash, Webhooks has no reason to retry it.
A typical receiver follows this sequence:
If validation or durable acceptance fails, return a non-2xx status that reflects whether another attempt could succeed.
Webhooks retries 408, 429, 5xx, transport errors, and timeouts. Other 4xx responses are terminal.
Make processing idempotent#
Webhook delivery can be duplicated at the HTTP boundary. A worker can lose its database connection after your endpoint accepted a request but before Webhooks recorded the successful attempt. Recovery can then send the event again.
Create a unique constraint on formance-webhook-id, then record the identifier and the business result in one transaction. If the identifier already exists, return the same successful response without applying the effect again. The delivery ID remains stable across automatic retries.
Manual replay also preserves the delivery ID.
Also make the business operation idempotent from stable domain identifiers in the payload when possible. Use formance-webhook-idempotency-key as additional context when present, but do not rely on it exclusively because producers can leave it empty.
Do not depend on delivery order#
Webhooks dispatches several deliveries concurrently. A retry can also allow a later event to arrive before an earlier one. If order matters, use the event's resource identifiers and timestamps to reconcile state, or fetch the current resource from its source API before applying a transition.
Test an endpoint#
Use the configuration test operation to exercise network access, signature verification, and response handling:
Test requests send {"data":"test"}, set formance-webhook-test: true, and are not retried or stored as durable deliveries. They test network access, signing, and the immediate endpoint response; they do not prove that a specific module event subscription or production payload handler is correct. Validate live event handling in a non-production Stack before enabling business effects.
Rotate a secret#
Secret rotation takes effect immediately; Webhooks does not provide a sender-managed dual-secret grace period. Deploy the new secret to your receiver in coordination with the rotation.
The dispatcher reads the current configuration before each attempt, so queued attempts use the new secret after rotation. A request that was already in flight can still carry the previous signature. During a controlled transition, let your receiver temporarily accept both secrets, rotate the Webhooks configuration, then remove the previous secret after in-flight requests have completed.