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
Omit ttlSeconds to keep an inbox forever; set it for temporary CI inboxes.
Mint a key bound to one inbox. It can read that inbox via HTTP / SSE / IMAP but nothing else on the account.
Link any webhook to any number of inboxes. Each arriving email fires every hook attached to its inbox.
x-api-key.Create Inbox
POST undefined/api/v1/inboxesRequest Body
| Field | Type | Description |
|---|---|---|
prefix | string? | 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. |
domainId | uuid? | Restrict the inbox to one verified domain. Defaults to your tenant's default domain. |
environmentId | uuid? | Environment scope. Defaults to production. |
tags | string[]? | Arbitrary tags for filtering in the list endpoint. |
ttlSeconds | number? | If set (60–86400), the inbox auto-deletes after this many seconds. Omit for a permanent inbox. |
Example
curl -X POST undefined/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 undefined/api/v1/inboxesSupports environmentId, domainId, tags, includeExpired, plus page & perPage. When called with an inbox-scoped key, returns only that inbox.
Get / Delete Inbox
undefined/api/v1/inboxes/:inboxIdReturns the inbox metadata and stats.
undefined/api/v1/inboxes/:inboxIdPermanently deletes the inbox and all captured messages. Requires a tenant-level key.
Messages
GET undefined/api/v1/inboxes/:inboxId/messagesGET undefined/api/v1/inboxes/:inboxId/messages/:messageIdEach message includes the parsed body and an extracted object with common auth artefacts (magic links, OTPs, verification codes).
{
"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 undefined/api/v1/inboxes/:inboxId/messages/waitLong-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
| Param | Type | Description |
|---|---|---|
timeout | number | Max wait in ms (default 30000, max 60000). |
subject | string | Case-insensitive substring match on subject. |
from | string | Case-insensitive substring match on sender. |
afterId | uuid | Only return messages received after this message ID. |
curl "undefined/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 undefined/api/v1/inboxes/:inboxId/api-keysGET undefined/api/v1/inboxes/:inboxId/api-keysDELETE undefined/api/v1/inboxes/:inboxId/api-keys/:keyIdkey. Store it somewhere safe — it cannot be retrieved again.curl -X POST undefined/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/:idand/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.messageevents 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 undefined/api/v1/inboxes/:inboxId/hooksPOST undefined/api/v1/inboxes/:inboxId/hooksDELETE undefined/api/v1/inboxes/:inboxId/hooks/:webhookIdPOST 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 undefined/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 "undefined/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