Docs Sign in Get started

REST API

Everything the dashboard does, under https://trapit.dev/api/v1 — which is what you want from a test suite.

Authentication

Mint a key under API keys. Keys start with trap_ and are shown once — only a hash is stored, so there is no way to recover one later. Lost a key, revoke it and mint another.

# bearer token
curl https://trapit.dev/api/v1/whoami \
  -H "Authorization: Bearer trap_…"

# or the header, if that is easier in your client
curl https://trapit.dev/api/v1/whoami \
  -H "X-Api-Key: trap_…"

A key is scoped to the workspace that issued it. There is no cross-workspace read: the API takes no workspace parameter at all, so a key can only ever see its own data.

Resources

Sandboxes

POST   /sandboxes                               # create
GET    /sandboxes                               # list
GET    /sandboxes/{id}
PUT    /sandboxes/{id}                          # name + tags
PUT    /sandboxes/{id}/mocks                    # the ordered HTTP mock rules
DELETE /sandboxes/{id}                          # takes both streams with it

Filtering the message list

GET …/messages takes, besides limit and cursor:

  • q — free-text, matched case-insensitively as a substring of the subject, sender, recipients, body preview and the captured-for address.
  • capturedForexact (case-insensitive) match on an address the mail actually arrived on, which is not necessarily the To header: that is only what the sender wrote. Exact, not substring, so run-4@… never picks up run-42@…'s mail.
  • since — only messages received at or after an ISO-8601 instant (inclusive: passing the newest receivedAt you have seen returns that message again, so de-duplicate by id).
  • isReadfalse for mail you have not yet consumed.

All filters and-combine. A filtered listing scans the sandbox server-side, so prefer an ephemeral sandbox per run over one giant shared one if you filter constantly — that isolates by construction rather than by filter.

Waiting instead of polling

GET …/messages/wait holds the request open until a matching message arrives — the REST counterpart of the MCP wait_for_email tool, so a CI fixture needs no polling loop. It takes subjectContains, fromContains, capturedFor, isRead and timeoutSeconds (default 30, max 300). A message already captured that matches returns immediately; otherwise the first arrival that matches is returned as its full detail. On timeout you get a 204 — distinguishable from the 404 of a sandbox that does not exist.

# block until the invite lands, then mark it consumed
curl "https://trapit.dev/api/v1/sandboxes/{id}/messages/wait?subjectContains=invite&timeoutSeconds=60" \
  -H "Authorization: Bearer trap_…"

curl -X PATCH https://trapit.dev/api/v1/sandboxes/{id}/messages/{messageId} \
  -H "Authorization: Bearer trap_…" \
  -H "Content-Type: application/json" \
  -d '{"isRead": true}'

PATCH with isRead is the non-destructive alternative to deleting a message once a test has consumed it: combined with isRead=false on the list or wait call, a handled message never re-matches, but the artifact is still there when a failing run needs it. false puts a message back.

A create takes a name, optional tags, and optionally ephemeral with ttlMinutes. The response carries everything needed to send it trafficsmtpHost, smtpPort, smtpUsername, smtpPassword and captureUrl — because one sandbox owns both ways in.

The TTL is 1 to 1440 minutes, default 60; outside that you get a 400 rather than a quietly adjusted sandbox, as does a ttlMinutes sent without ephemeral. An ephemeral sandbox expires on its own, so a CI run needs no cleanup step. Either way it captures the moment the 201 lands — nothing to provision, and nothing to wait for.

curl https://trapit.dev/api/v1/sandboxes \
  -H "Authorization: Bearer trap_…" \
  -H "Content-Type: application/json" \
  -d '{"name": "signup", "ephemeral": true, "ttlMinutes": 30}'

Names are not unique and nothing routes on them — a loop can reuse one across runs. The smtpPassword is returned on every read, not just the create: it is viewable by design, unlike an API key.

What a sandbox caught

GET    /sandboxes/{id}/messages                 # trapped email
GET    /sandboxes/{id}/messages/wait            # long-poll for a match
GET    /sandboxes/{id}/messages/{messageId}
PATCH  /sandboxes/{id}/messages/{messageId}     # {"isRead": true}
GET    /sandboxes/{id}/messages/{messageId}/raw # original source
GET    /sandboxes/{id}/messages/{messageId}/attachments/{index}
DELETE /sandboxes/{id}/messages                 # empty this stream only

GET    /sandboxes/{id}/requests                 # trapped HTTP
GET    /sandboxes/{id}/requests/wait            # long-poll for a match
GET    /sandboxes/{id}/requests/{requestId}
GET    /sandboxes/{id}/requests/{requestId}/raw
DELETE /sandboxes/{id}/requests

The two streams are independent: clearing the email one never discards captured requests. Deleting the sandbox is the operation that takes everything.

The request list's q matches method, path, query string, the mock status code, and the name of the mock rule that answered the request. …/requests/wait mirrors the messages wait — method (exact), pathContains (substring) and timeoutSeconds, with the same 200 / 204 / 404 contract.

Account and keys

GET    /account
GET    /whoami                                  # minimal credential probe
POST   /keys
GET    /keys
DELETE /keys/{id}                               # revoke

Conventions

  • Errors come back as application/problem+json, with the same shape everywhere.
  • List endpoints are cursor-paginated: limit defaults to 50 and caps at 200; pass back nextCursor until it is null.
  • Every response carries a correlation id, worth logging if you ever need to ask about one.

Rate limits and quotas

The numbers below are the deployed configuration, quoted live — if this page and the service ever disagree, something is genuinely broken. Over an API limit you get a 429 with a Retry-After header (honour it rather than spinning); the capture-side behaviour is on each capture page.

# REST API (this surface, plus per-IP on /mcp and /oauth)
per key     300 requests / 60 s window   # 429 + Retry-After
per IP      900 requests / 60 s window

# SMTP capture (per message; over-limit is a 421 temp-fail — senders retry, nothing is lost)
per IP      120 messages / 60 s window
per org     600 messages / 60 s window
size        10 MB max per message (advertised via ESMTP SIZE)

# HTTP capture (over-limit 429, oversize 413)
per IP      600 requests / 60 s window
per org     3000 requests / 60 s window
size        5 MB max body

A wait call counts as one request when it starts, however long it then blocks (max 300 s), so long-polling is far cheaper against the key limit than a polling loop. Nothing in the API counts your sandboxes or your stored captures and refuses the next one — the limits it actually applies are the ingress rates above, plus retention: captured items are purged after 30 days (ephemeral sandboxes go at their TTL, taking their captures with them).

Separately from any of that, the free tier is sold with ceilings — 100 sandboxes and 1,000,000 captures per organisation. Those are commercial, not technical: no call starts failing when you pass them, and there is no quota error to code against. They are the point at which we would like a conversation, so if a run is heading that way, mail sales@trapit.io and we will sort out what fits.

The generated reference at https://trapit.dev/api-docs has the full schemas and lets you try calls in the browser; the raw document is at https://trapit.dev/openapi/v1.json if you would rather generate a client.