Conventions
The rules that hold across every endpoint, so a client written against one carries over to the rest.
Base URL and formats
https://app.mailclutch.com/api/v1Requests and responses are JSON, except the two that are not: the raw message is message/rfc822 and an attachment is served with its own type. Timestamps are ISO 8601 in UTC (2026-09-02T09:14:22.000000Z). Identifiers are opaque strings: do not parse them, and do not expect them to be sequential.
Pagination
Listings are newest first and paged by cursor. Each page carries the cursor of the next one in meta.next_cursor; pass it back as cursor. When it is null, you have reached the end.
GET /api/v1/inboxes/{inbox_id}/emails?limit=50 → { "data": [...], "meta": { "total": 128, "next_cursor": "e9v2h6r4ldq1a7c3" } } GET /api/v1/inboxes/{inbox_id}/emails?limit=50&cursor=e9v2h6r4ldq1a7c3 → { "data": [...], "meta": { "total": 128, "next_cursor": null } }
A cursor is the id of the last message you received, and the page after it is everything older. That is what makes it safe on an inbox that is still receiving: a new message pushes nothing under you. offset is still accepted for scripts already using it, but a mailbox that receives while being paged by offset silently repeats or skips rows — for a job scanning “everything this run produced”, that is a flaky test with no visible cause.
| Parameter | Accepts |
|---|---|
limit | Integer, 1–100, default 50 |
cursor | The meta.next_cursor of the previous page |
offset | Integer ≥ 0. Ignored when cursor is given |
Idempotency
A runner that times out on a create and retries must not end up with two inboxes. Send an Idempotency-Key header on any POST that creates something — an inbox, a share link, a webhook — and the first successful response is stored for 24 hours and returned verbatim to every retry with the same key. A replay carries Idempotent-Replayed: true.
POST /api/v1/inboxes Idempotency-Key: gha-run-9127384-attempt-2 Content-Type: application/json { "name": "pr-482", "ttl": "30m" }
Keys are scoped to the token, so two organizations using the same key never see each other's response. The same key with a different body is a bug in the caller and answers 422. A failed create is not stored: the retry is evaluated again. A good key is something the run already has — its id, plus the attempt number if the platform retries whole jobs.
Errors
Every error is JSON with a message. Validation errors add an errors object keyed by field, with one or more sentences each.
{ "message": "The name field is required.", "errors": { "name": ["The name field is required."] } }
| Status | Meaning |
|---|---|
| 401 | Missing, malformed, revoked or expired token — see Authentication |
| 403 | The token lacks the scope the endpoint requires |
| 404 | Unknown identifier, or one the token cannot reach |
| 422 | Invalid input, a quota refusal, or an expect verdict of ok: false |
| 429 | Rate limited, or both wait slots of an inbox are busy. Honour Retry-After |
Rate limits
Requests are throttled per token, at 120 per minute. Poll a listing a few times a second at most; for anything busier, wait blocks until the message is there and a webhook lets Mailclutch call you instead.
The blocking calls have their own bound, because each one holds a server worker for as long as it waits: at most two concurrent waits per inbox, and a timeout of at most 120 seconds. A third wait on the same inbox answers 429 with a Retry-After. These do not count against the per-minute limit.