Skip to main content

Email Inboxes API

Create test inboxes, read the emails they receive, and wait for the next email from CI (Playwright, Cypress, plain scripts) without opening the dashboard. Every inbox has a public address at @hookinbox.com; your real email provider delivers to it. For a step-by-step guide, see Test emails in CI.

All routes use the usual authentication: an API key as Authorization: Bearer hklst_… or X-API-Key: hklst_…. Inboxes and emails that belong to another organization return 404.

RouteResult
GET /api/v1/inboxesList inboxes
POST /api/v1/inboxesCreate an inbox
GET /api/v1/inboxes/:idGet one inbox
PATCH /api/v1/inboxes/:idRename an inbox
DELETE /api/v1/inboxes/:idDelete an inbox and its emails
GET /api/v1/inboxes/:id/emailsList emails, newest first, with filters
GET /api/v1/inboxes/:id/emails/waitWait for the next matching email
GET /api/v1/inboxes/:id/emails/:email_idGet one email with bodies, headers, codes, links and attachments
GET /api/v1/inboxes/:id/emails/:email_id/attachments/:indexDownload an attachment
DELETE /api/v1/inboxes/:id/emailsDelete every email, keep the inbox
DELETE /api/v1/inboxes/:id/emails/:email_idDelete one email
POST /api/v1/inboxes/:id/messagesStore a message in the inbox over HTTP

Forwarding received email to webhooks has its own routes: see the Email Forwards API. Inboxes created without an account are read through the Anonymous Inboxes API.

Limits and retention​

  • Free: 1 inbox. Each inbox accepts up to 10 emails in any rolling 24 hours; mail over the limit is rejected until older emails leave the window or you clear the inbox.
  • Pro: 5 inboxes. Production and Scale: unlimited inboxes. All paid plans accept unlimited emails per inbox.
  • Retention: emails are kept as long as your plan keeps request history (Free 1 day, Pro 14 days, Production 30 days, Scale 90 days), then deleted automatically.
  • API keys, and therefore this API, are available on paid plans. On Free, use the MCP server or an inbox without an account.

The inbox object​

{
"id": "5c1d0e9a-3f7b-4f0e-9c1a-1b2e3f4a5b6c",
"name": "Signup tests",
"slug": "signup-tests-x1y2",
"email_address": "[email protected]",
"created_at": "2026-09-26T10:00:00Z",
"updated_at": "2026-09-26T10:00:00Z",
"email_count": 3
}

Mail sent to <slug>[email protected] lands in the same inbox. Give every test run its own plus-address and filter by it with to= so parallel runs never read each other's mail.

List inboxes​

GET /api/v1/inboxes
curl https://app.hooklistener.com/api/v1/inboxes \
-H "Authorization: Bearer hklst_your_api_key"

Returns {"data": [inbox, …]}.

Create an inbox​

POST /api/v1/inboxes
curl -X POST https://app.hooklistener.com/api/v1/inboxes \
-H "Authorization: Bearer hklst_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "Signup tests"}'
FieldTypeRequiredDescription
namestringYesInbox name
slugstringNoLocal part of the address: 3 to 100 lowercase letters, numbers and hyphens, unique across Hooklistener. Generated from the name if omitted

The body can also be wrapped as {"inbox": {"name": "…", "slug": "…"}}.

Returns 201 with {"data": inbox} and a Location header.

Errors:

StatusWhen
403Your plan has no email inboxes, or you reached the inbox limit. The body includes error and an upgrade object
422Invalid attributes, e.g. {"errors": {"name": ["can't be blank"]}}

Get an inbox​

GET /api/v1/inboxes/:id

Returns {"data": inbox}.

Rename an inbox​

PATCH /api/v1/inboxes/:id
curl -X PATCH https://app.hooklistener.com/api/v1/inboxes/INBOX_ID \
-H "Authorization: Bearer hklst_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "Signup tests (staging)"}'

Only name changes. The slug is the inbox's address and can't change: sending a different slug returns 400. Create a new inbox for a new address.

Returns {"data": inbox}.

Delete an inbox​

DELETE /api/v1/inboxes/:id

Deletes the inbox, every email in it and its webhook forwards. Mail to its address is no longer received, and the inbox stops counting toward your plan's inbox limit. Returns 204 No Content.

List emails​

GET /api/v1/inboxes/:id/emails
curl -G https://app.hooklistener.com/api/v1/inboxes/INBOX_ID/emails \
-H "Authorization: Bearer hklst_your_api_key" \
--data-urlencode "[email protected]"

