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 gated by a separate token scope 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 issued per mailbox. We recommend creating one token per agent or service — this keeps the blast radius small if any single token is compromised. 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, folders:write, messages:move. The messages:index scope covers both listing and fetching individual messages or their events, plus browsing the folder tree; folders:write and messages:move gate folder mutations and inter-folder message moves respectively. Calling an endpoint outside your token's scope returns 403.

Tokens do not expire. A token remains valid until explicitly revoked by its JTI via the operator CLI. You can obtain a replacement token by contacting your Squibble account contact.

03 Sending Email

Requires the messages:send scope.

POST /api/v1/messages/send

Idempotency-Key: 550e8400-e29b-41d4-a716-446655440001  # optional header, any unique string (UUID recommended), 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: 550e8400-e29b-41d4-a716-446655440001" \
  -d '{
    "to":      ["recipient@example.com"],
    "cc":      [],
    "bcc":     [],
    "subject": "Hello",
    "html_body": "<p>HTML body</p>",
    "text_body": "Plain text body",  # optional; if omitted, derived from html_body automatically
    "stream": "transactional"  # "transactional" (default) | "marketing" — controls queue priority and suppression rules
  }'

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: 550e8400-e29b-41d4-a716-446655440001  # optional header, any unique string (UUID recommended), 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
ATTACHMENT_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\":      \"$ATTACHMENT_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 your mailbox identifier — the same address provided during onboarding (e.g. inbox@yourcompany.squibble.ch). You can also extract it from the sub claim of your JWT.

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"

// example list response

{
  "messages": [
    {
      "id": 42,
      "from": "sender@example.com",
      "to": ["inbox@yourcompany.squibble.ch"],
      "subject": "Your order confirmation",
      "received_at": "2026-05-14T09:23:11Z",
      "has_attachments": false
    }
  ],
  "total": 1,
  "limit": 10,
  "offset": 0
}

Messages are returned newest-first. 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.

05.5 Folders & Folder Tree

List, create, delete, and subscribe to a mailbox's hierarchical folder tree, and move or copy messages between folders in bulk. Folder trees and thin metadata (MESSAGES / UNSEEN counts, UIDVALIDITY, UIDNEXT) are cached in Postgres so navigation sidebars render in under 10 ms — message bodies are still streamed statelessly from IMAP on demand. See the folder cache deep dive for the architecture.

GET     /api/v1/email/account/{mailbox}/folders  # list the cached folder tree
POST    /api/v1/email/account/{mailbox}/folders  # create a folder
DELETE  /api/v1/email/account/{mailbox}/folders/{folder_path}  # delete a folder
PUT     /api/v1/email/account/{mailbox}/folders/{folder_path}/subscription  # subscribe / unsubscribe
POST    /api/v1/email/account/{mailbox}/folders/{folder_path}/messages/actions  # bulk move / copy

Scopes

  • Listing the tree requires messages:index.
  • Create, delete, and subscription changes require folders:write.
  • Bulk move / copy requires messages:move.

A read-only messages:index token can browse the folder tree but cannot mutate folders or move messages.

// list the subscribed folder tree

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

Query parameters: subscribed_only (default true) and force_refresh (default false — runs an inline IMAP sync before returning the tree).

{
  "data": [
    {
      "path": "INBOX/Invoices",
      "name": "Invoices",
      "delimiter": "/",
      "special_use": "archive",
      "is_subscribed": true,
      "messages_count": 42,
      "unseen_count": 3
    }
  ]
}

// create a nested folder (201 Created)

curl -s -X POST https://api.email.squibble.ch/api/v1/email/account/{mailbox}/folders \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "path": "INBOX/Receipts/2026" }'

// delete a folder (204 No Content)

curl -s -X DELETE https://api.email.squibble.ch/api/v1/email/account/{mailbox}/folders/INBOX/Receipts/2026 \
  -H "Authorization: Bearer $TOKEN"

