Event Delivery Model
mailbot pushes webhook events to your endpoint when interesting things happen on an inbox: a message is sent, delivered, opened, clicked, bounced, or received. This page describes the contract: when events fire, in what order, and what your endpoint must handle.
Event types
| Event | When it fires |
|---|---|
message.sent | Outbound message accepted by the upstream MTA |
message.delivered | Recipient mailbox accepted the message |
message.opened | Tracking pixel loaded by the recipient |
message.clicked | Tracking link followed by the recipient |
message.bounced | Recipient mailbox rejected the message |
message.received | Inbound message landed on one of your inboxes |
message.parsed | Inbound message parsed and ready for downstream handlers |
Each event payload includes the event_id, event_type, inbox_id, the relevant message_id and thread_id, and a timestamp. Full payload schemas are in the API reference.
At-least-once delivery
Webhook delivery is at-least-once. mailbot guarantees that every event is delivered to your endpoint at least once. It does not guarantee exactly-once.
This means your endpoint must be idempotent. If mailbot retries an event after a transient failure, your handler will see the same event_id again. Treat event_id as a deduplication key.
A simple idempotency check:
async function handleEvent(event) {
const seen = await db.events.findOne({ event_id: event.event_id });
if (seen) return; // already processed
await db.events.insert({ event_id: event.event_id, processed_at: new Date() });
await processEvent(event);
}
Retry policy
If your endpoint returns a non-2xx response, mailbot retries with exponential backoff:
- attempt 1: immediately on event
- attempt 2: after 30 seconds
- attempt 3: after 2 minutes
- attempt 4: after 10 minutes
- attempt 5: after 1 hour
- attempt 6: after 6 hours
After 6 failed attempts, the delivery is marked failed and stops retrying. The event is preserved and remains replayable indefinitely via event replay.
A 2xx response (200, 201, 202, 204) is treated as success and stops retries. Anything else, including 3xx redirects, is treated as failure.
Ordering guarantees
mailbot does not guarantee strict global ordering across events.
Within a single message lifecycle, events generally arrive in the natural sequence: sent → delivered → opened → clicked. But your handler must not depend on this order. A delayed network can reorder webhook deliveries even when events were emitted in order on the mailbot side.
Two safe patterns:
- Order by event timestamp, not arrival time — every event payload carries
created_at. Reconstruct order from the timestamp, not the sequence in which your endpoint received them. - Use idempotent state transitions — design your data model so applying events out of order still converges to the right state. For example, "set
opened_atto the earliest known open" is order-independent.
Per-message vs per-inbox dispatch
Webhook destinations are configured at the inbox level. Events for messages on inbox A only fire to webhooks registered for inbox A. There is no global webhook fan-out.
If you want a single endpoint to receive events from many inboxes, register the same URL on each inbox. mailbot treats them as independent destinations, so retries and failures are scoped per inbox.
Signature verification
Every webhook delivery includes an X-mailbot-Signature header. Verify it before trusting the payload. See Webhook reliability for the verification recipe.