Emails

Everything about a captured message: the listing, the parsed detail, the raw bytes, its attachments, what it links to, and how it would fare in a real inbox.

List the emails of an inbox

GET /api/v1/inboxes/{inbox_id}/emails?limit=50&cursor=…&query=…
Scope: emails:read

Response:
{
  "data": [
    {
      "id": "e9v2h6r4ldq1a7c3",
      "subject": "Reset your password",
      "from_name": "Acme",
      "from_email": "noreply@acme.dev",
      "excerpt": "Click the link below to choose a new password…",
      "has_attachments": false,
      "attachment_count": 0,
      "received_at": "2026-09-02T09:14:22.000000Z"
    }
  ],
  "meta": { "total": 128, "next_cursor": "e9v2h6r4ldq1a7c3" }
}
Query parameters for listing the emails of an inbox
ParameterAccepts
limitInteger, 1–100, default 50
cursorThe previous page's meta.next_cursor — see pagination
queryString, ≤ 255 — full-text search over subject, sender and body, the same index the app's search box uses

Search across inboxes

GET /api/v1/emails?query=invoice&inbox[]={inbox_id}&inbox[]={inbox_id}
Scope: emails:read

Same response shape and parameters as the listing, over every inbox the token reaches. inbox[] narrows it to some of them; an id the token cannot reach is simply not searched.

Read one email

GET /api/v1/emails/{email_id}
Scope: emails:read

Response:
{
  "id": "e9v2h6r4ldq1a7c3",
  "subject": "Reset your password",
  "from_name": "Acme",
  "from_email": "noreply@acme.dev",
  "recipients": [{ "type": "to", "email": "user@example.com", "name": "" }],
  "text_body": "Click the link below…",
  "html_body": "<!doctype html>…",
  "headers": { "message-id": "<…>", "dkim-signature": "…", "…": "…" },
  "attachment_count": 1,
  "attachments": [
    {
      "id": "a4d8k1p2m7x9c3v6",
      "filename": "receipt.pdf",
      "mime_type": "application/pdf",
      "size": 48213,
      "inline": false,
      "download_url": "https://app.mailclutch.com/api/v1/emails/e9v2h6r4ldq1a7c3/attachments/a4d8k1p2m7x9c3v6"
    }
  ],
  "received_at": "2026-09-02T09:14:22.000000Z",
  "inbox": { "id": "k3n8x2p9qa7c1m4z", "name": "Acme — staging" }
}

The bodies are returned as the message carried them, after MIME parsing. An attachment's download_url is null when the file was recorded but not stored — too large, or a type the pipeline refuses.

The raw message and its attachments

GET /api/v1/emails/{email_id}/raw
Scope: emails:read
→ message/rfc822, byte for byte as captured

GET /api/v1/emails/{email_id}/attachments/{attachment_id}
Scope: emails:read
→ the file, with its own Content-Type and a Content-Disposition filename

The raw endpoint is the export: save it as .eml, open it in a mail client, feed it to another tool. Both are served with X-Content-Type-Options: nosniff.

Links and one-time codes

GET /api/v1/emails/{email_id}/extract
Scope: emails:read

Response:
{
  "email_id": "e9v2h6r4ldq1a7c3",
  "links": [
    { "url": "https://app.acme.dev/reset/8f3k2…", "text": "Reset password", "host": "app.acme.dev", "is_external": true }
  ],
  "codes": [
    { "code": "482913", "score": 9, "context": "Your verification code is 482913" }
  ],
  "primary_code": "482913"
}

For driving a sign-up, reset or two-factor flow onward. Codes are found by how they are worded, so they come as a ranked list; primary_code is set only when one candidate clearly wins, and null when it does not — a null beats a confident wrong answer. The expect call returns the same extraction alongside its verdict, so a test rarely needs this endpoint on its own.

Deliverability analysis

GET /api/v1/emails/{email_id}/analysis
Scope: analysis:run

Response:
{
  "email_id": "e9v2h6r4ldq1a7c3",
  "deliverability": {
    "score": 82,
    "by_category": { "authentication": 30, "content": 22, "…": … },
    "factors": [
      { "key": "dkim", "label": "DKIM signature", "status": "pass", "…": … },
      { "key": "list_unsubscribe", "label": "List-Unsubscribe header", "status": "warning", "…": … }
    ],
    "spam_assassin": { "score": 1.2, "threshold": 5, "rules": [ … ] }
  },
  "authentication": { "spf": { … }, "dkim": { … }, "dmarc": { … } },
  "bimi": { … }
}

A score out of 100 and what drags it down: SPF, DKIM, DMARC, BIMI, List-Unsubscribe and a spam-filter pass. It describes the message as captured and the host you sent from — a sandbox is not your production sending path, so this is about the message, not your reputation.

Delete one email, or empty an inbox

DELETE /api/v1/emails/{email_id}
Scope: emails:delete
→ { "deleted": true }

DELETE /api/v1/inboxes/{inbox_id}/emails
Scope: emails:purge
→ { "deleted": 128 }

Emptying is handy at the start of a run on a permanent inbox, so it contains only what the run produced. With an ephemeral inbox per run there is nothing to empty.