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.

curl
# 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

  1. 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.
  2. Point your signup or test at your inbox address. Addresses look like <key>@in.inboxpipe.net.
  3. Wait for the code. Call GET /api/v1/inboxes/<id>/messages/wait with Authorization: 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

ParamTypeMeaning
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

StatusBodyMeaning
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.
200 response
{
  "data": {
    "subject": "Your verification code",
    "from": "security@acme.test",
    "otp": "482193",
    "magic_link": null
  }
}

How long a wait blocks

ClientDefaultMaximumBehaviour
REST API (one call)20s25sReturns 204 when the timeout elapses. Your code calls again.
Python / TypeScript SDK60snoneCalls the API in a loop (up to 25s per call) until a match or the total timeout, then raises WaitTimeout.
MCP server (local)60s120sSame loop as the Python SDK.
MCP server (remote)60s55sEach tool call returns within 55s. Call the tool again if nothing has arrived.
n8n node60s300sSame 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
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.

Python SDK
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"
Node / TypeScript SDK
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"
curl
# 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 ***"
Python (requests)
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()
Node / JS (fetch)
// 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.

Python: install
pip install mailsocket
Python
from mailsocket import Client

client = Client("ms_live_...")
inbox = client.create_inbox()
otp = client.wait_for_otp(inbox["id"]).otp
Node / TypeScript: install
npm i mailsocket-sdk
Node / TypeScript
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.

AI agents (MCP)
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.

Remote MCP (no install)

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