Registering a webhook
You can register up to 10 webhook URLs per store. Each can subscribe to a different list of events; pick one model that suits you:
- Single endpoint, all events — easiest for small apps. Branch on
eventin your handler. - Multiple endpoints, one event each — tidier in microservice setups, but more URLs to manage.
POST /v1/webhooks — register
Auth: platform key with webhooks:write. Requires Idempotency-Key.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
url | string (https URL) | ✅ | Must be http:// or https://. Production: always https://. Max length 500. |
events | string[] | ✅ | List of event names. See Event catalog for the allowed values. Empty = error. |
Request
curl -X POST 'https://api.dzbuild.app/v1/webhooks' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"url": "https://yourapp.example/webhooks/dzbuild",
"events": ["order.created", "order.confirmed", "order.shipped",
"order.cancelled", "signup.counted"]
}'
Response 201
{
"data": {
"id": 17,
"secret": "fc9b5f0b51b4a93c1d6f8e29b6a2e30c7c2c44a4f2a6c8d8e0e1b9d4f6c1a8b3",
"note": "Save the secret now — it is not retrievable after this response."
}
}
The secret is shown ONCE. Save it next to the webhook id in your secrets store. It's the value you'll use to verify every incoming POST. If you lose it, delete the webhook and create a new one.
Errors
| Code | Cause |
|---|---|
bad_request "url must be a valid http(s) URL" | Bad URL format |
bad_request "events must be a non-empty list" | Empty array |
bad_request "unknown event: foo. Allowed: …" | Event name not in catalog |
GET /v1/webhooks — list
Auth: platform key with webhooks:read.
curl https://api.dzbuild.app/v1/webhooks \
-H "Authorization: Bearer $DZ_KEY"
{
"data": {
"items": [
{
"id": 17,
"url": "https://yourapp.example/webhooks/dzbuild",
"events": ["order.created", "order.confirmed"],
"status": "active",
"last_success_at": "2026-04-30 21:18:23",
"last_failure_at": null,
"failure_count": 0,
"created_at": "2026-04-30 19:00:00"
}
],
"allowed_events": [
"order.created", "order.confirmed", "order.shipped", "order.delivered",
"order.cancelled", "order.returned", "payment.received",
"signup.counted", "event.recorded", "product.stock_low"
]
}
}
allowed_events is the canonical list — use it to populate dashboards in your own admin tools.
| Status | Meaning |
|---|---|
active | Receiving deliveries |
paused | Not receiving (you can pause via dashboard) |
dead | Auto-disabled after 5 dead-letters in a row. Contact support to revive. |
POST /v1/webhooks/{id}/test
Trigger a webhook.test delivery so you can verify your endpoint receives + verifies signatures correctly.
Auth: platform key with webhooks:write. Requires Idempotency-Key.
curl -X POST 'https://api.dzbuild.app/v1/webhooks/17/test' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Idempotency-Key: test-17-$(date +%s)"
{ "data": { "tested": true, "note": "a webhook.test delivery was enqueued; check your endpoint" } }
The test delivery looks like:
{
"event": "webhook.test",
"store_id": 13,
"occurred_at": "2026-04-30T21:24:17+00:00",
"data": { "ts": 1717112657 },
"delivery_id": "..."
}
It carries a real signature — your verification code can be tested end-to-end.
DELETE /v1/webhooks/{id}
Auth: platform key with webhooks:write. Requires Idempotency-Key.
curl -X DELETE 'https://api.dzbuild.app/v1/webhooks/17' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Idempotency-Key: del-17"
{ "data": { "deleted": true, "id": 17 } }
After delete:
- No new deliveries are queued.
- In-flight deliveries (already in the queue) are still attempted once.
- The webhook's secret is now useless — no one can sign with it.
Endpoint requirements
Your webhook URL must:
- Respond with 2xx on success — anything else is treated as an error (4xx ack, 5xx retried).
- Respond within 10 seconds. Slower → counted as timeout → retried.
- Accept
POSTwithContent-Type: application/json. - Read the raw body for signature verification (don't re-serialize).
- Be idempotent — same
delivery_idMAY arrive more than once.
A common pitfall in some frameworks: middleware re-encodes the JSON body before your handler sees it, so sha256(body) won't match. Solutions:
- Express: use
express.raw({ type: 'application/json' })for the webhook route, thenJSON.parse(req.body)in the handler. - Django:
request.bodyis the raw bytes — that's what you want. - Laravel:
$request->getContent()returns the raw body. - PHP raw:
file_get_contents('php://input').
See Verifying signatures for full code in 4 languages.