Build a reliable webhook receiver
On this page
A webhook connects committed OpenQuoteStack events to another server. Delivery is asynchronous and at least once. Build the receiver around signature verification, durable acceptance and event deduplication, not an assumption that each event arrives once or in order.
Choose events and a destination
In Settings → Webhooks, add your public HTTPS receiver and select only the events it needs. The supported events are:
estimate.created
estimate.completed
lead.created
lead.updated
estimate.status_changed
estimator.publishedThe payload includes id, type, createdAt, organizationId and resource data captured at commit. Lead events contain contacts; estimate events contain answers and calculations. Sending them to a receiver is a customer-data flow, so choose a system permitted to handle that information.
Copy the signing secret when shown and store it privately on the receiver. It is not an API key. OpenQuoteStack encrypts its stored copy with OQS_ENCRYPTION_KEY; preserve that installation key in backups.
Verify unchanged bytes first
The signature header has this format:
OQS-Signature: t=UNIX_SECONDS,v1=LOWERCASE_HEX_HMACThe signed message is the timestamp, a dot, and the exact raw request body. JSON parsing and reserialization can change whitespace or key order and invalidate a correct signature. Capture raw bytes before middleware transforms them.
When using the locally built SDK:
import { verifyWebhook } from "@openquotestack/sdk";
const signature = request.headers.get("oqs-signature");
const rawBody = new Uint8Array(await request.arrayBuffer());
if (!signature || !(await verifyWebhook(secret, signature, rawBody))) {
return new Response(null, { status: 401 });
}
const event = JSON.parse(new TextDecoder().decode(rawBody));This is the verification section of a receiver, not a complete delivery processor. Bound body size in the server or reverse proxy before buffering it. Validate the event envelope and expected event type before applying business changes.
The verifier's default timestamp tolerance is five minutes. Keep both servers' clocks synchronized. Do not disable age checks merely to accept an old captured delivery.
Accept events durably
Use the event ID as a deduplication identity. A retry keeps that ID and the delivery ID but gets a fresh timestamp and signature.
A useful consumer transaction is:
- Insert the event into a durable inbox with a unique event-ID constraint.
- If the ID already exists, return a successful response without repeating the effect.
- Commit the inbox record before returning 2xx.
- Process it asynchronously or perform the business mutation in the same transaction.
Recording an ID and then performing a non-transactional effect can lose work if the process crashes between the two operations. Performing the effect first can duplicate it if the process crashes before recording the ID. Design the consumer for its actual side effect.
The repository's Node receiver example demonstrates raw-body verification and atomic local event-file storage. It is a learning fixture, not a substitute for your application's production transaction model.
Handle ordering explicitly
Events may arrive out of order. Do not assume a lead update always follows its creation delivery or that status changes arrive in the order the business performed them.
Use event timestamps, retained resource IDs and an appropriate conflict strategy. If a downstream record needs the latest state, reconcile it with the authenticated API rather than treating arrival order as truth. Signature timestamps authenticate delivery time; event createdAt describes the original event.
Understand retry decisions
OpenQuoteStack uses up to five attempts with approximate delays of 10 seconds, one minute, five minutes and fifteen minutes, plus small jitter. Network failures, 408, 429 and 5xx retry. Other 3xx/4xx fail; redirects are not followed. Any 2xx is successful.
Return a transient failure only when retrying is appropriate. A 401 for an incorrect secret will not automatically recover through retries. A 2xx before durable acceptance can permanently lose the event from your consumer's point of view.
Inspect Recent deliveries for HTTP code, duration, attempts and safe error category. Failed deliveries on active endpoints can be retried manually after correcting the cause. Receiver response bodies are discarded.
Respect destination restrictions
Destinations require public DNS hostnames, HTTPS and port 443. Literal IPs, credentials, fragments, private/reserved addresses and mixed public/private DNS results are rejected. Every attempt rechecks DNS and pins the accepted address while verifying the TLS hostname.
A local development receiver on port 4000 therefore cannot be registered directly. Put it behind a controlled public HTTPS ingress for delivery testing, or test signature verification locally with a fixture. There is no unsafe-destination override.
See the webhook reference for the contract and troubleshooting for worker checks.