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.

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
GET /api/v1/inboxes/:id/emailsList emails, newest first, with filters
GET /api/v1/inboxes/:id/emails/:email_idGet one email with bodies, headers, links and codes
GET /api/v1/inboxes/:id/emails/waitWait for the next matching email

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.
  • 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 webhook 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.

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; must be globally unique. 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}.

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-…",
"inbox_id": "5c1d0e9a-…",
"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 plus text_body, html_body, headers (parsed headers), links and codes.

  • links: unique http(s) URLs. Links from the HTML body's href attributes come first, then bare URLs in the text body. Use it to open a magic link or check that a link isn't broken.
  • codes: unique standalone runs of 4 to 8 digits, from the subject first and then the body. Numbers that are part of dates, times, versions, amounts or URLs are skipped, so codes[0] is usually the one-time code. Codes split by spaces (482 913) are not detected.

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

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.

Example: verify a signup email in CI​

curl​

Create the inbox once (by hand, in a setup job, or with an agent) and store its ID and address as CI secrets. Creating one per run would quickly hit your plan's inbox limit.

# One-time setup
curl -sf -X POST https://app.hooklistener.com/api/v1/inboxes \
-H "Authorization: Bearer $HOOKLISTENER_API_KEY" \
-H "Content-Type: application/json" -d '{"name": "ci-signup"}' \
| jq '.data | {id, email_address}'
# Save them as HOOKLISTENER_INBOX_ID and HOOKLISTENER_INBOX_ADDRESS

Each run then uses its own plus-address and a start time:

API=https://app.hooklistener.com/api/v1
AUTH="Authorization: Bearer $HOOKLISTENER_API_KEY"
ADDRESS=$(echo "$HOOKLISTENER_INBOX_ADDRESS" | sed "s/@/+run-$GITHUB_RUN_ID@/")
SINCE=$(date -u +%Y-%m-%dT%H:%M:%SZ)

./scripts/sign-up.sh "$ADDRESS"

CODE=$(curl -sf -G "$API/inboxes/$HOOKLISTENER_INBOX_ID/emails/wait" -H "$AUTH" \
--data-urlencode "to=$ADDRESS" --data-urlencode "since=$SINCE" \
-d timeout=60 | jq -r '.data.codes[0] // empty')
test -n "$CODE" || { echo "no verification email"; exit 1; }

curl -f treats 204 as success, so check that the output isn't empty.

Playwright​

import { test, expect } from "@playwright/test";

const API = "https://app.hooklistener.com/api/v1";
const headers = { authorization: `Bearer ${process.env.HOOKLISTENER_API_KEY}` };
const INBOX_ID = process.env.HOOKLISTENER_INBOX_ID!;
const INBOX_ADDRESS = process.env.HOOKLISTENER_INBOX_ADDRESS!;

async function waitForEmail(params: Record<string, string>) {
const query = new URLSearchParams({ timeout: "60", ...params });
for (let attempt = 0; attempt < 2; attempt++) {
const res = await fetch(`${API}/inboxes/${INBOX_ID}/emails/wait?${query}`, { headers });
if (res.status === 200) return (await res.json()).data;
if (res.status !== 204) throw new Error(`wait failed: ${res.status}`);
}
throw new Error("no email arrived");
}

test("signup emails a working code", async ({ page }) => {
// Two 60-second waits plus the signup; Playwright's default is 30 seconds.
test.setTimeout(150_000);
const address = INBOX_ADDRESS.replace("@", `+${Date.now()}@`);
const since = new Date().toISOString();

await page.goto("http://localhost:3000/signup");
await page.getByLabel("Email").fill(address);
await page.getByRole("button", { name: "Create account" }).click();

const email = await waitForEmail({ to: address, since });
expect(email.subject).toContain("verification code");

await page.getByLabel("Verification code").fill(email.codes[0]);
await expect(page.getByText("Verified")).toBeVisible();
});

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). See the guide Test signup, magic-link and OTP emails with AI agents.