Inboxes API

Virtual inboxes are scoped email addresses you can create from code — for E2E tests, CI runs, per-customer capture, or anywhere you need a mailbox that's disposable, long-lived, and programmatically accessible over HTTP, SSE, and IMAP.

Overview

Permanent by default

Omit ttlSeconds to keep an inbox forever; set it for temporary CI inboxes.

Scoped API keys

Mint a key bound to one inbox. It can read that inbox via HTTP / SSE / IMAP but nothing else on the account.

Attach hooks

Link any webhook to any number of inboxes. Each arriving email fires every hook attached to its inbox.

Create Inbox

POST https://mailhooks.dev/api/v1/inboxes

Request Body

FieldTypeDescription
prefixstring?Local part of the address. Used as-is when provided (e.g. "support" gives [email protected]). Auto-generated when omitted. A short suffix is appended only if the prefix is already taken.
domainIduuid?Restrict the inbox to one verified domain. Defaults to your tenant's default domain.
environmentIduuid?Environment scope. Defaults to production.
tagsstring[]?Arbitrary tags for filtering in the list endpoint.
ttlSecondsnumber?If set (60–86400), the inbox auto-deletes after this many seconds. Omit for a permanent inbox.

Example

curl -X POST https://mailhooks.dev/api/v1/inboxes \
  -H "x-api-key: mh_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "prefix": "ci-run",
    "tags": ["ci", "auth"]
  }'

Response

{
  "id": "5983de4e-af3a-482d-bce0-608b144d47d5",
  "address": "[email protected]",
  "addressPrefix": "ci-run",
  "tags": ["ci", "auth"],
  "expiresAt": null,
  "active": true,
  "messageCount": 0,
  "createdAt": "2026-04-19T18:00:00.000Z",
  "domain": { "id": "...", "domain": "yourdomain.com" },
  "environment": { "id": "...", "name": "Production", "slug": "production" }
}

List Inboxes

GET https://mailhooks.dev/api/v1/inboxes

Supports environmentId, domainId, tags, includeExpired, plus page & perPage. When called with an inbox-scoped key, returns only that inbox.

Get / Delete Inbox

GET
https://mailhooks.dev/api/v1/inboxes/:inboxId

Returns the inbox metadata and stats.

DELETE
https://mailhooks.dev/api/v1/inboxes/:inboxId

Permanently deletes the inbox and all captured messages. Requires a tenant-level key.

Messages

GET https://mailhooks.dev/api/v1/inboxes/:inboxId/messages
GET https://mailhooks.dev/api/v1/inboxes/:inboxId/messages/:messageId

Each message includes the parsed body and an extracted object with common auth artefacts (magic links, OTPs, verification codes).

Messages follow your plan's retention window (24 hours on Free, 365 days on Pro) and expire together with the email they captured, so the Inboxes and Emails views always cover the same period.

{
  "id": "62641ed7-1f7a-4daa-bd86-144da461587f",
  "inboxId": "5983de4e-af3a-482d-bce0-608b144d47d5",
  "from": "[email protected]",
  "to": ["[email protected]"],
  "subject": "Sign in to YourApp",
  "textBody": "Your code is 135791",
  "extracted": {
    "otp": "135791",
    "verificationCode": "135791",
    "links": [],
    "codes": ["135791"]
  },
  "createdAt": "2026-04-19T18:02:21.000Z"
}

Wait for a Message

GET https://mailhooks.dev/api/v1/inboxes/:inboxId/messages/wait

Long-poll for a matching message. Great for E2E tests: send a sign-up email, wait for it to arrive, pull the OTP or magic link, continue.

Query Params

ParamTypeDescription
timeoutnumberMax wait in ms (default 30000, max 60000).
subjectstringCase-insensitive substring match on subject.
fromstringCase-insensitive substring match on sender.
afterIduuidOnly return messages received after this message ID.
curl "https://mailhooks.dev/api/v1/inboxes/${INBOX_ID}/messages/wait?subject=Sign%20in&timeout=30000" \
  -H "x-api-key: mh_your_api_key"

Inbox-scoped API Keys

Generate a key that can only access one inbox. Handy for shipping a read-only credential to CI, a customer-facing integration, or a per-user test runner — without granting tenant-wide access.

POST https://mailhooks.dev/api/v1/inboxes/:inboxId/api-keys
GET https://mailhooks.dev/api/v1/inboxes/:inboxId/api-keys
DELETE https://mailhooks.dev/api/v1/inboxes/:inboxId/api-keys/:keyId
curl -X POST https://mailhooks.dev/api/v1/inboxes/${INBOX_ID}/api-keys \
  -H "x-api-key: mh_tenant_key" \
  -H "Content-Type: application/json" \
  -d '{"name": "CI runner", "scopes": ["api", "imap"]}'

Scope enforcement

When an inbox-scoped key is used:

  • Reads to /inboxes/:id and /inboxes/:id/messages* succeed for the bound inbox only.
  • Any cross-inbox request returns 403.
  • Management routes (create / delete inboxes, manage keys, manage hooks) return 403.
  • SSE streams are filtered to inbox.message events for the bound inbox.
  • IMAP LOGIN accepts the key as the password for the inbox's address; other mailboxes are denied.

Attach Hooks

Hooks are fire-and-forget delivery targets (see the Webhooks API). Attach any hook to any number of inboxes; every arriving email fires every attached hook.

GET https://mailhooks.dev/api/v1/inboxes/:inboxId/hooks
POST https://mailhooks.dev/api/v1/inboxes/:inboxId/hooks
DELETE https://mailhooks.dev/api/v1/inboxes/:inboxId/hooks/:webhookId

POST accepts either { "webhookId": "..." } to link an existing hook, or { "url": "https://..." } to create and link in one step. The same link is idempotent.

curl -X POST https://mailhooks.dev/api/v1/inboxes/${INBOX_ID}/hooks \
  -H "x-api-key: mh_tenant_key" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://my-ci.com/email-events"}'

Realtime (SSE)

Open an SSE stream with an inbox-scoped key and you'll receive only inbox.message events for that inbox (plus heartbeat / connected frames).

curl -N "https://mailhooks.dev/api/v1/realtime/events" \
  -H "x-api-key: mh_inbox_scoped_key"

IMAP Access

An inbox-scoped key with the imap scope can be used as the password for an IMAP LOGIN against your IMAP host, with the username set to the inbox's full address. Wildcards and other mailboxes are rejected.

Host:     imap.mailhooks.dev
Port:     993 (TLS)
Username: [email protected]
Password: mh_inbox_scoped_key