Zerokit

Events

Reference of every event type SES emits and we forward to webhooks.

Events are immutable records of something that happened to an email. They live on email_events server-side and feed both the Email events timeline in the dashboard AND outbound webhook subscriptions.

Event types

TypeTriggered when
sentSES accepted the message for delivery (handoff complete).
deliveredThe receiving mail server accepted the message (most positive outcome you can verify).
openedA tracking pixel fired. Estimate only — many clients block images. Requires open tracking ON.
clickedA tracked link in the body was followed. Requires click tracking ON.
bouncedThe receiving mail server rejected the message. Soft (transient) vs hard (permanent) in payload.
complainedThe recipient marked the message as spam. Treat as a strong "stop sending" signal.
rejectedSES itself rejected the message before delivery (rare — usually means we sent something invalid).
rendering_failedTemplate render failed. Check variables against the template body.
delivery_delayedSES couldn't deliver yet (greylist, soft bounce, queue depth) — will retry.

Payload shape

Every event row has the same shape regardless of type:

{
  "id": "evt_2bV4dXyZ8AfQpkw7Lp",
  "emailId": "em_...",
  "type": "delivered",
  "eventTimestamp": "2026-05-22T09:14:32.500Z",
  "createdAt": "2026-05-22T09:14:32.700Z",
  "payloadJson": "..."
}

payloadJson is the raw SES event JSON, stringified. Field structure differs by type — bounces carry bounce.bounceType + bounceSubType, complaints carry complaint.complaintFeedbackType, etc. We don't normalise these — when you need them, parse the JSON.

Listing events directly via REST isn't supported — they're attached to their parent email via GET /emails/:id. Subscribe to a webhook for push delivery.

Webhook payload

When forwarded to a webhook, the event is wrapped:

{
  "id": "wd_...",
  "type": "email.event",
  "createdAt": "2026-05-22T09:14:32.700Z",
  "data": {
    "emailId": "em_...",
    "event": { "eventId": "evt_..." }
  }
}

The X-Zerokit-Event-Type header carries the underlying event type (delivered, bounced, …) so you can route without parsing the body.

Idempotency

Events are de-duplicated server-side using the SNS message ID as a unique key. SNS occasionally re-delivers the same event on transient 5xx; we swallow the duplicate insert via the partial unique index on provider_event_id.

Webhook deliveries are NOT deduplicated automatically — same event that takes 3 retries will fire 3 POSTs at your endpoint with the same X-Zerokit-Delivery-Id. Use that header for client-side dedup if you process events more than once.

On this page