Last updated
Webhooks#
Receive asynchronous updates securely, acknowledge them durably, and reconcile delayed or repeated delivery.
When to use webhooks#
Some workflows finish after the initial API response. Transfers can remain pending, provider processing can take time, and account verification can require further review. A webhook lets the backend send an update to a registered HTTPS endpoint without requiring continuous status polling.
Treat a webhook as an authenticated notification about a resource, not as an instruction to create another payment. Correlate it with a saved operation reference and apply only the transitions allowed by the resource's contract.
Register a destination#
Use a stable HTTPS URL controlled by the receiving application. Keep sandbox and production destinations separate, and configure the endpoint to accept only the documented method and content type.
- Store the webhook verification secret separately from the outbound API secret unless the documented protocol explicitly uses the same key.
- Limit request size before buffering the body.
- Preserve the raw request body for signature verification.
- Avoid authentication redirects, interactive sessions, and CSRF flows that assume a human browser.
- Monitor endpoint availability and register a support contact for delivery failures.
A private network or IP restriction may add protection when officially supported, but it does not replace signature verification.
Verify before processing#
Verify the signature against the exact raw bytes before parsing or applying the event. Validate a signed timestamp or nonce when the protocol includes one. Compare signature buffers in constant time and reject malformed signatures before comparison.
The Node.js example below defines an illustrative protocol: example-signature contains a hexadecimal HMAC-SHA256 of the raw body. It is deliberately not presented as a deployed KlemPay header. Adapt it only after confirming the real protocol, including any timestamp signing input and secret rotation rules.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyExampleSignature(rawBody, suppliedHex, secret) {
if (!secret || typeof suppliedHex !== "string") return false;
if (!/^[a-f0-9]{64}$/i.test(suppliedHex)) return false;
const expected = createHmac("sha256", secret).update(rawBody).digest();
const supplied = Buffer.from(suppliedHex, "hex");
return supplied.length === expected.length && timingSafeEqual(supplied, expected);
}Do not reconstruct JSON before verifying. Parsing and serialising a body can change bytes even when the resulting object has the same values. Never log the verification secret or a complete signature-bearing request.
Acknowledge durable receipt#
After authentication and schema validation, record the event in durable storage or a durable queue. Return the documented successful acknowledgement only after the receiver has accepted responsibility for processing it.
Slow business work should run outside the request handler. Acknowledging before durable storage risks losing the event when the process restarts; waiting for every downstream service can cause unnecessary redelivery.
// Request body size is capped before rawBody is created.
if (!verifyExampleSignature(rawBody, suppliedSignature, webhookSecret)) {
return new Response("Invalid signature", { status: 401 });
}
const event = parseAndValidateEvent(rawBody); // Reject unexpected shapes.
// insertOnce uses a unique constraint on the provider's event identifier.
await inbox.insertOnce({ eventId: event.id, payload: event, receivedAt: new Date() });
// A worker reads the durable inbox and performs the business transition.
return new Response(null, { status: 204 });The acknowledgement status in this example is illustrative. Use the actual delivery contract's accepted status and body. Do not assume a particular timeout, retry count, or delivery ordering guarantee.
Handle duplicates and ordering#
Deliveries can repeat when an acknowledgement is lost. Deduplicate using the documented unique event identifier and make business processing idempotent as well. A unique event record alone does not protect a ledger effect if a worker can crash between applying the effect and marking the event complete.
Use an atomic database transaction when updating the business state and completing the inbox record. For external side effects, use a durable outbox and a stable operation reference accepted by the destination.
| Situation | Receiver behaviour |
|---|---|
| Event identifier already stored | Acknowledge the authenticated duplicate without applying its effects again. |
| Event arrives before its resource record | Queue for reconciliation or retrieve the resource through an approved endpoint. |
| Older pending update follows a final state | Preserve the valid final state; record the late event for diagnostics. |
| Unknown event type | Follow the documented compatibility policy; do not guess a financial action. |
| Schema is invalid | Reject or quarantine according to the contract; alert with redacted diagnostics. |
| Worker processing fails | Keep the durable event pending and retry through a bounded worker policy. |
Reconcile with the API#
If an event is missing, contradictory, or incomplete, retrieve the resource using its stored reference. The API's authoritative state should guide recovery. Webhooks and status polling should converge on the same state machine rather than updating independent copies of a transaction.
For a pending payment, keep the reference available to the user and provide a status check. Do not report a payment as failed solely because a webhook has not arrived.
Monitor and test#
Track receipt time, event identifier, resource reference, processing outcome, attempt count, and processing age. Keep payload access restricted and redact personal data in operational logs.
- Test a valid event, invalid signature, malformed body, and oversized request.
- Test duplicate and out-of-order delivery.
- Restart the worker after durable receipt and before processing.
- Confirm database constraints prevent duplicate effects under concurrency.
- Verify secret rotation using the provider's documented overlap policy.
- Alert when pending inbox age grows or the receiver repeatedly rejects delivery.