Sendex

Webhooks

Webhooks let Sendex push real-time event notifications to your server the moment something happens — an email is sent, a recipient opens a message, a contact unsubscribes, and more. No polling required.

Register and manage webhook endpoints from the Webhooks section of your dashboard. Each endpoint receives a unique signing secret shown only once at creation.

How it works

When a subscribed event occurs, Sendex delivers an HTTP POST request to your endpoint URL within seconds. Each request includes:

  • A Content-Type: application/json header
  • An X-Sendex-Signature header for authenticity verification
  • A JSON body with event, created_at, and data

Sendex retries failed deliveries up to 3 times with exponential backoff. Your endpoint should return a 2xx status within 10 seconds — any non-2xx response or timeout is treated as a failure and retried. The last 30 days of delivery logs are available in the dashboard.

Payload format

Every webhook POST has the same envelope structure:

POST https://your-server.com/webhook
Content-Type: application/json
X-Sendex-Signature: sha256=<hmac-sha256-hex>

{
  "event": "email.sent",
  "created_at": "2026-03-05T12:00:00.000Z",
  "data": {
    "id": "abc123",
    "from": "[email protected]",
    "to": ["[email protected]"],
    "subject": "Welcome aboard"
  }
}
FieldTypeDescription
eventstringThe event name, e.g. email.sent
created_atstring (ISO 8601)UTC timestamp when the event occurred
dataobjectEvent-specific payload — see each event below

Verifying signatures

Every request is signed with HMAC-SHA256 using your endpoint's secret. Always verify the signature before processing the payload to ensure the request genuinely came from Sendex.

Compute HMAC-SHA256(secret, rawBody) where rawBody is the raw request body string (before any JSON parsing). Compare the result, prefixed with sha256=, to the X-Sendex-Signature header using a timing-safe comparison to prevent timing attacks.

Node.js / TypeScript

import crypto from "crypto";

function verifyWebhook(
  rawBody: string,
  secret: string,
  signatureHeader: string
): boolean {
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex");

  const sigBuf = Buffer.from(signatureHeader);
  const expBuf = Buffer.from(expected);

  // Length check prevents timingSafeEqual from throwing
  return (
    sigBuf.length === expBuf.length &&
    crypto.timingSafeEqual(sigBuf, expBuf)
  );
}

Python

import hmac, hashlib

def verify_webhook(raw_body: bytes, secret: str, signature_header: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)
Sign the raw request body, not the parsed JSON object. Re-serialising JSON may change key order or whitespace and break the signature check.

Events

Events are grouped into three categories: Email, Broadcast, and Contact. You can subscribe to any combination per endpoint.

Email events

email.sent

Fired immediately after an email is accepted by SES — meaning it left your account successfully. This does not guarantee delivery to the recipient's inbox; use email.delivered for that.

Payload

{
  "id": "69a40a5f923b47ca68278e38",
  "from": "[email protected]",
  "to": ["[email protected]"],
  "subject": "Welcome aboard",
  "created_at": "2026-03-05T12:00:00.000Z"
}
email.failed

Fired when SES rejects the email outright — for example due to a throttling error or an invalid sending identity. This is distinct from a bounce (which happens after the email was accepted).

Payload

{
  "from": "[email protected]",
  "to": ["[email protected]"],
  "subject": "Welcome aboard",
  "error": "Account sending paused"
}
email.received

Fired when an inbound email arrives at one of your verified domains and is stored in the inbox. Requires inbound email routing to be configured via AWS SES receipt rules.

Payload

{
  "email_id": "69a40a5f923b47ca68278e38",
  "message_id": "0100019cbe53da54-9e5928a3-...",
  "from": "[email protected]",
  "to": ["[email protected]"],
  "subject": "Hello there",
  "received_at": "2026-03-05T12:00:00.000Z"
}
email.delivered

Fired when the receiving mail server confirms successful delivery. This is the strongest signal that the email reached the recipient's mail server (though it may still land in spam).

Payload

{
  "message_id": "0100019cbe53da54-9e5928a3-...",
  "from": "[email protected]",
  "to": ["[email protected]"]
}
email.bounced

Fired when the email bounces. A hard bounce means the address does not exist — you should remove it from your lists immediately. A soft bounce is temporary (e.g. mailbox full) and may succeed on retry.

Payload

{
  "message_id": "0100019cbe53da54-9e5928a3-...",
  "from": "[email protected]",
  "to": ["[email protected]"],
  "bounce_type": "Permanent",
  "bounce_sub_type": "General"
}
bounce_typeMeaning
PermanentHard bounce — address invalid, remove from list
TransientSoft bounce — temporary issue, may self-resolve
UndeterminedSES could not determine the bounce type
email.complained

