One token. Every inbox.
A small, token-authenticated REST API for reading and clearing test inboxes from your test runner or CI pipeline. No SDK required — plain HTTP in, JSON out.
Base URL: https://sendtrap.dev/api/v1
Interactive reference ↗ OpenAPI 3.1 (YAML) JSON Postman collection
Authentication
Every inbox has its own API token — find it on the inbox's Settings page, under Integration. Send it as a bearer token on every request. There's no separate account-level key and no OAuth flow: one token, scoped to one inbox, full stop.
curl https://sendtrap.dev/api/v1/inbox \
-H "Authorization: Bearer <your-inbox-token>"
If your HTTP client can't set an Authorization header,
send the token as an X-Api-Token header instead. Requests
without either return 401.
Rate limits
Scaled by your team's plan, per token. Exceeding it returns
429 Too Many Requests. Free is generous enough for a
tight assert-and-poll loop in CI — if you're hitting it, you're probably polling faster than you need to.
| Plan | Requests/min |
|---|---|
| Free | 60 |
| Starter | 120 |
| Team | 300 |
| Business | 600 |
| Enterprise | 1,200 |
See pricing for what each plan includes.
Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /inbox | Details about the authenticated inbox |
| GET | /messages | List messages (paginated, searchable, filterable, optionally blocking) |
| POST | /expect | Wait, match, assert and diagnose in one request — the recommended testing endpoint |
| POST | /assert | Block until a matching message arrives (or timeout), return pass/fail |
| POST | /messages/{id}/extract | Pull verification codes, links, addresses and attachments out of a message |
| GET | /messages/{id} | Full message detail — headers, HTML, text, links, lint checks |
| GET | /messages/{id}/raw | Raw RFC 822 source |
| GET | /messages/{id}/html | Rendered HTML body |
| GET | /messages/{id}/compatibility | HTML Check — email-client HTML/CSS support breakdown (Starter+) |
| GET | /messages/{id}/attachments/{attachment} | Download an attachment |
| PATCH | /messages/{id} | Mark a message read / unread |
| DELETE | /messages/{id} | Delete a single message |
| DELETE | /messages | Delete every message — or only those matching filters |
List messages
Returns the inbox's messages, newest first. Standard Laravel pagination — data for the page of results, meta for page/total info.
| Param | Description |
|---|---|
| search | Matches subject, from address, from name |
| to | Recipient contains this address — checked against the To/Cc headers and the SMTP envelope, so BCC'd recipients match too |
| test_id | Exact match against X-Sendtrap-Test-Id (see below) |
| subject_contains | Subject contains this substring — the same filter /assert uses |
| wait | Seconds to block if no message matches yet, up to 30 — see Wait & assert |
| page | Page number, default 1 |
| per_page | Default 50 |
Tag a message so a test can find it without needing a unique recipient address — set X-Sendtrap-Test-Id on the outgoing mail (any mail library lets you add a custom header) and filter by it later. Handy for flows where you don't control the recipient, like an admin-notification email that always goes to a fixed address.
curl "https://sendtrap.dev/api/v1/messages?test_id=ci-run-482&to=live-mail%2Babc%40example.com" \
-H "Authorization: Bearer <token>"
{
"data": [
{
"id": 33,
"test_id": "ci-run-482",
"from_address": "hello@acmeanvils.com",
"from_name": "Acme Anvils",
"to": [{ "name": null, "address": "live-mail+abc@example.com" }],
"envelope_to": ["live-mail+abc@example.com"],
"subject": "Welcome to Acme Anvils Ltd",
"size": 7146,
"is_read": false,
"has_attachments": false,
"has_unresolved_merge_tags": false,
"received_at": "2026-07-13T12:46:08+00:00"
}
],
"links": { "first": "...", "last": "...", "prev": null, "next": null },
"meta": { "current_page": 1, "last_page": 1, "per_page": 50, "total": 1 }
}
envelope_to is the SMTP RCPT TO list, captured independently of the To/Cc headers — it's the only reliable way to see a BCC'd recipient, since BCC is invisible in headers by definition. has_unresolved_merge_tags flags a message whose body still contains an unresolved {{ tag }} or %tag% placeholder — see the detail response below for the full list.
Wait & assert
Mail arrives asynchronously, so a naive first request often runs before it lands. Instead of sleep-polling yourself, add wait=<seconds> to GET /messages — if nothing matches yet, the request blocks (checking every ~250ms–1s) until a match arrives or the timeout hits, capped at 30s. One request, no sleep loop, no extra round trips.
curl "https://sendtrap.dev/api/v1/messages?test_id=ci-run-482&wait=15" \
-H "Authorization: Bearer <token>"
POST /assert takes the same matching idea further: give it a condition, it waits for a match (or the timeout) and always returns 200 with a pass/fail flag in the body — an unmatched assertion is an expected test outcome, not an HTTP error, so you don't need special-case error handling in your assertion library.
curl -X POST https://sendtrap.dev/api/v1/assert \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"test_id": "ci-run-482", "subject_contains": "Welcome", "timeout": 10}'
{
"matched": true,
"message": { "id": 33, "test_id": "ci-run-482", "subject": "Welcome to Acme Anvils Ltd", "..." : "..." }
}
/assert accepts the same filters as List messages (search, to, test_id, subject_contains) plus timeout (seconds, capped at 30, omit or 0 for an instant check with no blocking). Both endpoints share a tighter rate limit than ordinary API calls — 15 wait/assert requests per minute per token, on every plan — because a blocking request can hold a connection open for its full timeout, so keep parallel wait/assert calls modest (a handful per token, not one per test in a large matrix).
POST /expect is the richer successor: it separates match conditions (which message are we waiting for?) from assert conditions (is its content right?), so a miss tells you whether no mail arrived, mail arrived but didn't match, or the right mail arrived with the wrong content — with per-condition pass/fail and safe actual values. Conditions cover subject, recipients, envelope, bodies, headers, links, attachments and quality checks. An optional extract object additionally pulls named values — verification codes, links, addresses, attachments — out of the matched message in the same request: see Extract values.
curl -X POST https://sendtrap.dev/api/v1/expect \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"match": [{"field": "to", "op": "contains", "value": "alice@example.com"},
{"field": "subject", "op": "contains", "value": "Welcome"}],
"assert": [{"field": "links", "op": "matches", "value": "example\\.com/verify\\?code=\\d+"}],
"scope": {"test_id": "ci-run-482"},
"wait": {"timeout_ms": 10000},
"mode": "strict"
}'
In strict mode an unmet expectation returns
422 with the full diagnostic body, so a
plain HTTP-error check fails the CI step; the default report mode
always returns 200. The complete field and
operator matrix is in the interactive reference.
min_compatibility_score (0–100, Starter plan and above) additionally requires the matched message's HTML Check compatibility ratio to be at or above this value — handy for gating a CI run on email-client compatibility, not just on the message arriving. Only the matched message is checked (not the whole inbox), so this adds at most one HTML Check's worth of latency to the request.
Extract values
Signup-verification, password-reset and magic-link tests all end the same way: fishing a code or link out of an email. Named extractors do that server-side, so your test never parses MIME or regexes HTML. Add an extract object to POST /expect and matching + extraction happen atomically in one request — wait for the mail, get the code back:
curl -X POST https://sendtrap.dev/api/v1/expect \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"match": [{"field": "to", "op": "contains", "value": "alice@example.com"},
{"field": "subject", "op": "contains", "value": "Verify"}],
"extract": {
"code": {"type": "code", "near": "verification code"},
"verify_link": {"type": "link", "path_prefix": "/verify"}
},
"wait": {"timeout_ms": 10000},
"mode": "strict"
}'
{
"matched": true,
"status": "matched",
"extract": {
"code": { "found": true, "status": "found", "value": "482913", "source": "text",
"context": "Your verification code is 482913. It expires in 10 minutes." },
"verify_link": { "found": true, "status": "found",
"value": { "url": "https://app.example.com/verify?token=abc123", "text": "Verify my account" } }
}
}
Extraction is part of the expectation: a missing value keeps the wait polling (a later message can
still satisfy everything at once) and otherwise reports
status: extraction_failed — in strict mode that's a
422, so the CI step fails. Mark an
extractor "optional": true to keep a miss from failing the run.
The same extractors also run against a message you already have, via
POST /messages/{id}/extract.
Five extractor types:
| Type | What it does | Options |
|---|---|---|
| code | Verification-code helper — finds a standalone token in the visible text (never inside a longer id or URL hash). With near, the token closest to the anchor phrase wins. | length (4–12, default 6), charset (digits | letters | upper | alphanumeric), near, from |
| link | Selects a link from the HTML body — criteria AND together, links are returned, never fetched. Relative URLs stay relative unless the mail declares a valid absolute <base href>. | url, host, path_prefix, query_param, text_contains, matches |
| regex | Bounded regex capture (first capture group, else the whole match). Patterns are server-delimited — input never chooses modifiers. | pattern, from (text | html | subject | header.<Name>) |
| address | An {address, name} pair from the headers or the SMTP envelope — envelope_to catches BCC-only recipients. | field (from | to | cc | envelope_from | envelope_to), matches |
| attachment | Attachment metadata (id, filename, content type, size, checksum) plus its authenticated download URL — bytes are never inlined. | filename, filename_contains, matches, content_type (wildcard subtype: application/*) |
Every extractor takes select (first | last | all). Without it, one match is returned directly and several distinct matches come back as status: ambiguous with the candidate list — the server never guesses which value your test meant. Results are typed and diagnosable: found, the source field searched, a bounded context excerpt, and a matches count. Caps: 10 extractors per request, 256-byte regexes. The full option matrix is in the interactive reference.
Get a message
Full detail for one message — parsed HTML and text bodies, all headers, and attachment metadata.
curl https://sendtrap.dev/api/v1/messages/33 \
-H "Authorization: Bearer <token>" | jq '.data.subject'
{
"data": {
"id": 33,
"inbox_id": 15,
"message_id": "95024650-...@example.com",
"test_id": "ci-run-482",
"envelope_from": "bounce@acmeanvils.com",
"envelope_to": ["test@example.com"],
"from_address": "hello@acmeanvils.com",
"from_name": "Acme Anvils",
"to": [{ "name": "Test User", "address": "test@example.com" }],
"cc": [],
"subject": "Welcome to Acme Anvils Ltd",
"size": 7146,
"is_read": false,
"has_html": true,
"has_text": true,
"has_attachments": false,
"has_unresolved_merge_tags": false,
"unresolved_merge_tags": [],
"received_at": "2026-07-13T12:46:08+00:00",
"html": "<html>...</html>",
"text": "Welcome...",
"links": ["https://acmeanvils.com/verify?token=abc123"],
"checks": [
{ "key": "missing_text_part", "passed": true, "severity": "warn" },
{ "key": "oversized_html", "passed": true, "severity": "warn" },
{ "key": "missing_list_unsubscribe", "passed": true, "severity": "info" },
{ "key": "from_address_present", "passed": true, "severity": "error" }
],
"headers": [{ "name": "Subject", "value": "Welcome to Acme Anvils Ltd" }],
"attachments": [],
"urls": {
"raw": "https://sendtrap.dev/api/v1/messages/33/raw",
"html": "https://sendtrap.dev/api/v1/messages/33/html"
}
}
}
links is every href pulled from the HTML body — handy for asserting a verification link's full shape (including query string) without wrestling with &-encoded entities in the raw HTML, or for feeding it back into a test to complete a verification round-trip. checks is a lint report — each entry's passed is false when that check fails; treat it as a heuristic, not a guarantee (broken-image-URL checking isn't included, since that would mean this service fetching arbitrary remote URLs on your behalf).
Messages belonging to a different inbox than the one your token authenticates always return
404 — never a 403, so you can't probe for the existence of IDs outside your inbox.
HTML Check Starter+
Checks a message's HTML/CSS against caniemail.com's email-client feature-support data and flags anything unsupported or partially supported, and in which clients. Computed on first request and cached — later requests for the same message return instantly unless the underlying support data has since been refreshed. The HTML Check tab in the dashboard is available on every plan; this API endpoint (plus the checks[] summary entry below and assert's min_compatibility_score) requires Starter or above.
curl https://sendtrap.dev/api/v1/messages/33/compatibility \
-H "Authorization: Bearer <token>"
{
"status": "ok",
"compatibility_ratio": 76.5,
"issues": [
{
"feature_id": "css-gap",
"title": "gap, column-gap, row-gap",
"category": "css",
"severity": "error",
"unsupported_clients": [
{ "client": "outlook", "platform": "windows", "support": "n", "note": null },
{ "client": "yahoo", "platform": "desktop-webmail", "support": "n", "note": null }
]
}
],
"checked_at": "2026-07-13T12:47:03+00:00"
}
compatibility_ratio is the percentage of distinct HTML/CSS features detected in the message that are fully supported across a fixed reference set of major clients (Apple Mail, Gmail, Outlook, Yahoo and a few others) — equally weighted, not market-share weighted, since no reliable market-share dataset exists. Treat it as a rough filter for CI gating (see min_compatibility_score in Wait & assert), not a precise "% of your subscribers will see this correctly" figure.
On plans below Starter, this endpoint returns
403, and the checks[] array
on Get a message simply omits the
html_compatibility entry — it's never forced to compute just because you fetched
a message, so it only appears once something (the dashboard tab or this endpoint) has actually run the check.
Raw & rendered HTML
urls.raw and
urls.html on a message's detail response are
ready-to-fetch links (same bearer token required) — useful if you want the full RFC 822 source
or a sandboxed-render-ready HTML string without re-fetching the message detail.
curl https://sendtrap.dev/api/v1/messages/33/raw \
-H "Authorization: Bearer <token>"
Attachments
Each attachment listed on a message detail response includes its own bearer-token-authenticated
url — fetch it directly to get the file bytes.
It also includes a checksum (sha256 of the raw
content) and content_type, so you can assert an
attachment is present and non-trivial without downloading it.
curl https://sendtrap.dev/api/v1/messages/33/attachments/9 \
-H "Authorization: Bearer <token>" \
-o invoice.pdf
Mark as read
PATCH with an
is_read boolean (defaults to true).
curl -X PATCH https://sendtrap.dev/api/v1/messages/33 \
-H "Authorization: Bearer <token>" \
-d is_read=true
Delete messages
Delete one message by ID, clear the whole inbox, or — by adding any of the List messages filters (search, to, test_id, subject_contains) — delete only the matching messages. Filtered deletes are what you want on a shared inbox: each test run cleans up its own mail and leaves everyone else's alone.
curl -X DELETE \
https://sendtrap.dev/api/v1/messages/33 \
-H "Authorization: Bearer <token>"
curl -X DELETE \
"https://sendtrap.dev/api/v1/messages?test_id=ci-run-482" \
-H "Authorization: Bearer <token>"
# → { "deleted": 3 }
curl -X DELETE \
https://sendtrap.dev/api/v1/messages \
-H "Authorization: Bearer <token>"
# → { "deleted": 12 }
Reserve the unfiltered form for an inbox only one runner uses at a time — it deletes everything, including messages a parallel run is still asserting on.
Errors
| Status | Meaning |
|---|---|
| 401 | Missing or invalid token |
| 403 | Request IP not on the inbox's allowlist (if configured), or a Starter+ feature called on a plan below Starter |
| 404 | Message/attachment not found, or belongs to a different inbox |
| 429 | Rate limit exceeded — see plan limits above |
CI recipe
Mail arrives asynchronously, so don't assume it's there the instant your app finishes the request. Use /expect instead of hand-rolling a sleep loop — one request, no flaky timing, and a tagged test_id means parallel runs sharing an inbox can't see each other's mail even without unique recipient addresses.
# tag the outgoing mail with X-Sendtrap-Test-Id: $CI_RUN_ID, then — strict mode
# turns an unmet expectation into a non-2xx, so --fail-with-body both fails the
# step and prints the diagnostic explaining exactly what didn't match. The
# `|| status=$?` captures the verdict without tripping `set -e`, so cleanup
# below always runs and the step still fails at the end:
status=0
curl -sS --fail-with-body -X POST https://sendtrap.dev/api/v1/expect \
-H "Authorization: Bearer $SENDTRAP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"match": [{"field": "test_id", "op": "equals", "value": "'"$CI_RUN_ID"'"},
{"field": "subject", "op": "contains", "value": "Welcome to Acme Anvils Ltd"}],
"assert": [{"field": "has_unresolved_merge_tags", "op": "equals", "value": false}],
"wait": {"timeout_ms": 10000},
"mode": "strict"
}' || status=$?
# clean up this run's mail — the filter leaves parallel runs' messages intact,
# and this runs on failure too
curl -s -X DELETE "https://sendtrap.dev/api/v1/messages?test_id=$CI_RUN_ID" \
-H "Authorization: Bearer $SENDTRAP_TOKEN"
exit $status
Store the token as a CI secret (e.g. SENDTRAP_TOKEN) — never commit it.
Give each parallel test environment (staging, PR previews, per-branch) its own inbox and its own token so
runs can't see each other's mail — or, if provisioning a separate inbox per run isn't practical, a unique
test_id per run does the same job inside a single shared inbox.
The examples below all follow the same three-part pattern: scope each test to its own mail (a unique recipient address per test, or a test_id when you don't control the recipient), expect with one server-side /expect call instead of a sleep loop, and clean up with the same filter in teardown — which runs on failure too, so a red test can't leak messages into the next one. If a raw POST is awkward in your tooling, GET /messages?to=…&wait=10 gives you the same server-side wait on a plain GET.
PHPUnit (Laravel)
One /expect call replaces the poll loop —
match conditions find this test's message, assert conditions verdict its content — and the
filtered tearDown() delete removes only this
test's mail, safe even when several suites share the inbox.
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
class WelcomeEmailTest extends TestCase
{
// unique recipient per test = this test's mail is identifiable,
// even when parallel runs share the inbox
protected string $email;
protected function setUp(): void
{
parent::setUp();
$this->email = 'welcome-'.Str::uuid().'@example.com';
}
protected function tearDown(): void
{
// delete only this test's mail — runs on failure too, so a red
// test can't leak messages into the next run
$this->sendtrap()->delete('/messages?to='.$this->email);
parent::tearDown();
}
protected function sendtrap(): \Illuminate\Http\Client\PendingRequest
{
return Http::withToken(config('services.sendtrap.token'))
->baseUrl('https://sendtrap.dev/api/v1');
}
public function test_it_sends_a_welcome_email(): void
{
User::factory()->create(['email' => $this->email]);
// one request: waits server-side (up to 10s), matches, and asserts content
$result = $this->sendtrap()->post('/expect', [
'match' => [
['field' => 'to', 'op' => 'contains', 'value' => $this->email],
['field' => 'subject', 'op' => 'equals', 'value' => 'Welcome to Acme Anvils Ltd'],
],
'assert' => [
['field' => 'has_unresolved_merge_tags', 'op' => 'equals', 'value' => false],
],
'wait' => ['timeout_ms' => 10000],
])->json();
// status explains a failure: no_candidates / no_match / assertions_failed
$this->assertSame('matched', $result['status'], json_encode($result['conditions']));
}
}
Jest / Vitest (Node.js)
const sendtrap = (path, opts = {}) =>
fetch(`https://sendtrap.dev/api/v1${path}`, {
...opts,
headers: {
Authorization: `Bearer ${process.env.SENDTRAP_TOKEN}`,
'Content-Type': 'application/json',
},
}).then((r) => r.json());
// unique recipient per test — identifiable even on a shared inbox
const email = `welcome-${crypto.randomUUID()}@example.com`;
afterEach(async () => {
// delete only this test's mail — runs on failure too
await sendtrap(`/messages?to=${email}`, { method: 'DELETE' });
});
test('sends a welcome email', async () => {
await createUser({ email });
// one request: waits server-side (up to 10s), matches, and asserts content
const result = await sendtrap('/expect', {
method: 'POST',
body: JSON.stringify({
match: [
{ field: 'to', op: 'contains', value: email },
{ field: 'subject', op: 'equals', value: 'Welcome to Acme Anvils Ltd' },
],
assert: [{ field: 'has_unresolved_merge_tags', op: 'equals', value: false }],
wait: { timeout_ms: 10000 },
}),
});
// status explains a failure: no_candidates / no_match / assertions_failed
expect(result.status).toBe('matched');
});
pytest (Python)
import os, uuid, requests, pytest
BASE = "https://sendtrap.dev/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SENDTRAP_TOKEN']}"}
@pytest.fixture
def test_email():
# unique recipient per test — identifiable even on a shared inbox
email = f"welcome-{uuid.uuid4()}@example.com"
yield email
# delete only this test's mail — runs on failure too
requests.delete(f"{BASE}/messages", params={"to": email}, headers=HEADERS)
def test_sends_a_welcome_email(test_email):
create_user(email=test_email)
# one request: waits server-side (up to 10s), matches, and asserts content
result = requests.post(f"{BASE}/expect", headers=HEADERS, json={
"match": [
{"field": "to", "op": "contains", "value": test_email},
{"field": "subject", "op": "equals", "value": "Welcome to Acme Anvils Ltd"},
],
"assert": [{"field": "has_unresolved_merge_tags", "op": "equals", "value": False}],
"wait": {"timeout_ms": 10000},
}).json()
# status explains a failure: no_candidates / no_match / assertions_failed
assert result["status"] == "matched", result["conditions"]
Signup verification / password reset / magic link (Playwright)
The flows that used to need custom email parsing are one extract object away: wait for the mail and get the OTP code or magic link back, typed, in the same request — then drive the browser with it. The same shape covers password resets (extract the reset link) and invoice mails (extract the attachment's download URL).
test('verifies a new account with the emailed code', async ({ page }) => {
await page.goto('/signup');
await page.fill('#email', email);
await page.click('#submit');
// one request: wait for the mail, extract the code and the magic link
const result = await sendtrap('/expect', {
method: 'POST',
body: JSON.stringify({
match: [{ field: 'to', op: 'contains', value: email }],
extract: {
code: { type: 'code', near: 'verification code' },
link: { type: 'link', path_prefix: '/verify' },
},
wait: { timeout_ms: 10000 },
mode: 'strict',
}),
});
// typed values, no email parsing: submit the OTP…
await page.fill('#otp', result.extract.code.value);
// …or follow the magic link directly
// await page.goto(result.extract.link.value.url);
});
Coming from Mailtrap
Migrating a Mailtrap Email Sandbox test helper? Every endpoint you're already calling has a compatible
alias under /api/sandboxes/{sandbox}/... —
swap the base URL and token and it should just work.
# Mailtrap
curl https://sandbox.api.mailtrap.io/api/sandboxes/12345/messages \
-H "Api-Token: <token>"
# Sendtrap — same shape, your inbox's token, sandbox id is ignored
curl https://sendtrap.dev/api/sandboxes/12345/messages \
-H "Authorization: Bearer <your-inbox-token>"
The {sandbox} segment is accepted but not
checked — your bearer token already scopes every request to one inbox, so whatever sandbox ID your old script
has hardcoded is fine to leave in place.
| Method | Path | Mailtrap equivalent |
|---|---|---|
| GET | /sandboxes/{s}/messages | Get Messages (search, page, last_id) |
| GET | /sandboxes/{s}/messages/{id} | Show Email Message |
| PATCH | /sandboxes/{s}/messages/{id} | Update Message — {"message":{"is_read":true}} |
| DELETE | /sandboxes/{s}/messages/{id} | Delete Message |
| GET | .../messages/{id}/body.txt | Get Text Message Body |
| GET | .../messages/{id}/body.html | Get Formatted HTML Message |
| GET | .../messages/{id}/body.htmlsource | Get HTML Message Source |
| GET | .../messages/{id}/body.raw | Get Raw Message Body |
| GET | .../messages/{id}/body.eml | Get Message as EML |
| GET | .../messages/{id}/mail_headers | Get Mail Headers |
| GET | .../messages/{id}/attachments | Get Attachments |
| GET | .../attachments/{id} | Get Single Attachment |
| GET | .../attachments/{id}/download | (file bytes, not just metadata) |
| PATCH | /sandboxes/{s}/clean | Clean Sandbox |
| PATCH | /sandboxes/{s}/all_read | Mark All as Read |
Not implemented — these have no real equivalent in Sendtrap's model,
so we don't fake them: message templates (template_id/template_variables),
HTML client-compatibility analysis, spam/blacklist reports, message forwarding, POP3 access, and
account-level project/sandbox management (create, list, or delete sandboxes) — a Sendtrap token is
already scoped to one inbox, so there's nothing to list or switch between.
Ready to wire it up?
Create a free inbox, grab its token from Settings, and you're making assertions in CI within minutes.
Create your inboxNo credit card required