Sendry Docs
API Reference

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/sendry",
  "events": ["email.delivered", "email.bounced", "email.opened", "email.clicked"]
}
FieldTypeRequiredDescription
urlstringYesHTTPS endpoint to receive events. Must be a public URL.
eventsstring[]YesEvent types to subscribe to. At least one required.

Available event types:

EventFires when...
email.deliveredEmail successfully delivered to the recipient's server
email.bouncedEmail permanently or temporarily rejected
email.openedRecipient opened the email (requires tracking enabled)
email.clickedRecipient clicked a tracked link
email.complainedRecipient marked the email as spam
contact.createdA contact was created
contact.updatedA contact's properties were updated
contact.deletedA contact was deleted
domain.createdA sending domain was created
domain.updatedA sending domain's DNS state or config changed
domain.deletedA sending domain was deleted
automation.run.startedAn automation run transitioned from pending to in_progress
automation.run.completedAn automation run finished successfully
automation.run.failedAn automation run ended in error

Response

{
  "id": "wh_abc123",
  "url": "https://api.acme.com/webhooks/sendry",
  "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/sendry",
      "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/sendry-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, Sendry 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"
  }
}

Automation run payloads

Automation lifecycle events share the same envelope and identify the run, parent automation, and contact:

{
  "event": "automation.run.started",
  "created_at": "2026-05-14T09:00:00Z",
  "data": {
    "run_id": "arun_abc123",
    "automation_id": "auto_xyz789",
    "contact_id": "cnt_456",
    "contact_email": "user@example.com",
    "trigger_event_id": "evt_111",
    "started_at": "2026-05-14T09:00:00Z"
  }
}

automation.run.completed and automation.run.failed include timing and (on failure) the reason:

{
  "event": "automation.run.completed",
  "created_at": "2026-05-14T09:05:12Z",
  "data": {
    "run_id": "arun_abc123",
    "automation_id": "auto_xyz789",
    "contact_id": "cnt_456",
    "contact_email": "user@example.com",
    "status": "completed",
    "started_at": "2026-05-14T09:00:00Z",
    "completed_at": "2026-05-14T09:05:12Z",
    "failed_at": null,
    "failure_reason": null
  }
}
{
  "event": "automation.run.failed",
  "created_at": "2026-05-14T09:03:44Z",
  "data": {
    "run_id": "arun_abc123",
    "automation_id": "auto_xyz789",
    "contact_id": "cnt_456",
    "contact_email": "user@example.com",
    "status": "failed",
    "started_at": "2026-05-14T09:00:00Z",
    "completed_at": null,
    "failed_at": "2026-05-14T09:03:44Z",
    "failure_reason": "send_email step failed: SES throttled"
  }
}

Signature verification

Every webhook request includes a Sendry-Signature header. Verify it to confirm the payload came from Sendry 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("Sendry-Signature") ?? "";
if (!verifyWebhook(rawBody, signature, process.env.SENDRY_WEBHOOK_SECRET!)) {
  return new Response("Forbidden", { status: 403 });
}

Retries

If your endpoint returns a non-2xx status, Sendry 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

PlanWebhooks
Free2
Pro10
Business50
EnterpriseUnlimited

On this page