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:

message lifecycle
  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.