Quickstart
Wait for the OTP. One API call.
Point your signup form at a mailsocket inbox, then block on the wait endpoint until the OTP or magic link arrives. No Gmail, no IMAP.
60-second path (no signup)
One unauthenticated call creates an API key and an inbox. Then send a code to the inbox address and wait for it.
# 1. create a key + inbox
curl -X POST \
"https://dash.mailsocket.app/api/v1/agents/register" \
-H "Content-Type: application/json" \
-d '{"name": "my-test"}'
# returns data.api_key (ms_live_...), data.inbox.id
# and data.inbox.address (...@in.inboxpipe.net)
# 2. email a code to that address, then wait
# (200 = code, 204 = call again)
curl "https://dash.mailsocket.app/api/v1/inboxes/inbox_xxx/messages/wait?require=otp&timeout=25" \
-H "Authorization: Bearer ***"
- The key is shown once and is on the Free plan: 1 inbox, 1-day message retention, 2 concurrent waits.
- Registration is limited to 10 keys per hour per IP address.
- The account has no dashboard login. Sign up for a key you manage in the dashboard, with more inboxes on paid plans.
With an account
- Create an inbox and an API key. Sign up, verify your email, and open the dashboard. Your first inbox is created automatically, and you can create an API key in one click.
-
Point your signup or test at your inbox address.
Addresses look like
<key>@in.inboxpipe.net. -
Wait for the code.
Call
GET /api/v1/inboxes/<id>/messages/waitwithAuthorization: Bearer <key>. The call blocks until the OTP or magic link arrives, then returns it.
The wait endpoint
GET /api/v1/inboxes/{id}/messages/wait blocks until a matching parsed
message arrives, or returns 204 when the timeout elapses.
Query parameters
| Param | Type | Meaning |
|---|---|---|
timeout |
seconds (float) | How long to block. Default 20. Clamped to [1, 25] server-side. |
since |
ISO-8601 | public_id | unix ts |
Only messages received strictly after this point match. Default: the request start time, so a code that arrives during the call is caught and an older one is not. Pass since=0 to match messages already in the inbox (the oldest match is returned first), or a message id to continue after it. |
require |
otp | link | any |
What counts as a match. Default otp. link matches a message with a magic link; any matches either. |
min_confidence |
float 0..1 |
Only match an OTP whose otp_confidence is at least this. Default 0.0. Ignored for link. |
Responses
| Status | Body | Meaning |
|---|---|---|
200 |
{"data": {...otp, magic_link, subject, from...}} |
A matching parsed message arrived (the earliest one after since). |
204 |
empty | The timeout elapsed with no match. Call again. |
404 |
error body | The inbox does not exist or is not yours. |
429 |
error body | too_many_wait_requests (your plan's concurrent waits: 2 Free / 3 Pro / 8 Scale / 16 Business) or wait_capacity (service-wide limit). Retry after the Retry-After header. |
{
"data": {
"subject": "Your verification code",
"from": "security@acme.test",
"otp": "482193",
"magic_link": null
}
}
How long a wait blocks
| Client | Default | Maximum | Behaviour |
|---|---|---|---|
| REST API (one call) | 20s | 25s | Returns 204 when the timeout elapses. Your code calls again. |
| Python / TypeScript SDK | 60s | none | Calls the API in a loop (up to 25s per call) until a match or the total timeout, then raises WaitTimeout. |
| MCP server (local) | 60s | 120s | Same loop as the Python SDK. |
| MCP server (remote) | 60s | 55s | Each tool call returns within 55s. Call the tool again if nothing has arrived. |
| n8n node | 60s | 300s | Same loop; on timeout it errors or returns an empty item. |
Mark a message read
PATCH /api/v1/messages/{id} with {"read": true} marks a message read;
{"read": false} marks it unread. The body accepts only read.
curl -X PATCH "https://dash.mailsocket.app/api/v1/messages/msg_xxx" \
-H "Authorization: Bearer ms_live_..." \
-H "Content-Type: application/json" \
-d '{"read": true}'
# → 200 {"data": {"id": "msg_xxx", "read": true, ...}}
Paid plans can also mark or delete up to 100 messages in one call with POST /api/v1/messages/batch. See the API reference.
Code samples
Each sample submits a signup form, then waits for the OTP. Replace *** with your API key and inbox_xxx with your inbox id. The SDKs repeat the wait call for you; the raw HTTP samples show that loop.
import requests
from mailsocket import Client
client = Client("***")
requests.post("https://app.example.com/signup", json={"email": "you@in.inboxpipe.net"})
result = client.wait_for_otp("inbox_xxx", timeout=60) # raises WaitTimeout after 60s
print(result.otp) # → "482193"
import { MailsocketClient } from "mailsocket-sdk";
const client = new MailsocketClient("***");
await fetch("https://app.example.com/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "you@in.inboxpipe.net" }),
});
const { otp } = await client.waitForOtp("inbox_xxx", { timeout: 60_000 });
console.log(otp); // → "482193"
# 1. submit your signup form with a mailsocket address
curl -X POST https://app.example.com/signup \
-H "Content-Type: application/json" \
-d '{"email":"you@in.inboxpipe.net"}'
# 2. wait up to 25s; repeat while the status is 204
curl "https://dash.mailsocket.app/api/v1/inboxes/inbox_xxx/messages/wait?require=otp&timeout=25" \
-H "Authorization: Bearer ***"
import time
import requests
requests.post("https://app.example.com/signup", json={"email": "you@in.inboxpipe.net"})
deadline = time.monotonic() + 60
otp = None
while otp is None and time.monotonic() < deadline:
r = requests.get(
"https://dash.mailsocket.app/api/v1/inboxes/inbox_xxx/messages/wait",
params={"require": "otp", "timeout": 25},
headers={"Authorization": "Bearer ***"},
timeout=35,
)
if r.status_code == 200:
otp = r.json()["data"]["otp"] # → "482193"
elif r.status_code != 204: # 204 = no code yet, loop again
r.raise_for_status()
// 1. submit your signup form with a mailsocket address
await fetch("https://app.example.com/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "you@in.inboxpipe.net" }),
});
// 2. wait up to 25s per call; 204 means no code yet, so call again
const url = new URL("https://dash.mailsocket.app/api/v1/inboxes/inbox_xxx/messages/wait");
url.searchParams.set("require", "otp");
url.searchParams.set("timeout", "25");
const deadline = Date.now() + 60_000;
let otp = null;
while (otp === null && Date.now() < deadline) {
const res = await fetch(url, {
headers: { Authorization: "Bearer ***" },
});
if (res.status === 200) otp = (await res.json()).data.otp; // → "482193"
else if (res.status !== 204) throw new Error(`wait failed: ${res.status}`);
}
Run it in your E2E suite
A ready-to-copy Playwright example fills a signup form with a
mailsocket address and waits for the OTP:
examples/playwright/otp-signup.spec.ts. It reads
MAILSOCKET_API_KEY, MAILSOCKET_INBOX_ID and MAILSOCKET_INBOX_ADDRESS from the environment.
More in Guides.
SDKs & MCP server
Official clients for the API above, with wait_for_otp built in.
pip install mailsocket
from mailsocket import Client
client = Client("ms_live_...")
inbox = client.create_inbox()
otp = client.wait_for_otp(inbox["id"]).otp
npm i mailsocket-sdk
import { MailsocketClient } from "mailsocket-sdk";
const client = new MailsocketClient("ms_live_...");
const inbox = await client.createInbox();
const { otp } = await client.waitForOtp(inbox.id);
The SDK wait methods default to since=0, so they also return a matching code already in the inbox. On an inbox you reuse across runs, pass the id of the last message you handled (or a timestamp from before the trigger) as since.
uvx mailsocket-mcp
{
"mcpServers": {
"mailsocket": {
"command": "uvx",
"args": ["mailsocket-mcp"],
"env": { "MAILSOCKET_API_KEY": "ms_live_..." }
}
}
}
Requires Python 3.10+. Tools: create_inbox, wait_for_otp, wait_for_link, list_inboxes, list_messages, get_latest, delete_inbox. Listed on the Official MCP Registry as app.mailsocket/mailsocket-mcp.
The same tools are hosted at https://mcp.mailsocket.app/mcp over Streamable HTTP. Send your key as a header on every request. A key in the URL (?api_key=) is rejected.
Claude Code CLI:
claude mcp add --transport http mailsocket https://mcp.mailsocket.app/mcp \
--header "Authorization: Bearer ***"
Cursor (~/.cursor/mcp.json):
{
"mcpServers": {
"mailsocket": {
"url": "https://mcp.mailsocket.app/mcp",
"headers": { "Authorization": "Bearer ms_live_..." }
}
}
}
VS Code (.vscode/mcp.json):
{
"servers": {
"mailsocket": {
"type": "http",
"url": "https://mcp.mailsocket.app/mcp",
"headers": { "Authorization": "Bearer ms_live_..." }
}
}
}
The remote server is stateless, with per-key limits of 3 waits and 8 other calls in flight. See how long a wait blocks.
Claude.ai, Claude Desktop connectors and Smithery need OAuth, which is planned but not available yet. Use the local stdio server in those clients.
Why addresses use inboxpipe.net
Inbox addresses live on in.inboxpipe.net, not on mailsocket.app. Keeping inbound test mail on its own domain isolates its reputation from the domain we use for our website, account email and support, so a problem with one never takes down the other.
Some signup forms block domains they do not recognise, including new ones. If a form you test rejects the address, use a custom domain you own: on the Business plan you can receive mail at your own domain (and use catch-all). See Pricing.
Public inboxes
Inboxes are private. An inbox created with "public": true can also be read without a key at GET /api/v1/public/emails/{address}/messages. This is off by default and only possible on in.inboxpipe.net addresses.
The public response includes otp, magic_link, from, to, cc, subject and received_at for the 20 most recent messages. It never includes the message body, links, OTP context or metadata. Anyone who knows the address can read the codes, so use public inboxes only for demos and shared fixtures, never for real user signups.
More references
- Try it live: get a demo inbox in one click and watch a real OTP arrive. No signup.
- API reference: every v1 endpoint, parameter and status code, plus pagination and rate limits.
- Error reference: every error code the API returns.
- Guides: pytest, Cypress, Puppeteer and GitHub Actions.
- Versioning, FAQ and changelog.