Webhooks
Receive and verify order-status events
When an API-placed order reaches a terminal status — or pauses awaiting a code from your
customer — Voodoo Center POSTs a signed JSON event to your configured webhook URL.
This is the recommended way to learn an order’s outcome — it is push-based, so you don’t poll.
When webhooks fire
A webhook is sent when all of these are true:
- The order reaches a terminal status:
completed,failedorpartial. - Or the order pauses on
need_client_code— the one NON-terminal event, telling you to collect a confirmation code from your customer and submit it viaPOST /v1/orders/{id}/client-code. The order does not move until you do. See Orders awaiting a customer code. - The order was placed through the API (
source: "api"). Dashboard orders do not trigger webhooks. - A webhook URL is configured for your account.
Payload
The request body is compact, sorted-keys JSON with these fields:
The webhook body is not identical to GET /v1/orders/{id}. A partial order that timed out
shows error: "ORDER_TIMEOUT" on the API but sends error: "" in the webhook — the payload only
carries an error code for failed. If you need the error code on a partial, read the order back
from the API rather than trusting the event body.
Verifying the signature
Every webhook carries a header:
where <hex> is HMAC-SHA256(webhook_secret, raw_request_body_bytes).
Verify against the raw received bytes, exactly as delivered — never against a dict you re-serialized. The signed body is compact, sorted-keys JSON, so any re-encoding (different key order, spacing or number formatting) produces a different signature and fails verification.
Compute the same HMAC with your signing secret and compare it to the header using a constant-time comparison.
FastAPI receiver + verifier
This receiver verifies the raw bytes before parsing, uses a constant-time
compare, and returns 200 quickly:
Acknowledging, retries and timeouts
- Acknowledge with HTTP
200as soon as you have stored the event. Any non-200response (or a timeout) counts as a failed delivery. - Failed deliveries are retried up to 3 attempts, 60 seconds apart.
- Each attempt has a 30-second timeout.
- Redirects are not followed, and your URL must be publicly routable — a server-side request forgery (SSRF) guard blocks internal/private addresses.
Because a single event may be delivered more than once (e.g. a retry after your
200 was slow), make your handler idempotent: dedupe on order_id and
treat re-delivery of an already-processed event as a no-op.
Do heavy work (fulfilling your own downstream order, sending email, etc.) in the
background and return 200 fast — a slow handler risks the 30-second timeout
and an unnecessary retry.
Configuring the URL and rotating the secret
Both webhook settings live on the dashboard API page (dashboard-only, not API endpoints):
- Webhook URL — where events are
POSTed. Must be publicly reachable over HTTPS. - Signing secret — used to compute
X-Signature. Formatwhsec_..., shown once at creation/rotation. Rotate it if it may have leaked, and updateWEBHOOK_SECRETin your receiver to match.