# Devmailr > Devmailr is a disposable email inbox service for developers. Create temporary > mailboxes (`prefix@devmailr.app`), send mail to them from the system under test, > then read the received emails over a REST API — ideal for signup/OTP/password-reset > flows in test suites, CI pipelines, and AI agent workflows. This file is a self-contained API reference for LLMs and coding agents. Human-readable docs live at https://app.devmailr.app/docs (login required). ## Essentials - Base URL: `https://api.devmailr.app/api` - Auth: `Authorization: Bearer dmk_...` header. API keys are created in the dashboard (Settings → API Keys) and shown only once. The REST API requires an Indie or Pro plan; if the account downgrades to Free, existing keys return 403 until it re-upgrades. - Key scopes: `read` (list/read only), `crud` (+ create/delete mailboxes and emails), `crud_webhooks` (+ manage webhook endpoints). - All responses are JSON. Errors are `{ "error": "message" }` with standard status codes (400 invalid input, 401 bad key, 403 plan/scope limit, 404 not found, 429 rate limited). - Rate limit: 300 requests/minute per IP. On 429, honor the `RateLimit-*` headers and back off. - Mailbox addresses are always `@devmailr.app`. Mail sent to an expired or nonexistent mailbox is silently discarded. ## Plans | Plan | Mailboxes | Mailbox lifetime | API | Webhooks | Custom prefix | |-------|-----------|------------------|-----|----------|---------------| | Free | 1 | 24 hours | no | no | no | | Indie | 10 | 7 days | yes | yes | yes | | Pro | unlimited | no expiry | yes | yes | yes | Each mailbox holds up to 500 emails (oldest dropped first); body fields are capped at 1 MB each. ## Fair use & rate limits Per-plan limits enforced server-side: | Limit | Free | Indie | Pro | |--------------------------|------------|-------------|--------------| | Concurrent mailboxes | 1 | 10 | Unlimited† | | Mailbox expiry | 24 hours | 7 days | Never | | Email retention | (mailbox expiry) | (mailbox expiry) | 30 days | | Mailbox creations | 5 / day | 20 / hour | 60 / hour | | Inbound emails / account | 200 / day | 2,000 / day | 20,000 / day | | Inbound per mailbox | 50 / hour | 50 / hour | 50 / hour | † Pro "Unlimited" mailboxes have a soft cap of 200 concurrent mailboxes. The cap is raised per account on request. Behavior: - Exceeding a mailbox-creation cap returns HTTP 429 with a `Retry-After` header indicating when the client can retry. - Inbound mail beyond the per-account daily cap or the per-mailbox hourly cap is silently dropped — it is never stored or delivered. - Pro email retention: emails older than 30 days are automatically pruned. Free and Indie emails are removed when their mailbox expires. ## Endpoints ### Mailboxes - `GET /mailboxes` (scope: read) — list active mailboxes, newest first. - `POST /mailboxes` (scope: crud) — create a mailbox. Body is optional JSON: `{ "prefix": "my-project" }` (3–32 lowercase letters/digits/hyphens, must start and end alphanumeric; Indie/Pro only). Omit the body for a random prefix. Returns 201 with the mailbox object; 403 if your plan's mailbox limit is reached. - `DELETE /mailboxes/:id` (scope: crud) — delete the mailbox and all its emails. Returns `{ "ok": true }`. - `GET /mailboxes/:id/emails` (scope: read) — up to 100 emails, newest first, metadata only (`_id, from, to, subject, date, isRead`), plus the derived `extractedCodes`, `primaryLink`, and `verdicts` (see below) so you can read a one-time code or spam flag without a second request. Fetch a single email for its body and full link list. Optional filters (combined with AND): `from`, `subject` (case-insensitive substring), `q` (substring across subject/from/ body), `since` and `before` (ISO 8601 or epoch ms, on receive time), `unread`, `has_code`. Example: `?from=stripe&has_code=true`. - `GET /mailboxes/:id/wait` (scope: read) — **long-poll**: block until an email arrives and return it, instead of polling the list yourself. Optional query: `from` and `subject` (case-insensitive substring), `has_code=true` (only match emails that have an extracted code), `match` (a regex the subject/body must match — also returns its captures as `matches`; see "Custom regex matcher"), `received_after` (ISO 8601 or epoch ms — only emails received strictly after this instant), `timeout` (seconds, default 30, max 90). On a match returns 200 with the same object as `GET /emails/:id`; on timeout returns 404 `{ "error": "...", "timedOut": true }`. Without `received_after`, a matching email already in the mailbox is returned immediately (so you never miss mail that lands before you start waiting). One wait call replaces ~30 list polls and the rate-limit cost that comes with them. - `GET /mailboxes/:id/stream` (scope: read) — **Server-Sent Events**: holds an HTTP connection open and pushes each inbound email as it arrives, so you can receive mail without hosting a webhook (ideal for CI runners behind NAT). Send `Authorization: Bearer dmk_...` and read the stream (`curl -N`). Each event is `event: email.received` + `data: `; lines starting with `:` are heartbeat comments (every ~25s). Optional filters: `from`, `subject` (substring), `has_code`. The connection stays open until you close it. Mailbox object: ```json { "_id": "665f1c2e9b3a7d0012345678", "address": "my-project@devmailr.app", "prefix": "my-project", "isCustomPrefix": true, "expiresAt": "2026-06-18T12:00:00.000Z", "createdAt": "2026-06-11T12:00:00.000Z" } ``` `expiresAt` is `null` on the Pro plan (no expiry). ### Emails - `GET /emails/recent?limit=5` (scope: read) — most recent emails across all mailboxes (limit 1–20, default 5). Each item includes `mailboxPrefix`. - `GET /emails/:id` (scope: read) — full email including `bodyText`, `bodyHtml`, parsed `headers`, `attachments` metadata, the derived `extractedCodes`, `extractedLinks`, and `primaryLink` fields, and SES `verdicts`. Optional `?match=` (see "Custom regex matcher") adds a `matches` array. - `GET /emails/:id/attachments/:index` (scope: read) — returns `{ "url", "filename", "contentType" }` where `url` is a short-lived (60-second) presigned download link. Attachments are stored only on paid plans (Indie/Pro); Free-plan emails always have an empty `attachments` array. - `DELETE /emails/:id` (scope: crud) — delete the email. Returns `{ "ok": true }`. Email object (full): ```json { "_id": "665f1d419b3a7d0012345679", "mailboxId": "665f1c2e9b3a7d0012345678", "from": "noreply@example.com", "to": "my-project@devmailr.app", "subject": "Your verification code", "date": "2026-06-11T12:01:30.000Z", "bodyText": "Your code is 482913. Confirm at https://example.com/verify?t=abc", "bodyHtml": "

