Webhooks
A webhook delivers every domain event to an HTTPS endpoint you control.
Create
POST /v1/webhookscurl -X POST http://api.localhost/v1/webhooks \
-H "X-API-Key: $NOTIFYZR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/hooks/notifyzr" }'url must be an absolute http(s) URL, up to 2,048 characters — anything else is 422 notifyzr.webhook.error.invalid_url.
The response includes a signing secret, shown once. Store it: it is how you verify a delivery came from Notifyzr.
List and delete
GET /v1/webhooks
DELETE /v1/webhooks/{id}There is no update and no per-event subscription — a webhook receives everything. Filter on X-Notifyzr-Event at your end.
What a delivery looks like
| Header | Contents |
|---|---|
X-Notifyzr-Event | The event type, e.g. contact.created |
X-Notifyzr-Signature | HMAC-SHA256 of the raw body, hex-encoded |
Verify against the raw bytes, before any JSON parsing, with a constant-time comparison:
$expected = hash_hmac('sha256', $rawBody, $secret);
if (!hash_equals($expected, $request->getHeaderLine('X-Notifyzr-Signature'))) {
http_response_code(401);
return;
}const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
const given = req.header('X-Notifyzr-Signature') ?? ''
if (expected.length !== given.length ||
!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given))) {
return res.sendStatus(401)
}Events
Events are named resource.verb, and this is the whole catalog:
| Event | Emitted when |
|---|---|
contact.created / contact.updated / contact.deleted | A contact changes |
contact.unsubscribed / contact.resubscribed | Opt-out changes — only on a real state change. segment_id is null for an opt-out from all email. |
contact_import.completed | A CSV import finishes writing |
form.submitted | A hosted form receives a submission |
email.queued / email.scheduled | A send is accepted |
email.delivered / email.failed | A worker finishes a send |
broadcast.sent | A fan-out completes |
channel_message.sent / channel_message.failed | A Discord, Telegram or ntfy push finishes |
domain.created / domain.verified / domain.deleted | A sending domain changes |
domain.verification_lost | The scheduled re-check demoted a verified domain |
The same catalog drives automations — an integration you build and an automation someone configures react to exactly the same events.
Delivery behaviour
Deliveries are queued, not made inline with the request that caused them, so a slow endpoint of yours never slows the API call that triggered it.
Failures are retried, each attempt waiting 30 seconds longer than the last; exhausted retries are dead-lettered rather than dropped.
Emitting is best-effort by design. If the broker is unavailable the failure is logged and the originating request still succeeds — Notifyzr will not fail a customer's send because your endpoint was unreachable.
Return quickly. Acknowledge with a 2xx and do your work afterwards; a slow endpoint burns retry budget.