Integration Guide · v1

Client Integration Guide


Everything you need to integrate squibble/email into your application — auth, sending, reading the inbox, delivery tracking, error handling.

01 Overview

Squibble/email exposes three primary API surfaces, each scoped by a separate JWT action so an agent only gets what it actually needs:

  • Outbound sending (messages:send) — Queue email through POST /api/v1/messages/send with automatic sender binding, suppression checks, idempotent retries, open/click tracking, and optional file attachments (base64-encoded, up to 25 MiB total).
  • Markdown sending (messages:send) — Write email bodies in Markdown and let POST /api/v1/messages/send-markdown convert them to email-safe HTML via the emailmd sidecar — brand colours, plain-text fallback, and theme tokens included.
  • Inbox reading (messages:index + attachments:show) — Query received mail over JSON. Selective BODYSTRUCTURE fetch keeps attachments out of the agent's context window until explicitly requested.
  • Delivery tracking (messages:index) — Every send returns a UUID. Pull delivery status, bounce records, suppression reasons, and engagement events from /api/v1/messages/outbound/{id}/events without polling or webhook plumbing.

Each token is bound to one mailbox and a precise set of those actions. Sharing a single SMTP credential across N agents — the failure mode this product exists to prevent — is not a configuration we expose.

02 Authentication

All API calls require a Bearer token in the Authorization header. Tokens are JWTs issued by Squibble per mailbox per agent. After you join the waitlist we onboard your mailbox and provision your first token.

Authorization: Bearer <your-jwt-token>

// export your token once, use it everywhere below

export TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

curl -s https://api.email.squibble.ch/api/v1/health_check \
  -H "Authorization: Bearer $TOKEN"

Each token encodes the target mailbox and a set of permitted actions: messages:send, messages:index, attachments:show. The messages:index scope covers both listing and fetching individual messages or their events. Calling an endpoint outside your token's scope returns 403.

03 Sending Email

Requires the messages:send scope.

POST /api/v1/messages/send

Idempotency-Key: order-123-confirm  # optional header, 24-hour dedup window

{
  "to": ["recipient@example.com"],  # required, max 100 recipients
  "cc": [],  # optional, max 100
  "bcc": [],  # optional, max 100
  "subject": "Hello",  # required, max 998 chars
  "html_body": "<p>HTML body</p>",  # required, max 1 MB
  "text_body": "Plain text body",  # optional plain-text fallback
  "stream": "transactional",  # "transactional" (default) | "marketing"
  "attachments": [   # optional, max 20 files
    {
      "filename": "report.pdf",
      "content_type": "application/pdf",
      "content": "<base64-encoded file content>"  # max 10 MiB decoded per file; 25 MiB total
    }
  ]
}

// send a transactional email

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":      ["recipient@example.com"],
    "cc":      [],
    "bcc":     [],
    "subject": "Hello",
    "html_body": "<p>HTML body</p>",
    "text_body": "Plain text body",
    "stream": "transactional"
  }'

Response codes

  • 202 Accepted — message queued for delivery.
  • 422 Validation error or recipient suppressed (bounce / unsubscribe).
  • 429 Per-token send quota exceeded (per_hour or per_day).

202 response

The response body contains one UUID per recipient. Use these IDs to track delivery status and engagement events per message.

{
  "message_ids": ["550e8400-e29b-41d4-a716-446655440000"]
}

04 Sending Markdown

Write your email body in Markdown and let the API render it to email-safe HTML. Requires the messages:send scope. The same suppression checks, idempotency, delivery tracking, and attachment support apply as for POST /api/v1/messages/send.

POST /api/v1/messages/send-markdown

Idempotency-Key: order-123-confirm  # optional header, 24-hour dedup window

{
  "to": ["recipient@example.com"],  # required, max 100 recipients
  "cc": [],                         # optional
  "bcc": [],                        # optional
  "subject": "Hello",             # required, max 998 chars
  "markdown_body": "# Hello\n\nYour **order** has shipped.",  # required, max 1 MB
  "stream": "transactional",      # "transactional" (default) | "marketing"
  "render_options": {               # optional
    "minify": true,               # default true
    "theme": {
      "brand_color": "#4f46e5",    # hex colour tokens
      "button_color": "#4f46e5",
      "font_family": "Inter, sans-serif"
    }
  }
}

