Skip to content

Domains and events

Webhooks

Chebu posts an event to your endpoint whenever something happens to an email.

Create a webhook

endpoint must be an https URL. events lists what to send. The signing_secret is shown once. Store it to verify requests.

POST/webhooks
Request
curl -X POST https://api.chebu.io/webhooks \
  -H "Authorization: Bearer $CHEBU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "endpoint": "https://northwind.studio/hooks/chebu",
    "events": ["email.delivered", "email.bounced", "email.complained"]
  }'
JSON

Events

EventWhen
email.sentChebu handed the email to the sending network.
email.deliveredThe recipient's mail server accepted the email.
email.delivery_delayedDelivery failed for now and will be retried, for example because the inbox is full.
email.bouncedThe recipient's server rejected the email for good. The address is suppressed.
email.complainedThe recipient marked the email as spam. The address is suppressed.
email.openedThe recipient opened the email. Needs open tracking on the domain.
email.clickedThe recipient clicked a link. Needs click tracking on the domain.
email.failedThe email couldn't be sent, for example an invalid attachment.
email.scheduledThe email was scheduled for later.
email.suppressedThe email wasn't sent because the address is on your suppression list.

Payloads

Every event is a JSON POST with type, created_at, and data. data describes the email, with bounce details on bounces and click details on clicks.

email.delivered
JSON
{
  "type": "email.delivered",
  "created_at": "2026-09-27T10:04:15.902Z",
  "data": {
    "email_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
    "from": "Northwind <[email protected]>",
    "to": ["[email protected]"],
    "subject": "Your receipt #1042",
    "created_at": "2026-09-27T10:04:12.418Z",
    "tags": [
      { "name": "category", "value": "receipt" }
    ]
  }
}
email.bounced
JSON
{
  "type": "email.bounced",
  "created_at": "2026-09-27T10:04:16.120Z",
  "data": {
    "email_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
    "from": "Northwind <[email protected]>",
    "to": ["[email protected]"],
    "subject": "Your receipt #1042",
    "created_at": "2026-09-27T10:04:12.418Z",
    "tags": [],
    "bounce": {
      "type": "Permanent",
      "subType": "General",
      "message": "550 5.1.1 The email account does not exist."
    }
  }
}
email.clicked
JSON
{
  "type": "email.clicked",
  "created_at": "2026-09-27T11:20:03.004Z",
  "data": {
    "email_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
    "from": "Northwind <[email protected]>",
    "to": ["[email protected]"],
    "subject": "Your receipt #1042",
    "created_at": "2026-09-27T10:04:12.418Z",
    "tags": [],
    "click": {
      "link": "https://northwind.studio/orders/1042",
      "ipAddress": "203.0.113.24",
      "userAgent": "Mozilla/5.0",
      "timestamp": "2026-09-27T11:20:02.871Z"
    }
  }
}

Verify signatures

Chebu signs every request with the Standard Webhooks scheme. Each request has three headers, also sent as svix-id, svix-timestamp, and svix-signature for tools that expect those:

HeaderContents
webhook-idA unique ID for the message. The same on every retry, so you can drop duplicates.
webhook-timestampUnix seconds when it was sent.
webhook-signatureOne or more space-separated v1,<base64 signature> entries.
  1. Reject the request if webhook-timestamp is more than 5 minutes from your clock.
  2. Take the signing secret, drop the whsec_ prefix, and base64-decode the rest to get the key.
  3. Compute HMAC-SHA256 with that key over webhook-id + . + webhook-timestamp + . + the raw request body.
  4. Base64-encode the result and compare it, in constant time, with each v1, entry. Accept if any matches.
Verify a webhook
import crypto from "node:crypto";

const TOLERANCE_SECONDS = 5 * 60;

// rawBody must be the exact text Chebu sent, before any JSON parsing.
export function verifyWebhook(secret, headers, rawBody) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"];
  if (!id || !timestamp || !signatures) {
    throw new Error("Missing webhook headers");
  }

  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!(age <= TOLERANCE_SECONDS)) {
    throw new Error("Webhook timestamp is too old or too new");
  }

  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = crypto
    .createHmac("sha256", key)
    .update(id + "." + timestamp + "." + rawBody)
    .digest();

  const valid = signatures.split(" ").some((entry) => {
    const [version, signature] = entry.split(",");
    if (version !== "v1" || !signature) return false;
    const given = Buffer.from(signature, "base64");
    return (
      given.length === expected.length &&
      crypto.timingSafeEqual(given, expected)
    );
  });
  if (!valid) throw new Error("Invalid webhook signature");

  return JSON.parse(rawBody);
}

Because the scheme is standard, the standardwebhooks libraries can verify Chebu webhooks too.

Retries

Reply with a status from 200 to 299 within a few seconds to acknowledge an event, and do slow work afterwards. Any other response, or a timeout, is retried after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, 14 hours, 20 hours, and 24 hours.

  • An endpoint that keeps failing for 5 days is disabled.
  • Replying 410 Gone disables the endpoint straight away.
  • Events can arrive more than once or out of order. Use webhook-id to skip duplicates and created_at to order them.

Manage webhooks

RequestDoes
GET /webhooksList webhooks.
GET /webhooks/{id}Retrieve one.
PATCH /webhooks/{id}Change endpoint, events, or status (enabled or disabled).
DELETE /webhooks/{id}Delete it.
GET /webhooks/{id}/attemptsRecent delivery attempts and responses.
POST /webhooks/{id}/messages/{message_id}/replaySend a past event again.
POST /webhooks/{id}/rotateIssue a new signing secret.
PATCH/webhooks/7c1f1e4a-3b2d-4f8e-9a61-2d0c5b8e4f13
Request
curl -X PATCH https://api.chebu.io/webhooks/7c1f1e4a-3b2d-4f8e-9a61-2d0c5b8e4f13 \
  -H "Authorization: Bearer $CHEBU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "events": ["email.delivered", "email.bounced", "email.opened"],
    "status": "enabled"
  }'
JSON