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
bash
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 }'
json
{
  "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
bash
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"
json
{
  "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.

GET /api/messages/:id/extract finds what a test usually wants from an email, best match first:

json
{
  "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).