// send a markdown email with brand theming

curl -s -X POST https://api.email.squibble.ch/api/v1/messages/send-markdown \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to":      ["recipient@example.com"],
    "subject": "Your order has shipped",
    "markdown_body": "# Your order has shipped\n\nHi there,\n\nYour **order #1234** is on its way.\n\n[Track your package](https://example.com/track/1234)\n\nThanks for shopping with us.",
    "stream": "transactional",
    "render_options": {
      "theme": { "brand_color": "#4f46e5", "button_color": "#4f46e5" }
    }
  }'

Theme tokens

All theme fields are optional hex colour strings or CSS values. Omitting a field leaves it at the emailmd default.

  • brand_color Primary accent used for links and borders
  • heading_color H1–H6 text colour
  • body_color Body copy text colour
  • background_color Outer page background
  • content_color Content card background
  • card_color Inner card / blockquote background
  • button_color CTA button fill
  • button_text_color CTA button label
  • secondary_color Secondary button fill
  • secondary_text_color Secondary button label
  • font_family CSS font-family stack
  • font_size Base font size (e.g. "16px")
  • line_height Body line height (e.g. "1.6")
  • content_width Max content width (e.g. "600px")
  • border_radius Button / card corner radius

Response codes

  • 202 Accepted — message queued for delivery.
  • 422 Validation error or recipient suppressed (bounce / unsubscribe).
  • 429 Per-token send quota exceeded (per_hour or per_day).

202 response

{
  "message_ids": ["550e8400-e29b-41d4-a716-446655440000"]
}

04.5 Attachments

Both POST /api/v1/messages/send and POST /api/v1/messages/send-markdown accept an optional attachments array. Each file is base64-encoded by your client and declared with a filename and MIME type — the API decodes, validates, and assembles the MIME multipart message before dispatch. Requires the messages:send scope.

Limits

  • Max 20 attachments per request.
  • Max 10 MiB decoded per individual file.
  • Max 25 MiB decoded total across all attachments in a single request.
  • The content field must be valid standard base64 (RFC 4648). Invalid encoding returns 422.
  • The filename must be 1–255 characters, no newlines or null bytes.
  • The content_type must be a valid MIME type string (e.g. application/pdf, image/png).

// send an email with a PDF attachment

# base64-encode the file first
CONTENT=$(base64 -i report.pdf)

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\": \"Your invoice\",
    \"html_body\": \"<p>Please find your invoice attached.</p>\",
    \"stream\": \"transactional\",
    \"attachments\": [
      {
        \"filename\":     \"invoice-2026-001.pdf\",
        \"content_type\": \"application/pdf\",
        \"content\":      \"$CONTENT\"
      }
    ]
  }"

// multiple attachments in one send

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": "Your order documents",
    "html_body": "<p>Please find your documents attached.</p>",
    "stream": "transactional",
    "attachments": [
      {
        "filename":     "invoice.pdf",
        "content_type": "application/pdf",
        "content":      "JVBERi0xLjQK..."
      },
      {
        "filename":     "receipt.png",
        "content_type": "image/png",
        "content":      "iVBORw0KGgoAAAA..."
      }
    ]
  }'

Validation errors

Attachment validation runs before the message is queued. A single invalid attachment rejects the entire request with 422:

{
  "type": "about:blank",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "The request contains invalid parameters.",
  "instance": "/api/v1/messages/send",
  "errors": [
    {
      "type": "value_error",
      "loc": ["body"],
      "msg": "attachments[0].content: invalid base64 encoding"
    }
  ]
}

05 Reading the Inbox

Requires the messages:index scope. The {mailbox} path segment is the mailbox identifier encoded in your JWT — use the value provided during onboarding.

GET  /api/v1/email/account/{mailbox}/messages  # list messages
GET  /api/v1/email/account/{mailbox}/messages/{message_id}  # get a message (integer ID)
GET  /api/v1/email/account/{mailbox}/messages/{message_id}/attachments/{filename}  # attachment

// list the 10 most recent inbox messages

curl -s "https://api.email.squibble.ch/api/v1/email/account/{mailbox}/messages?limit=10" \
  -H "Authorization: Bearer $TOKEN"

