Webhooks
Receive real-time email delivery events via HTTP callbacks.
Webhooks push delivery events to your server as they happen. Each delivery creates an HTTP POST to your endpoint with a signed JSON payload.
Create webhook
POST /v1/webhooks
Requires full_access scope.
Request body
{
"url": "https://api.acme.com/webhooks/sendr",
"events": ["email.delivered", "email.bounced", "email.opened", "email.clicked"]
}
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS endpoint to receive events. Must be a public URL. |
events | string[] | Yes | Event types to subscribe to. At least one required. |
Available event types:
| Event | Fires when... |
|---|---|
email.delivered | Email successfully delivered to the recipient's server |
email.bounced | Email permanently or temporarily rejected |
email.opened | Recipient opened the email (requires tracking enabled) |
email.clicked | Recipient clicked a tracked link |
email.complained | Recipient marked the email as spam |
Response
{
"id": "wh_abc123",
"url": "https://api.acme.com/webhooks/sendr",
"events": ["email.delivered", "email.bounced", "email.opened", "email.clicked"],
"secret": "whsec_abc123xyz...",
"active": true,
"created_at": "2025-03-12T09:00:00Z",
"updated_at": "2025-03-12T09:00:00Z"
}
Save the secret immediately — it is only returned at creation time and is used to verify webhook signatures. If lost, delete and recreate the webhook.
Get webhook
GET /v1/webhooks/:id
Requires read_only scope. The signing secret is not included in get/list responses.
List webhooks
GET /v1/webhooks
Requires read_only scope.
Response
{
"data": [
{
"id": "wh_abc123",
"url": "https://api.acme.com/webhooks/sendr",
"events": ["email.delivered", "email.bounced"],
"active": true,
"created_at": "2025-03-12T09:00:00Z",
"updated_at": "2025-03-12T09:00:00Z"
}
],
"has_more": false,
"next_cursor": null
}
Update webhook
PATCH /v1/webhooks/:id
Requires full_access scope.
Request body
All fields optional:
{
"url": "https://api.acme.com/webhooks/sendr-v2",
"events": ["email.delivered", "email.bounced", "email.opened"],
"active": false
}
Set active: false to pause delivery without deleting the webhook.
Delete webhook
DELETE /v1/webhooks/:id
Requires full_access scope.
Webhook payload
When an event fires, Sendr sends a POST request to your endpoint:
{
"event": "email.delivered",
"created_at": "2025-03-12T09:00:05Z",
"data": {
"email_id": "em_abc123",
"recipient": "user@example.com",
"timestamp": "2025-03-12T09:00:05Z"
}
}
Bounce payloads include additional fields:
{
"event": "email.bounced",
"created_at": "2025-03-12T09:00:05Z",
"data": {
"email_id": "em_abc123",
"recipient": "user@example.com",
"bounce_type": "permanent",
"bounce_code": "550",
"bounce_message": "5.1.1 User does not exist"
}
}
Signature verification
Every webhook request includes a Sendr-Signature header. Verify it to confirm the payload came from Sendr and was not tampered with.
See the Webhook Verification guide for full examples in TypeScript, Python, and Go.
Quick example:
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyWebhook(payload: string, signature: string, secret: string): boolean {
const hmac = createHmac("sha256", secret);
hmac.update(payload);
const expected = `sha256=${hmac.digest("hex")}`;
const a = Buffer.from(expected);
const b = Buffer.from(signature);
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}
// In your route handler:
const rawBody = await request.text();
const signature = request.headers.get("Sendr-Signature") ?? "";
if (!verifyWebhook(rawBody, signature, process.env.SENDR_WEBHOOK_SECRET!)) {
return new Response("Forbidden", { status: 403 });
}
Retries
If your endpoint returns a non-2xx status, Sendr retries the delivery up to 3 times with exponential backoff (2s, 4s, 8s). After 3 failures, the event is dropped.
Return 200 OK as quickly as possible — do heavy processing asynchronously.
Plan limits
| Plan | Webhooks |
|---|---|
| Free | 2 |
| Pro | 10 |
| Business | 50 |
| Enterprise | Unlimited |