Skip to content

Events and payloads

The vocabulary SESKit normalises Amazon SES's notifications into, and the shape it forwards to your webhooks.

Event types

email.sent   email.delivered   email.bounced      email.suppressed
email.opened email.clicked     email.complained
Type Emitted when
email.sent A provider accepted the message
email.delivered The receiving server accepted it
email.bounced It could not be delivered
email.complained The recipient marked it as spam
email.opened The tracking pixel loaded — only with tracking on
email.clicked A rewritten link was followed — tracking only
email.suppressed SESKit added an address to the suppression list

SES emits more types than these — Reject, Rendering Failure, DeliveryDelay — which SESKit records but does not currently forward.

Payload

{
  "id": "evt_01J8XQ...",
  "type": "email.bounced",
  "email_id": "email_01J8XQ...",
  "created_at": "2026-09-02T09:00:05+00:00",
  "data": {
    "to": ["user@example.com"],
    "bounce_type": "Permanent",
    "diagnostic": "smtp; 550 5.1.1 user unknown"
  }
}
Field
id Unique per event. Deduplicate on this — delivery is at-least-once
type One of the seven above
email_id The message this is about; matches GET /v1/emails/{id}
created_at When the event happened, not when SESKit heard about it
data Type-dependent. Always includes to

Bounce types

The distinction that matters for your sender reputation:

bounce_type Means Do
Permanent The address does not exist, or refused permanently Stop sending to it. These are what push your bounce rate up
Transient A temporary failure — full mailbox, server down Safe to retry later
Undetermined The receiving server was unclear Treat as transient, watch for repeats

SESKit acts on this itself: a Permanent bounce puts the address on the project's suppression list and later sends to it are refused. Transient and Undetermined are left alone, because the address usually works again tomorrow.

email.suppressed

Raised by SESKit rather than by a provider, when an address is added to the list.

{
  "id": "evt_01J8XQ...",
  "type": "email.suppressed",
  "email_id": "email_01J8XQ...",
  "created_at": "2026-09-02T09:00:05+00:00",
  "data": {
    "to": ["user@example.com"],
    "reason": "bounce",
    "caused_by": "evt_01J8XP..."
  }
}
Field
reason bounce, complaint or unsubscribe
caused_by The event that led to it, or null — an unsubscribe is the recipient telling SESKit directly

One event per cause, not per address: a bounce naming three dead mailboxes is one thing that happened, so to is a list.

created_at is when SESKit suppressed the address, not when the bounce occurred. A notification that sat in a queue for an hour did not suppress anything an hour ago.

An address already on the list produces no second event, so a repeat bounce is not something your application has to deduplicate.

Timestamps

created_at is when the event occurred at the provider, not when SESKit ingested it. A queue backlog that delivers a bounce an hour late still reports the hour it happened, which is why the metrics filter on it.