// fetch a single message by integer ID

curl -s "https://api.email.squibble.ch/api/v1/email/account/{mailbox}/messages/42" \
  -H "Authorization: Bearer $TOKEN"

The list endpoint supports optional query filters: filter[from], filter[to], filter[subject], plus limit (default 10, max 999) and offset. Attachment downloads require the attachments:show scope.

06 Outbound Messages

Requires the messages:index scope. Use these endpoints to inspect delivery status and per-message event timelines for mail you have sent.

GET  /api/v1/messages/outbound  # list sent messages
GET  /api/v1/messages/outbound/{message_id}  # single message (UUID)
GET  /api/v1/messages/outbound/{message_id}/events  # delivery event timeline

// list sent messages — filter by status

curl -s "https://api.email.squibble.ch/api/v1/messages/outbound?status=sent&limit=20" \
  -H "Authorization: Bearer $TOKEN"

// check delivery status for a specific message

curl -s https://api.email.squibble.ch/api/v1/messages/outbound/$MESSAGE_ID \
  -H "Authorization: Bearer $TOKEN"

// fetch the full delivery + engagement event timeline

curl -s https://api.email.squibble.ch/api/v1/messages/outbound/$MESSAGE_ID/events \
  -H "Authorization: Bearer $TOKEN"

List filters

The list endpoint accepts optional query parameters: status (queued | processing | sent | failed | bounced), stream (transactional | marketing), recipient (email address string), and limit (default 100, max 200).

Event types

The /events endpoint returns a chronological list of delivery events for a message: queued, sent, bounced, opened, and clicked. Open and click events are recorded automatically — see Engagement Tracking.

07 Engagement Tracking

Open and click tracking is automatic — no integration work required. When you send an html_body, the API injects a 1×1 tracking pixel and rewrites all links through a redirect. Here is how each event is captured:

  • Open — a hidden <img> pixel is embedded in the HTML body. When the recipient's mail client renders the email it fetches the pixel URL, which records an opened event.
  • Click — every link in the HTML body is rewritten to pass through a redirect. When the recipient clicks, the redirect records a clicked event and forwards them to the original URL.

Reading events

Use the message_id from the send response to query the event timeline for that recipient.

// fetch open and click events for a message

curl -s https://api.email.squibble.ch/api/v1/messages/outbound/$MESSAGE_ID/events \
  -H "Authorization: Bearer $TOKEN"

// example response
GET  /api/v1/messages/outbound/550e8400-e29b-41d4-a716-446655440000/events

# example response
[
  {
    "id": "a1b2c3d4-...",
    "event_type": "opened",
    "occurred_at": "2025-05-07T10:42:00Z",
    "payload": {}
  },
  {
    "id": "b2c3d4e5-...",
    "event_type": "clicked",
    "occurred_at": "2025-05-07T10:43:15Z",
    "payload": {}
  }
]

Events are returned in chronological order. All event types — queued, sent, bounced, opened, clicked — appear in the same timeline, giving you the full delivery and engagement history for each recipient in one call.

08 Unsubscribe

All marketing stream emails include a List-Unsubscribe header with a signed token. Two public (no auth) endpoints handle unsubscribe requests:

GET   /api/v1/unsubscribe?t={token}  # browser-based unsubscribe page
POST  /api/v1/unsubscribe?t={token}  # RFC 8058 one-click (email client automation)

// one-click unsubscribe (RFC 8058 — no auth required)

curl -s -X POST "https://api.email.squibble.ch/api/v1/unsubscribe?t={signed-token}"

// 200 response
{ "unsubscribed": true }

Both return { "unsubscribed": true } on success. An invalid or expired token returns 400. Successful unsubscribes are added to the suppression list immediately — subsequent send attempts to that address return 422.

09 Error Handling

All errors follow RFC 9457 Problem JSON with Content-Type: application/problem+json. The instance field always contains the request URL of the failing call.

// 422 — send to a suppressed address

curl -s -X POST https://api.email.squibble.ch/api/v1/messages/send \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "to": ["bounced@example.com"], "subject": "Hello", "html_body": "<p>Hi</p>", "stream": "transactional" }'

