Reference
On this page
Email API
Endpoints for email inboxes and the messages they capture. Authentication, roles, errors and paging are described in the API overview.
Inboxes
| Method and path | Role | Description |
|---|---|---|
GET /api/inboxes |
Member | Every inbox of the team, oldest first, with messageCount, unreadCount and SMTP credentials |
POST /api/inboxes |
Admin | Create an inbox (see below). 201 { inbox } |
GET /api/inboxes/:id |
Member | One inbox |
PATCH /api/inboxes/:id |
Admin | Change name, maxMessages or projectId (null ungroups it). maxMessages is capped at the plan's messages kept per inbox |
DELETE /api/inboxes/:id |
Admin | Delete the inbox and its messages. 204 |
POST /api/inboxes/:id/credentials |
Admin | Replace the SMTP password. The username stays. { inbox } |
POST /api/inboxes/:id/test-message |
Member | Store a sample HTML email (with an inline image). Counts toward the monthly message quota; at most 10 per inbox per minute (429). 201 { message: { id, subject, receivedAt } } |
GET /api/inboxes/:id/events |
Member | A server-sent events stream (see below) |
GET, PUT /api/inboxes/:id/spam-check |
Member / Admin | Whether new messages are scored automatically: { "autoSpamCheck": true }. See Spam reports |
GET, PUT /api/inboxes/:id/forwarding |
Member / Admin | Automatic forwarding (see Forwarding) |
Create an inbox
| Field | Notes |
|---|---|
name |
Required, 1 to 80 characters |
maxMessages |
Optional, 1 to 10,000 (default 500, capped at the plan's messages kept per inbox). The inbox keeps at most this many messages |
projectId |
Optional. A project of your team, or null |
expiresInMinutes |
Optional, 1 to 10,080 (7 days). Makes the inbox disposable: it is deleted with its messages after this time. See Disposable inboxes |
curl -X POST https://app.testlie.com/api/inboxes \
-H "Authorization: Bearer $TESTLIE_API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "ci-run-4812", "expiresInMinutes": 60 }'{
"inbox": {
"id": "cm1x9k2v80000abcd",
"name": "ci-run-4812",
"projectId": null,
"emailAlias": "k2m9v0q3x7",
"inboundAddress": "k2m9v0q3x7@in.testlie.com",
"maxMessages": 500,
"expiresAt": "2026-10-23T11:30:00.000Z",
"createdAt": "2026-10-23T10:30:00.000Z",
"messageCount": 0,
"unreadCount": 0,
"smtp": { "host": "smtp.testlie.com", "ports": [587, 2525, 465], "username": "…", "password": "…", "auth": ["PLAIN", "LOGIN"], "tls": true }
}
}Creating more inboxes than the plan allows answers 402 with "code": "quota_exceeded".
Events stream
GET /api/inboxes/:id/events keeps the connection open and sends text/event-stream events as JSON in data: lines:
type |
When |
|---|---|
message.created |
A message was stored (messageId) |
message.updated |
A message was marked read or unread |
message.deleted |
A message was deleted |
inbox.updated |
The inbox was emptied or every message was marked read |
resync |
The stream reconnected and may have missed events: reload the list |
A comment line (: ping) is sent every 25 seconds to keep proxies from closing the connection. For tests, the wait endpoint is simpler.
Messages
| Method and path | Role | Description |
|---|---|---|
GET /api/inboxes/:id/messages |
Member | List messages, newest first: q (searches subject, sender and recipients), unread=true, page, limit (default 50, max 200). { total, page, limit, messages } |
PATCH /api/inboxes/:id/messages |
Member | Mark every message read. { updated } |
DELETE /api/inboxes/:id/messages |
Member | Empty the inbox. { deleted } |
GET /api/inboxes/:id/messages/wait |
Member | Long-poll for a message (see below) |
GET /api/messages/:id |
Member | The full message (see below) |
PATCH /api/messages/:id |
Member | { "isRead": true | false }. { ok: true } |
DELETE /api/messages/:id |
Member | Delete one message. 204 |
GET /api/messages/:id/raw |
Member | The original MIME as text/plain; ?download=1 downloads it as <id>.eml |
GET /api/messages/:id/html |
Member | The HTML body rendered for the preview: sandboxed, scripts blocked, cid: images inlined. ?dark=respect|invert renders it as dark-mode clients do |
GET /api/messages/:id/preview-url |
Member | { url }: a signed URL for the preview that works without a session, valid for 15 minutes. Carries ?dark= |
GET /api/messages/:id/attachments/:attachmentId |
Member | Download an attachment. Always application/octet-stream, never rendered |
GET /api/messages/:id/extract |
Member | Links, one-time codes and magic links (see below) |
GET /api/messages/:id/spam |
Member | Spam score, inbox placement and the Gmail/Yahoo checklist. See Spam reports |
GET /api/messages/:id/html-check |
Member | Email-client compatibility of the HTML. { report, cached } |
GET /api/messages/:id/accessibility |
Member | Accessibility report. { report, cached }. See Email quality checks |
GET /api/messages/:id/links |
Member | Link check, run in the background. Team plan and above. See Email quality checks |
GET /api/messages/:id/auth |
Member | SPF, DKIM, DMARC, ARC and BIMI results. Team plan and above. See Email quality checks |
POST /api/messages/:id/forward |
Member | Forward to verified addresses: { "to": ["dev@example.com"] }, up to 10. See Forwarding |
Summaries in lists and in wait have id, inboxId, subject, from ({ address, name }), to and cc (arrays of { address, name }), envelopeTo (the SMTP recipients), size, isRead and receivedAt. The full message adds messageId (the Message-ID header), bcc (envelope recipients not in To or Cc), envelope ({ from, to }), html, text, headers and attachments (id, filename, contentType, size, contentId, inline).
Wait for a message
GET /api/inboxes/:id/messages/wait answers as soon as a matching message is stored:
| Parameter | Notes |
|---|---|
to |
An envelope recipient, matched exactly (case-insensitive) |
subject |
Text the subject contains (case-insensitive) |
since |
ISO time; only messages received at or after it match |
timeout |
1 to 120 seconds, default 30 |
curl -f -H "Authorization: Bearer $TESTLIE_API_KEY" \
"https://app.testlie.com/api/inboxes/$INBOX_ID/messages/wait?to=new-user@example.com&subject=Welcome&since=2026-10-23T10:30:00Z"{
"message": {
"id": "k2m9v0q3…",
"inboxId": "cm1x9k2v80000abcd",
"subject": "Welcome aboard",
"from": { "address": "app@example.com", "name": "Example App" },
"to": [{ "address": "new-user@example.com", "name": "" }],
"cc": [],
"envelopeTo": ["new-user@example.com"],
"size": 18234,
"isRead": false,
"receivedAt": "2026-10-23T10:30:04.512Z"
}
}With several matches it returns the newest. On timeout it answers 408 { "error": "No matching message before the timeout" }. Testing in CI shows it in Playwright, pytest and GitHub Actions.
Extract links and codes
GET /api/messages/:id/extract finds what a test usually wants from an email, best match first:
{
"messageId": "k2m9v0q3…",
"subject": "Your sign-in code",
"codes": [{ "value": "482913", "context": "Your sign-in code is 482913", "score": 12 }],
"magicLinks": [{ "url": "https://app.example.com/verify?token=…", "text": "Verify email", "reason": "text and URL mention verify", "score": 9 }],
"links": [{ "url": "https://app.example.com/verify?token=…", "text": "Verify email" }]
}links has every link in the HTML and text parts. Known tracking redirects are unwrapped; the link then also has originalUrl. magicLinks are the links that look like verify, reset, sign-in or invite links. Read-only keys may call it. The SDKs wrap it.
Forwarding
Forwarding sends captured mail to real addresses. Every address must first be confirmed by its owner, and the plan must include forwarding (Team and above). See Forwarding.
| Method and path | Role | Description |
|---|---|---|
GET /api/forward-addresses |
Member | { addresses: [{ id, email, verifiedAt, createdAt }], verificationRequired } |
POST /api/forward-addresses |
Admin | { "email" }: adds the address and emails it a confirmation link (201 { address, sent }). For an unverified address it resends the link and answers 200, at most once a minute (429). A verified address answers 200 with sent: false. At most 50 addresses per team |
DELETE /api/forward-addresses/:id |
Admin | Remove an address, also from automatic forwarding. 204 |
GET /api/forwarding-status |
Member | { outboundEnabled, maxRecipients, mode, verificationRequired } |
GET /api/inboxes/:id/forwarding |
Member | { forwarding: { enabled, to, match }, outboundEnabled } |
PUT /api/inboxes/:id/forwarding |
Admin | { "enabled", "to": [...], "match"? }. Up to 10 verified recipients. With match, only messages with an envelope recipient that contains this text are forwarded. An unverified recipient answers 400 with "code": "unverified_recipient" |
POST /api/messages/:id/forward |
Member | Forward one message now: { "to": [...] }. { forwarded, to, accepted, rejected }. An unverified recipient answers 400 with "code": "unverified_recipient", like the forwarding PUT; if the outbound server refuses the message, 502 |
Forwarded recipients count toward the plan's daily limit (402 when it is used up).
Inbound email to your app
Simulate email arriving at your app as SendGrid, Mailgun or Postmark would post it. Details and payloads are in Inbound SMS & email to your app.
| Method and path | Role | Description |
|---|---|---|
GET /api/inboxes/:id/inbound |
Member | The app endpoint and format: { target }. The signing secret is shown to admins only |
PUT /api/inboxes/:id/inbound |
Admin | { "format"?: "sendgrid" | "mailgun" | "postmark" | "raw", "url"?, "secret"? }. Mailgun needs a secret (its signing key) |
POST /api/inboxes/:id/inbound/messages |
Member | Send an email to your app: compose one (from, to, subject, text, html, cc, replyTo, headers, inReplyTo, up to 20 base64 attachments), replay a captured one ({ replayMessageId, to? }), send raw MIME ({ raw, to? }, base64), or post a message/rfc822 body with ?to=. 201 { message } with its deliveries. At most 60 per team per minute |
GET /api/inboxes/:id/inbound/messages |
Member | Emails sent to the app, newest first, with deliveries: page, limit (default 50, max 500) |
GET, DELETE /api/inbound/:id |
Member | One inbound message with every delivery attempt; ?raw=1 downloads an email as .eml |
POST /api/inbound/:id/redeliver |
Member | Deliver again: every delivery, or { "deliveryId" }. 409 while an earlier delivery is still running |
A simulated email counts toward the monthly message quota (402 when it is used up).