// unsubscribe from a folder

curl -s -X PUT https://api.email.squibble.ch/api/v1/email/account/{mailbox}/folders/INBOX/Invoices/subscription \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "subscribed": false }'

Bulk move / copy

Move or copy messages by UID from the folder in the path to a target_folder. Requires messages:move.

curl -s -X POST https://api.email.squibble.ch/api/v1/email/account/{mailbox}/folders/INBOX/messages/actions \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "move",
    "target_folder": "Archive/Processed",
    "uids": [101, 102, 103]
  }'

action is "move" or "copy". On servers advertising the IMAP MOVE capability (RFC 6851) the operation runs atomically; otherwise it transparently falls back to COPY + \Deleted + EXPUNGE. Folder paths are validated against control-character injection — carriage returns, line feeds, and other control bytes are rejected with 422.

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"

// example list response

{
  "messages": [
    {
      "message_id": "550e8400-e29b-41d4-a716-446655440000",
      "to": ["recipient@example.com"],
      "subject": "Hello",
      "status": "sent",
      "stream": "transactional",
      "sent_at": "2026-05-14T09:23:11Z",
      "created_at": "2026-05-14T09:23:00Z"
    }
  ]
}

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: 550e8400-e29b-41d4-a716-446655440001" \
  -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: 550e8400-e29b-41d4-a716-446655440001" \
  -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.

13 MCP / AI Agents

The official Squibble MCP server exposes 15 tools covering sending, inbox reading, delivery tracking, and folder management. It is available as a hosted HTTP/SSE endpoint — no local install needed — and as a stdio container image for air-gapped or local setups. Both transports carry your own scoped JWT; the token is never logged, cached, or persisted beyond the single request it authorises.

HTTP/SSE — hosted (recommended)

Point any MCP-compatible client directly at https://api.email.squibble.ch/mcp. The endpoint handles session initialisation, tool dispatch, and SSE streaming. Pass your JWT as a Bearer token on the initial request.

// claude_desktop_config.json — HTTP transport (no Docker required)

{
  "mcpServers": {
    "squibble": {
      "url": "https://api.email.squibble.ch/mcp",
      "headers": {
        "Authorization": "Bearer <your-jwt-token>"
      }
    }
  }
}

Stdio — local container (air-gapped / local agents)

Run the published container image locally. The MCP server process reads your JWT from the environment and proxies every tool call to the REST API over HTTPS.

// claude_desktop_config.json — stdio transport

{
  "mcpServers": {
    "squibble": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "SQUIBBLE_API_BASE_URL",
        "-e", "SQUIBBLE_JWT",
        "registry.git.intern.squibble.me/me.squibble/applications/me.squibble.email/mcp:latest"
      ],
      "env": {
        "SQUIBBLE_API_BASE_URL": "https://api.email.squibble.ch",
        "SQUIBBLE_JWT": "<your-jwt-token>"
      }
    }
  }
}

Tools & required scopes

Issue tokens with only the scopes the agent needs. A token without the required scope returns 403 from the gateway — surfaced verbatim to the agent, never swallowed.

Tool Scope required
send_email messages:send
send_markdown messages:send
forward_message messages:send
list_messages messages:index
get_message messages:show
get_attachment attachments:show
wait_for_message messages:index
list_sent_messages messages:index
get_sent_message messages:index
get_message_events messages:index
list_folders messages:index
create_folder folders:write
delete_folder folders:write
set_folder_subscription folders:write
move_messages messages:move

Security model

  • Every tool call forwards your JWT as Authorization: Bearer — no separate MCP credential layer.
  • Sender binding, suppression checks, send quotas, and domain allowlists are all enforced by the gateway, not the MCP server.
  • The JWT is held in memory only for the duration of the tool call. It is never written to disk, logged, or cached.
  • All successful tool calls appear in the gateway audit log under the token's JTI — indistinguishable from direct API calls.

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, hierarchical folder cache & management, 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.