Aller au contenu principal

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 event in 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

FieldTypeRequiredNotes
urlstring (https URL)Must be http:// or https://. Production: always https://. Max length 500.
eventsstring[]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

CodeCause
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.

StatusMeaning
activeReceiving deliveries
pausedNot receiving (you can pause via dashboard)
deadAuto-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 POST with Content-Type: application/json.
  • Read the raw body for signature verification (don't re-serialize).
  • Be idempotent — same delivery_id MAY 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, then JSON.parse(req.body) in the handler.
  • Django: request.body is 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.