Newest first, paginated like captured requests: page (default 1) and page_size (default 50, max 100).

Filters (optional, combined):

ParameterDescription
sinceISO 8601 timestamp with a UTC offset (emails received at or after that second), or an email ID from this inbox (emails after it)
toExact recipient address, case-insensitive. Use it with plus-addresses
fromCase-insensitive substring of the From header
subject_containsCase-insensitive substring of the subject

A malformed since, or an email ID from another inbox, returns 400.

Response:

{
"data": [
{
"id": "b72d4c1e-6a0f-4a51-9f3e-2d8c1b7e9a10",
"inbox_id": "5c1d0e9a-3f7b-4f0e-9c1a-1b2e3f4a5b6c",
"from": "Acme <[email protected]>",
"recipients": ["[email protected]"],
"subject": "Your verification code",
"message_id": "<[email protected]>",
"size_bytes": 5821,
"has_text_body": true,
"has_html_body": true,
"text_preview": "Your verification code is 482913. It expires in 10 minutes.",
"html_preview": "<p>Your verification code is <b>482913</b>…",
"created_at": "2026-09-26T10:02:11Z"
}
],
"pagination": { "page": 1, "page_size": 50, "total_count": 1, "total_pages": 1 }
}

Previews are capped at 500 bytes. Fetch the email for the full bodies.

Get an email​

GET /api/v1/inboxes/:id/emails/:email_id

Returns {"data": email}: the list fields (without the previews) plus:

FieldDescription
codeThe likely one-time code: the first entry of codes, or null
verification_linkThe link most likely to confirm the address, sign in with a magic link, reset a password or accept an invite; null when no link looks like one
codesUnique standalone runs of 4 to 8 digits, from the subject first and then the body
linksUnique http(s) URLs: href links of the HTML body first, then bare URLs in the text body, then bare URLs in the HTML body's text
textThe text body, or the HTML body rendered as plain text when there is no text body
text_body, html_bodyThe bodies as sent
headersParsed headers as an object with lowercase names, first value of each
header_listEvery header in order, repeated ones (Received, DKIM-Signature) included, as {"name", "value"} objects
attachmentsAttachments and inline images (see below)
{
"data": {
"id": "b72d4c1e-6a0f-4a51-9f3e-2d8c1b7e9a10",
"inbox_id": "5c1d0e9a-3f7b-4f0e-9c1a-1b2e3f4a5b6c",
"from": "Acme <[email protected]>",
"recipients": ["[email protected]"],
"subject": "Confirm your email",
"message_id": "<[email protected]>",
"size_bytes": 58210,
"has_text_body": true,
"has_html_body": true,
"created_at": "2026-09-26T10:02:11Z",
"text_body": "Your code is 482913.\n\nOr confirm here: https://acme.test/verify?token=8f2c1e9a7b6d5c4e3f2a",
"html_body": "<p>Your code is <b>482913</b>.</p><p><a href=\"https://acme.test/verify?token=8f2c1e9a7b6d5c4e3f2a\">Confirm your email</a></p>",
"text": "Your code is 482913.\n\nOr confirm here: https://acme.test/verify?token=8f2c1e9a7b6d5c4e3f2a",
"headers": { "from": "Acme <[email protected]>", "subject": "Confirm your email" },
"header_list": [
{ "name": "From", "value": "Acme <[email protected]>" },
{ "name": "Subject", "value": "Confirm your email" }
],
"codes": ["482913"],
"code": "482913",
"links": ["https://acme.test/verify?token=8f2c1e9a7b6d5c4e3f2a"],
"verification_link": "https://acme.test/verify?token=8f2c1e9a7b6d5c4e3f2a",
"attachments": [
{
"index": 0,
"filename": "welcome.pdf",
"content_type": "application/pdf",
"size_bytes": 48211,
"content_id": null,
"disposition": "attachment",
"resource_uri": "hooklistener://email/messages/b72d4c1e-6a0f-4a51-9f3e-2d8c1b7e9a10/attachments/0",
"download_url": "https://app.hooklistener.com/api/v1/inboxes/5c1d0e9a-3f7b-4f0e-9c1a-1b2e3f4a5b6c/emails/b72d4c1e-6a0f-4a51-9f3e-2d8c1b7e9a10/attachments/0"
}
]
}
}
  • codes: numbers that are part of dates, times, versions, amounts, phone numbers or URLs are skipped, so codes[0] is usually the one-time code. Codes split by spaces (482 913) are not detected.
  • verification_link: links are scored by their anchor text (or the text before a bare URL) and their URL. Words like verify, confirm, activate, magic, reset, invite and accept count most; sign in, log in, password and token count less. Unsubscribe, preferences, "view in browser", social profiles, app store badges, images and tracking pixels never count.