Fired when a recipient marks the email as spam via their mail client. High complaint rates damage your sender reputation — suppress complaining addresses immediately.

Payload

{
  "message_id": "0100019cbe53da54-9e5928a3-...",
  "from": "[email protected]",
  "to": ["[email protected]"],
  "complaint_feedback_type": "abuse"
}
email.openedHTML email required

Fired when a recipient opens the email. SES automatically injects an invisible 1×1 tracking pixel into the HTML body. When the pixel is loaded, AWS records the open and sends the event via your configuration set's SNS destination.

Open tracking only works with HTML emails (the html field). Plain-text emails cannot carry a tracking pixel. Some email clients and privacy tools (e.g. Apple Mail Privacy Protection) will load the pixel automatically, potentially inflating open counts.

Payload

{
  "message_id": "0100019cbe53da54-9e5928a3-...",
  "from": "[email protected]",
  "to": ["[email protected]"]
}
email.clickedHTML email required

Fired when a recipient clicks a link in the email. SES automatically rewrites all links in the HTML body to route through an AWS tracking endpoint. When the link is clicked, AWS records it and redirects the user to the original URL.

Click tracking only works with HTML emails containing <a href> links. Plain-text emails and emails without links will never produce this event.

Payload

{
  "message_id": "0100019cbe53da54-9e5928a3-...",
  "from": "[email protected]",
  "to": ["[email protected]"],
  "link": "https://example.com/your-original-url"
}

Broadcast events

EventWhen it firesNotable data fields
broadcast.queuedBroadcast created and added to the send queue.id, subject, stats.total
broadcast.startedWorker picked up the broadcast and started sending.id, from, subject, stats
broadcast.completedAll recipients processed; sending finished.id, stats (sent / failed / skipped)
broadcast.pausedBroadcast was paused mid-send via the API or dashboard.id
broadcast.cancelledBroadcast was cancelled before completing.id
broadcast.failedWorker encountered a fatal error and stopped.id, error

Example — broadcast.completed payload

{
  "id": "69a40a5f923b47ca68278e38",
  "stats": {
    "total": 1000,
    "sent": 987,
    "failed": 3,
    "skipped": 10
  }
}

Contact events

EventWhen it fires
contact.createdA new contact was added to an audience (via API or upsert).
contact.subscribedA contact's subscribed status was set to true via the API.
contact.unsubscribedContact unsubscribed — either via the API or the unsubscribe page.

Example — contact.unsubscribed payload

{
  "contact_id": "69a40a5f923b47ca68278e38",
  "email": "[email protected]",
  "audience_id": "69a40a5f923b47ca68278e39"
}

Open & click tracking

Sendex automatically handles delivery event tracking on your behalf — there is nothing to configure. The email.delivered, email.bounced, email.complained, email.opened, and email.clicked events all fire automatically as they happen.

You write the HTML — we inject the tracking

You do not need to add anything special to your emails. When you send an email with an html body, Sendex (via AWS SES) automatically:

  • Injects an invisible 1×1 tracking pixel at the bottom of your HTML — when the recipient's email client loads it, email.opened fires.
  • Rewrites every <a href> link in your HTML to route through a tracking URL — when the recipient clicks, email.clicked fires and they are immediately redirected to the original destination.

Your recipients never see the tracking infrastructure — the pixel is invisible and link rewrites are transparent. The only thing you need to do is pass an html field in your request. If you only pass text, there is no HTML to inject into and these events will never fire.

Example — sending with HTML to enable tracking

{
  "from": "[email protected]",
  "to": ["[email protected]"],
  "subject": "Your weekly digest",
  "html": "<p>Hello! Check out <a href=\"https://example.com/news\">this week's news</a>.</p>",
  "text": "Hello! Check out this week's news: https://example.com/news"
}

Including both html and text is recommended — the text fallback is shown to clients that do not render HTML, while the html body enables open and click tracking.

Some email clients (notably Apple Mail with Mail Privacy Protection) pre-load images automatically — including the tracking pixel — even if the recipient never actually reads the email. This can cause email.opened to fire more often than expected.

Quick reference

EventCategoryHTML email required
email.sentEmailNo
email.failedEmailNo
email.receivedEmailNo
email.deliveredEmailNo
email.bouncedEmailNo
email.complainedEmailNo
email.openedEmailYes
email.clickedEmailYes
broadcast.queuedBroadcastNo
broadcast.startedBroadcastNo
broadcast.completedBroadcastNo
broadcast.pausedBroadcastNo
broadcast.cancelledBroadcastNo
broadcast.failedBroadcastNo
contact.createdContactNo
contact.subscribedContactNo
contact.unsubscribedContactNo