Your code is 482913

", "headers": {}, "attachments": [ { "filename": "invoice.pdf", "size": 20480, "contentType": "application/pdf" } ], "extractedCodes": ["482913"], "extractedLinks": ["https://example.com/verify?t=abc"], "primaryLink": "https://example.com/verify?t=abc", "verdicts": { "spf": "PASS", "dkim": "PASS", "dmarc": "PASS", "spam": "PASS", "virus": "PASS" }, "isRead": false, "createdAt": "2026-06-11T12:01:31.000Z" } ``` Attachments (paid plans only) are capped at 10 MB per file and 25 MB per email; larger files are skipped. Download bytes via `GET /emails/:id/attachments/:index` (the array index). ### Extracted codes & links Every received email is scanned at delivery time and these fields are stored on it (available on all plans): - `extractedCodes` — array of candidate verification/OTP codes, **best guess first**, so `extractedCodes[0]` is almost always the code you want. Handles 4–8 digit numbers, space/hyphen-grouped codes (`123 456` → `123456`), and keyword-adjacent alphanumeric codes. Prefer this over regexing `bodyText`. - `extractedLinks` — every http(s) link in the email (anchor hrefs + bare URLs), de-duplicated, in document order. Returned only by `GET /emails/:id`. - `primaryLink` — best-guess primary call-to-action link (verify/confirm/reset/ magic-link), skipping unsubscribe/legal/social links. May be absent. These fields are also present in the `email.received` webhook payload. They are heuristics — fall back to the body if a value is missing or wrong. ### Custom regex matcher When `extractedCodes`/`primaryLink` don't fit (a custom token format, an order number, your own pattern), pass `match=` to `GET /emails/:id` or `GET /mailboxes/:id/wait` to extract it yourself: - The response gains `matches`: an array of strings — **capture group 1** when your pattern has one, else the whole match; all occurrences, capped at 20. - On `/wait`, `match` is also a filter: only an email whose subject/body matches is returned (so you can block until "the email that contains my token" arrives). - Matches run against the subject + text body (raw HTML if there's no text part). - Optional `match_flags` — any of `i`, `m`, `s`. Invalid regex → 400. - Example: `…/wait?match=order%20%23(\d+)&match_flags=i` → `{"matches":["10245"]}`. ### Authentication & spam verdicts `verdicts` carries what AWS SES reported for the message at receive-time, useful for testing your own sending setup (deliverability, DMARC alignment): - `spf`, `dkim`, `dmarc` — sender-authentication results. - `spam`, `virus` — SES content scan results. Each value is one of `PASS`, `FAIL`, `GRAY` (inconclusive), or `PROCESSING_FAILED`. Individual keys are present only when SES reported them, and the whole object is absent on emails received before this was added. Present on all plans, on `GET /emails/:id`, the list endpoint, and the `email.received` webhook. ### Webhooks (scope: crud_webhooks) - `GET /webhooks` — list endpoints (secrets never returned after creation). - `POST /webhooks` — body `{ "url": "https://...", "events": ["email.received"] }`. Response includes the signing `secret` (shown once). - `PUT /webhooks/:id` — update `url`, `events`, or `isActive`. - `DELETE /webhooks/:id` — remove the endpoint. Events: `email.received` (data = full email object), `email.deleted` (`{ emailId, mailboxId }`), `mailbox.created` (full mailbox object), `mailbox.deleted` (`{ mailboxId, address }`), `mailbox.expired` (`{ mailboxId, address }`). Delivery: HTTP POST of `{ "event": "...", "data": { ... } }`, 10s timeout, retried up to 3 times (2s, 4s backoff). Verify the `X-Devmailr-Signature` header: `sha256=` is the HMAC-SHA256 of the raw request body using the webhook secret. Use a constant-time comparison. ## Recommended agent workflow (email verification testing) 1. `POST /mailboxes` with a descriptive prefix, e.g. `{"prefix": "ci-run-1234"}`. Save `_id` and `address` from the response. 2. Trigger the system under test to send mail to `address`. 3. `GET /mailboxes/:id/wait?timeout=60` (optionally with `from`, `subject`, or `has_code=true`). It blocks until a matching email arrives and returns it — no polling. For a reused mailbox, capture a timestamp before step 2 and pass it as `received_after` so a stale email isn't matched. 4. Read the code straight from `extractedCodes[0]` (or `primaryLink` for magic links). Fall back to parsing `bodyText` only if those are empty. 5. Clean up with `DELETE /mailboxes/:id` so the mailbox doesn't count against the plan limit on subsequent runs. Complete example: ```bash BASE=https://api.devmailr.app/api AUTH="Authorization: Bearer $DEVMAILR_API_KEY" # 1. Create MAILBOX=$(curl -sf -X POST "$BASE/mailboxes" -H "$AUTH" \ -H "Content-Type: application/json" -d '{"prefix":"ci-run-1234"}') ID=$(echo "$MAILBOX" | jq -r ._id) ADDRESS=$(echo "$MAILBOX" | jq -r .address) # 2. ...trigger your app to send mail to $ADDRESS... # 3. Wait for the email (one blocking call, up to 60s) and read the code. # On timeout the endpoint returns 404, so `curl -f` exits non-zero → empty OTP. OTP=$(curl -sf --max-time 65 "$BASE/mailboxes/$ID/wait?has_code=true&timeout=60" \ -H "$AUTH" | jq -r '.extractedCodes[0] // empty') [ -z "$OTP" ] && echo "No code within 60s" && exit 1 # 4. Clean up curl -sf -X DELETE "$BASE/mailboxes/$ID" -H "$AUTH" > /dev/null ``` ## Rules for agents - Never hardcode API keys; read them from an environment variable or secret store (conventionally `DEVMAILR_API_KEY`). - Always delete mailboxes you created once a test run finishes — plans have mailbox limits and leftover mailboxes break subsequent runs with 403s. - Prefix-based addressing is deterministic: if you create `signup-test`, the address is `signup-test@devmailr.app`. Prefixes are globally unique; if taken, the API appends a random suffix, so always read `address` from the response instead of constructing it yourself. - Poll at 2–3 second intervals, not faster — the rate limit is shared per IP. - Use a `read`-scoped key when you only need to read mail; reserve `crud` keys for workflows that create/delete.