Both are heuristics. The full bodies are always included if you need your own pattern.

Attachments​

Each entry of attachments has:

FieldDescription
indexPosition in the list; use it to download the attachment
filenameFile name, or null
content_typeMIME type, e.g. application/pdf
size_bytesDecoded size
content_idContent-ID of an inline image, or null
dispositionattachment or inline
download_urlREST URL that returns the bytes (needs an API key)
resource_uriMCP resource for the same bytes

Wait for the next email​

GET /api/v1/inboxes/:id/emails/wait

Long-polls until an email matching the filters arrives, then returns it with 200 and the same body as Get an email. Accepts the same filters as List emails plus:

ParameterDefaultDescription
timeout30Seconds to wait, up to 60. 0 returns immediately
  • If since is given and a matching email already arrived after it, that email (the oldest match) is returned at once.
  • Without since, only emails that arrive during the call count. Record the time before you trigger the email and pass it as since so you can't miss a fast email.
  • On timeout the API returns 204 No Content with an empty body. Call it again to keep waiting.
curl -sf -G https://app.hooklistener.com/api/v1/inboxes/INBOX_ID/emails/wait \
-H "Authorization: Bearer hklst_your_api_key" \
--data-urlencode "[email protected]" \
--data-urlencode "since=2026-09-26T10:02:00Z" -d timeout=60 \
| jq -r '.data.code'

Download an attachment​

GET /api/v1/inboxes/:id/emails/:email_id/attachments/:index
curl -f -o welcome.pdf \
https://app.hooklistener.com/api/v1/inboxes/INBOX_ID/emails/EMAIL_ID/attachments/0 \
-H "Authorization: Bearer hklst_your_api_key"

Returns the attachment's bytes with its content type and Content-Disposition: attachment (the file's name, or attachment-<index> plus an extension). Attachments are never rendered inline. An unknown index returns 404. Attachments are kept as long as their email.

Delete emails​

DELETE /api/v1/inboxes/:id/emails

Deletes every email in the inbox and keeps the inbox and its address. On Free, the inbox's 10-email allowance frees up at once.

{ "data": { "inbox_id": "5c1d0e9a-3f7b-4f0e-9c1a-1b2e3f4a5b6c", "deleted_count": 12 } }
DELETE /api/v1/inboxes/:id/emails/:email_id

Deletes one email. Returns 204 No Content, or 404 if it's already gone.

Store a message​

POST /api/v1/inboxes/:id/messages

Stores a message in the inbox without sending real mail: for test harnesses, or to exercise webhook forwards. The message is handled like received mail: it counts toward the per-inbox limit, wakes up waits and is forwarded.

Send JSON fields:

curl -X POST https://app.hooklistener.com/api/v1/inboxes/INBOX_ID/messages \
-H "Authorization: Bearer hklst_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <[email protected]>",
"to": ["[email protected]"],
"subject": "Your verification code",
"text": "Your verification code is 482913.",
"html": "<p>Your verification code is <b>482913</b>.</p>",
"headers": {"X-Run": "42"}
}'
FieldTypeRequiredDescription
fromstringYesSender, e.g. Acme <[email protected]>
to, cc, bccstring or arrayNoRecipients, up to 100 in total. Bcc recipients are kept but not written into the headers. With no recipients, the message is addressed to the inbox
subjectstringNoSubject
text, htmlstringNoBodies
headersobjectNoExtra headers as strings. They can't replace From, To, Cc, Bcc, Subject or the MIME headers

Or send a raw MIME message, for example one with attachments, with Content-Type: message/rfc822; recipients come from its To, Cc and Bcc headers:

curl -X POST https://app.hooklistener.com/api/v1/inboxes/INBOX_ID/messages \
-H "Authorization: Bearer hklst_your_api_key" \
-H "Content-Type: message/rfc822" \
--data-binary @welcome.eml

Returns 201 with {"data": email} (the list fields) and a Location header pointing at the stored email. Needs the execute API key scope, which keys have by default.

Errors:

StatusWhen
413The message is over 10 MB
415The content type is neither application/json nor message/rfc822
422Invalid fields, e.g. a missing from
429The inbox reached its plan's email limit. The body includes error and an upgrade object

From an AI agent​

The same inboxes are available to Claude Code, Codex, Cursor and other agents through the MCP server (create_inbox, wait_for_email, get_email, delete_emails…). See Test emails with AI agents.