Integration Walkthrough · v1
Sending Transactional Email
A worked example from first API call to delivered message. Covers the send sequence, how to read delivery status and engagement events, and what happens when a message permanently bounces — including automatic suppression.
01 The Lifecycle
Every transactional email follows the same path. One POST to send, one UUID per recipient to track it, and a chronological event timeline that grows as delivery and engagement happen. Here is the complete state machine:
your app
│
▼
POST /api/v1/messages/send
│
▼
202 Accepted ── { "message_ids": ["550e84…"] }
│
▼
queued → processing → sent
│
┌───────────────┴────────────────┐
▼ ▼
bounced opened / clicked
(suppressed) (automatic tracking) State definitions
- queued Accepted and waiting to be picked up by the delivery worker.
- processing Handed to the SMTP relay; waiting for server acknowledgement.
- sent Remote server accepted the message. Delivery confirmed.
- failed Temporary delivery failure. The API will not retry automatically.
- bounced Permanent bounce. The address is now suppressed.
02 Step 1 — Send
A single POST to
/api/v1/messages/send
queues the message and returns one UUID per recipient. That UUID is your handle
for everything that follows: status checks, event timelines, and bounce records.
The Idempotency-Key header
is optional but strongly recommended — it makes the call safe to retry. More on
that in Step 5.
// 1. export your token once
export TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... // 2. send the order confirmation
curl -s -X POST https://api.email.squibble.ch/api/v1/messages/send \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-12345-confirm" \
-d '{
"to": ["customer@example.com"],
"cc": [],
"bcc": [],
"subject": "Your order #12345 is confirmed",
"html_body": "<h1>Order confirmed</h1><p>Hi Sarah, your order is on its way.</p><p><a href=\"https://example.com/orders/12345\">Track your order</a></p>",
"text_body": "Hi Sarah, your order #12345 is confirmed and on its way.\n\nTrack: https://example.com/orders/12345",
"stream": "transactional"
}' // 3. capture the message_id for tracking
MESSAGE_ID=$(curl -s -X POST https://api.email.squibble.ch/api/v1/messages/send \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-12345-confirm" \
-d '{ "to": ["customer@example.com"], "subject": "Your order #12345 is confirmed", "html_body": "<p>Hi Sarah</p>", "stream": "transactional" }' | jq -r '.message_ids[0]')
echo "Tracking: $MESSAGE_ID" 202 response
The API returns one UUID per recipient. Save this — it is the key for every subsequent lookup.
{
"message_ids": ["550e8400-e29b-41d4-a716-446655440000"]
} 03 Step 2 — Check Delivery Status
Use the message_id from the
send response to fetch the current delivery state. Requires the
messages:index scope.
curl -s https://api.email.squibble.ch/api/v1/messages/outbound/$MESSAGE_ID \
-H "Authorization: Bearer $TOKEN"
// response
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"to": "customer@example.com",
"subject": "Your order #12345 is confirmed",
"stream": "transactional",
"status": "sent",
"sent_at": "2026-05-07T10:43:03Z",
"created_at": "2026-05-07T10:43:00Z"
} All status values
- queued Accepted and waiting to be picked up by the delivery worker.
- processing Handed to the SMTP relay; waiting for server acknowledgement.
- sent Remote server accepted the message. Delivery confirmed.
- failed Temporary delivery failure. The API will not retry automatically.
- bounced Permanent bounce. The address is now suppressed.
04 Step 3 — Event Timeline
The /events endpoint returns
a chronological list of every delivery and engagement event for that message.
Open and click tracking require no integration work — when you send an
html_body, the API
injects a tracking pixel and rewrites all links automatically.
curl -s https://api.email.squibble.ch/api/v1/messages/outbound/$MESSAGE_ID/events \
-H "Authorization: Bearer $TOKEN"
// response — full delivery + engagement history in one call
[
{
"id": "a1b2c3d4-0001-0000-0000-000000000001",
"event_type": "queued",
"occurred_at": "2026-05-07T10:43:00Z", // T+0s
"payload": {}
},
{
"id": "a1b2c3d4-0001-0000-0000-000000000002",
"event_type": "sent",
"occurred_at": "2026-05-07T10:43:03Z", // T+3s
"payload": {}
},
{
"id": "a1b2c3d4-0001-0000-0000-000000000003",
"event_type": "opened",
"occurred_at": "2026-05-07T10:51:14Z", // T+8min
"payload": {}
},
{
"id": "a1b2c3d4-0001-0000-0000-000000000004",
"event_type": "clicked",
"occurred_at": "2026-05-07T10:52:30Z", // T+9min
"payload": {}
}
] Event types
- queued — message accepted and sitting in the send queue.
- sent — remote mail server acknowledged receipt. Delivery is confirmed at the protocol level.
- opened — the tracking pixel in the HTML body was fetched. Recorded on first hit only.
- clicked — a rewritten link was followed. Recorded on first click only.
- bounced — permanent delivery failure. See Step 4.
05 Step 4 — Bounces & Suppression
When a message permanently bounces — an address that does not exist, a domain
with no MX record, or a mailbox explicitly rejecting mail — the delivery system
classifies it via RFC 3463 and writes a
bounced event. No polling
interval needed: the status field on the outbound message updates at the same
moment.
// a bounced message
curl -s https://api.email.squibble.ch/api/v1/messages/outbound/$MESSAGE_ID \
-H "Authorization: Bearer $TOKEN"
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"to": "customer@example.com",
"status": "bounced",
"sent_at": null
} // the events timeline confirms it
curl -s https://api.email.squibble.ch/api/v1/messages/outbound/$MESSAGE_ID/events \
-H "Authorization: Bearer $TOKEN"
[
{ "event_type": "queued", "occurred_at": "2026-05-07T10:43:00Z" },
{ "event_type": "bounced", "occurred_at": "2026-05-07T10:43:11Z" }
] Automatic suppression
The bounce is added to the per-mailbox suppression list the moment it is
classified. Any subsequent send attempt to that address returns
422 immediately, before the message is ever
queued. The error body tells you exactly why:
// 422 — recipient suppressed (same address, later attempt)
curl -s -X POST https://api.email.squibble.ch/api/v1/messages/send \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "to": ["customer@example.com"], "subject": "...", "html_body": "...", "stream": "transactional" }'
{
"type": "https://email.squibble.ch/docs/errors/messages/send/suppressed",
"title": "Recipient Suppressed",
"status": 422,
"detail": "suppressed: [customer@example.com]",
"instance": "/api/v1/messages/send"
} A bounce is data. We surface it immediately so you can handle it in your code, not discover it weeks later in a deliverability dashboard.
In practice, a 422 on a send is a signal to
update your database, not to retry. The address cannot receive mail. Remove it
from your send list and move on. The
detail field gives you
the suppressed addresses so you know exactly which ones to remove when sending
to multiple recipients.
06 Step 5 — Retry Safely
Network failures, timeouts, and load-balancer restarts happen. The
Idempotency-Key header
(max 255 chars) makes the send endpoint safe to retry within a 24-hour window:
a second call with the same key returns the original
message_id without dispatching a duplicate.
// first attempt — succeeds
curl -s -X POST https://api.email.squibble.ch/api/v1/messages/send \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-12345-confirm" \
-d '{ "to": ["customer@example.com"], "subject": "Your order #12345 is confirmed", "html_body": "<p>Hi Sarah</p>", "stream": "transactional" }'
// → 202 { "message_ids": ["550e8400-e29b-41d4-a716-446655440000"] } // retry with the same key — returns the original UUID, no duplicate send
curl -s -X POST https://api.email.squibble.ch/api/v1/messages/send \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-12345-confirm" \
-d '{ "to": ["customer@example.com"], "subject": "Your order #12345 is confirmed", "html_body": "<p>Hi Sarah</p>", "stream": "transactional" }'
// → 202 { "message_ids": ["550e8400-e29b-41d4-a716-446655440000"] } ← same UUID Key design
- Use a deterministic key derived from your business entity:
order-{id}-confirm,invoice-{id}-reminder. - Keys are scoped to your token — two different tokens can safely reuse the same key string.
- After 24 hours the key expires. A new send with the same key creates a new message.
- Omitting the key is fine for one-off sends where duplication is acceptable. For transactional sends triggered by user actions, always include it.
What's next?
This walkthrough covers the transactional sending path. The full API reference documents every endpoint, parameter, and error type — including inbox reading, marketing stream, unsubscribe handling, and the health check endpoint.