API v1 does not enforce a per-store webhook limit — register as many URLs as your integration genuinely needs, and clean up the ones you stop using. (The no-code Merchant Webhooks addon does cap endpoints: 1 on Unlimited, 3 on Enterprise.)
Each webhook 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.
⚠️ Warning — Pilot access
API v1 is pilot-gated in production. A valid key is not enough — DZBuild must enrol it in the pilot, otherwise every call returns 403 forbidden "API is in pilot mode; key not enrolled".
POST /v1/webhooks — register
Auth: platform key with webhooks:write. Requires Idempotency-Key.
Body
Field | Type | Required | Notes |
| string (https URL) | ✅ | Must be |
| 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 200
{
"data": {
"id": 17,
"secret": "fc9b5f0b51b4a93c1d6f8e29b6a2e30c7c2c44a4f2a6c8d8e0e1b9d4f6c1a8b3",
"note": "Save the secret now — it is not retrievable after this response."
},
"meta": { "request_id": "...", "api_version": "v1" }
}
💡 Tip — Never branch on == 201
No create endpoint in v1 returns 201 today — /v1/webhooks, /v1/products, /v1/orders, /v1/landing-pages and /v1/keys all answer 200 on success. Branch on any 2xx instead.
The secret is shown ONCE. Save it next to the webhook id in your secrets store. Note that it is not currently the key API v1 deliveries are signed with — see Signature — so today it is a value to keep for later rather than one you can verify with. If you lose it, delete the webhook and create a new one.
Errors
HTTP | Code / Message | Cause |
400 |
| Bad URL format |
400 |
| Empty array |
400 |
| Event name not in catalog |
400 |
| Missing |
400 |
| Key too long, or contains characters outside that set (base64 |
403 |
| Key exists but isn't pilot-enrolled |
500 |
| Usually a |
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 list the API will accept at registration — but it is wider than what actually fires. payment.received, event.recorded and product.stock_low are accepted and then never emitted. Check the Event catalog before you build against one.
Status | Meaning |
| Receiving deliveries. In practice this is the only value you will ever see. |
| Reserved for future use — nothing sets it today, and there is no dashboard page for API v1 webhooks. |
| Reserved for future use. There is no auto-disable; |
To stop deliveries, delete the webhook.
POST /v1/webhooks/{id}/test
Trigger a webhook.test delivery so you can check your endpoint is reachable and see the envelope shape.
webhook.test cannot be subscribed to: putting it in the events array at registration returns 400 bad_request "unknown event: webhook.test. Allowed: …". A test delivery is sent to the target webhook regardless of what that webhook subscribes to.
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": "9f2c41ab77e05d18"
}
Two things to know about it:
It is queued, not synchronous. The response only confirms the delivery was enqueued; the POST itself arrives shortly after, typically within a minute.
It cannot validate your verification code. Like every API v1 delivery, its signature is not derived from your webhook's
secret— so it proves reachability and payload shape, nothing more. See Signature.
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. - Every queued delivery for that webhook is discarded immediately. Nothing is "attempted one last time". If you care about the backlog, pause your writes and let the queue drain (usually under a minute) before deleting. - The webhook's secret is now useless.
Idempotency-Key replay on test and DELETE
The API caches the response for each Idempotency-Key for 24 hours and replays it with an Idempotency-Replay: 1 header. That has two consequences:
Reusing a literal key like
del-17within 24 hours replays the cached response instead of performing a new call. Use a fresh key (or one that encodes the attempt) whenever you really want the operation to run.Error responses are cached too. A botched write replays its own 4xx for 24 hours under the same key — change the key after you fix the request.
Replay is guaranteed for 24 hours whichever host you call, and always answers with Idempotency-Replay: 1. Prefer https://api.dzbuild.app/v1 anyway: the dzbuild.com/api/v1 path is only an alias, it does not get the 30-second read cache, and some request paths can be blocked there.
Endpoint requirements
Your webhook URL must:
Respond with 2xx on success. 4xx and 3xx are one-shot failures; 5xx is re-POSTed every minute until it stops (see Retry behaviour).
Answer within 10 seconds total (5 seconds to connect). Slower counts as a timeout, and a timeout is not retried — the delivery is abandoned.
Serve valid TLS. Certificates are strictly verified, so self-signed certs fail with no retry.
Don't redirect.
Locationis not followed; a301/302is a failure.Accept
POSTwithContent-Type: application/json.Read the raw body if you hash anything (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.