{
  "type": "https://email.squibble.ch/docs/errors/messages/send/suppressed",
  "title": "Recipient Suppressed",
  "status": 422,
  "detail": "suppressed: [recipient@example.com]",
  "instance": "/api/v1/messages/send"
}

// 422 — validation failure (missing required fields)

curl -s -X POST https://api.email.squibble.ch/api/v1/messages/send \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "html_body": "<p>Hi</p>" }'

{
  "type": "about:blank",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "The request contains invalid parameters.",
  "instance": "/api/v1/messages/send",
  "errors": [
    {"type": "missing", "loc": ["body", "to"], "msg": "Field required"},
    {"type": "missing", "loc": ["body", "subject"], "msg": "Field required"}
  ]
}

// 429 — rate limit exceeded

{
  "type": "https://email.squibble.ch/docs/errors/rate-limit-exceeded",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Rate limit exceeded",
  "instance": "/api/v1/messages/send"
}

10 Idempotency

Add an Idempotency-Key header (max 255 chars) to make sends safe to retry. The same key returns the original response for 24 hours.

// first attempt

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": ["recipient@example.com"], "subject": "Hello", "html_body": "<p>Hello</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": ["recipient@example.com"], "subject": "Hello", "html_body": "<p>Hello</p>", "stream": "transactional" }'

// → 202  { "message_ids": ["550e8400-e29b-41d4-a716-446655440000"] }  ← same UUID

11 Suppressions & Bounces

Permanent bounces and unsubscribes are added to a per-mailbox suppression list automatically. Any send attempt to a suppressed address returns 422 — the whole request is rejected, not silently skipped. RFC 3463 bounce classification is applied to all delivery failures.

// attempt to send to a suppressed address

curl -s -X POST https://api.email.squibble.ch/api/v1/messages/send \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "to": ["bounced@example.com"], "subject": "...", "html_body": "...", "stream": "transactional" }'

{
  "type": "https://email.squibble.ch/docs/errors/messages/send/suppressed",
  "title": "Recipient Suppressed",
  "status": 422,
  "detail": "suppressed: [bounced@example.com]",
  "instance": "/api/v1/messages/send"
}

How bounces are classified

Delivery failures are tagged with the RFC 3463 enhanced status code returned by the receiving MTA. The class digit determines whether the recipient is added to the suppression list:

  • 5.x.x — hard bounce. Permanent failure: mailbox does not exist, domain has no MX, or the recipient is explicitly rejected. Added to the suppression list immediately.
  • 4.x.x — soft bounce. Temporary failure: greylisting, rate-limit, or transient mailbox-full. Marked as failed on the message but not suppressed — the address remains sendable.
  • Policy / admin rejection. Suppressed only when the receiving MTA classifies the rejection as 5.x.x. A pure policy 4.x.x stays retryable.

Unsubscribes (RFC 8058 one-click or the /api/v1/unsubscribe web form) are added immediately. Suppression is per mailbox: the same address can be hard-bounced for one of your mailboxes and reachable for another. Within a given mailbox, a suppressed address is permanently blocked until the suppression record is cleared by the operator.

Sending to a suppressed address is a bug, not a fallback. We surface it loudly.

12 Health Check

A public, unauthenticated endpoint useful for uptime monitoring and verifying connectivity to the API from your environment.

// verify connectivity — no auth required

curl -s https://api.email.squibble.ch/api/v1/health_check

// 200 response
{
  "status": "ok",
  "hostname": "api-1.squibble.ch",
  "version": "2.0.13",
  "timestamp": "2026-05-07T10:00:00Z"
}

No authentication required. Returns 200 when the service is healthy. Use this to gate your integration startup sequence or confirm your network path to the API is open.

Deep Dives

Integration Walkthrough

A step-by-step guide through the full transactional email lifecycle: send, check delivery status, read the event timeline, and handle bounces.

See the integration walkthrough

Architecture & Design Decisions

IMAP integration strategy, outbound delivery model, bounce processing, and the idempotency contract. Sourced from the repo.

Read the architecture overview

Feature Guides

Per-feature deep dives: outbound API + HTML parsing, async worker queue, tracking endpoints, IMAP bounce processing, and the database + config layer.

Browse the feature overview

Want early access?

Join the waitlist and we'll provision your mailbox and first JWT. Most teams ship their first email within a business day of onboarding.