MailOtp
API v1

REST API reference.

Base URL, authentication, the task lifecycle, and every endpoint with examples. Machine-readable spec at /v1/openapi.json.

https://api.mailotp.org OpenAPI 3.1

Authentication

Create a key in Dashboard → API keys. Send it as a Bearer token, or as X-API-Key if that fits your client better — both are accepted and behave identically. Keys start with mo_live_, are 40 characters long and are shown once; press Show on the keys page to see one again.

Authorization: Bearer mo_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
# or
X-API-Key: mo_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

A key carries read and tasks:write. Treat it like a password: anyone holding it can spend your balance, so keep it server-side. Revoking takes effect within 30 seconds. The full machine-readable contract is at https://api.mailotp.org/v1/openapi.json.

Task lifecycle

A task rents one mailbox for one service. The service price is held immediately. When a matching mail lands (inbox or junk) the code is extracted and the hold is captured. If the window passes with no code, or you cancel after cancellable_at, the hold is refunded.

Only mail that arrives after the task is created counts. Create the task (or call /again) first, then have the site send the code. Mail that was already in the mailbox — even from a few seconds earlier, or the same second — is ignored, so an older code is never handed to a new task.

How long is the window? Every service sets its own, returned as ttl_sec by GET /v1/services — today that is 5 minutes for every service. Send ttl on POST /v1/tasks to ask for a different one; values are clamped to 60–900 seconds. The task times out the moment the window closes and the hold is released automatically.

waitingreceivedcancelledtimeout · refundedfailed · refunded

Create a task

POST /v1/tasks with the service slug. Optional domain (e.g. hotmail.com) and an Idempotency-Key header so retries never double-charge.

POST /v1/tasks201 Created
$ curl -X POST https://api.mailotp.org/v1/tasks \
  -H "Authorization: Bearer mo_live_…" \
  -H "Idempotency-Key: order-8812" \
  -H "Content-Type: application/json" \
  -d '{"service":"instagram","domain":"hotmail.com"}'

{
  "id": "c1d5cac4-c7e0-4a67-9030-07d6b921f54d",
  "status": "waiting",
  "service": "instagram",
  "email": "quiet.harbor42@hotmail.com",
  "price": "0.00075",
  "created_at": "2026-09-12T10:35:45Z",
  "expires_at": "2026-09-12T10:40:45Z",
  "cancellable_at": "2026-09-12T10:38:45Z",
  "result": null
}

Long-poll & SSE

Don't hammer the API. Ask once with ?wait=30: the request blocks up to 30 seconds and returns the instant the status changes. Or open /stream for Server-Sent Events.

Empty fields are omitted, not null. code and link appear only when that mail actually carried one — a code-only mail has no link key at all, so read them with a presence check. result itself is null until a mail lands.

GET /v1/tasks/{id}?wait=30200 OK
$ curl "https://api.mailotp.org/v1/tasks/c1d5cac4-…?wait=30" -H "Authorization: Bearer mo_live_…"

{
  "status": "received",
  "result": {
    "code": "483920",
    "from": "security@mail.instagram.com",
    "subject": "483920 is your Instagram code",
    "received_at": "2026-09-12T10:35:48Z",
    "confidence": "high"
  }
}

# SSE: one "status" event per change, ": ping" every 15s
$ curl -N "https://api.mailotp.org/v1/tasks/c1d5cac4-…/stream" -H "Authorization: Bearer mo_live_…"

All endpoints

MethodPathPurpose
GET/v1/meBalance, discount and the key's scopes
GET/v1/servicesCatalog with price, window and code format
GET/v1/domainsDomains with ready mailbox counts. Add ?service=<slug> to count only what that service can still use
POST/v1/tasksCreate a task (rent a mailbox, hold the price)
GET/v1/tasksList your tasks · ?status=&limit=
GET/v1/tasks/{id}Task status · ?wait=0..30 long-poll
GET/v1/tasks/{id}/streamServer-Sent Events stream
GET/v1/tasks/{id}/messageDelivered message: code, link, subject, sender, text body
POST/v1/tasks/{id}/againAnother code on the same address: new task pinned to that mailbox (charged again). Request the code after calling it — earlier mail is ignored. If your own waiting task for the same service already holds the address, that task comes back with 200 instead
POST/v1/tasks/{id}/cancelCancel after cancellable_at · full refund

Errors & limits

Every error is JSON with a stable code and a request_id you can quote to support. Branch on code, never on the message text — messages change, codes do not.

There is no requests-per-second limit. What is capped is failure: more than 500 failed requests in one UTC minute (HTTP 400 and above, except 429) pauses your API access for 10 minutes, then 30 minutes, 1 hour and 24 hours if it repeats. It counts per account, across all keys. The 429 body says until when and Retry-After gives the seconds; only an admin can lift a pause early. Successful calls never count toward it.

Watch it from your client. GET /v1/me returns pause.fail_limit_per_minute, pause.failures_this_minute, pause.window_resets_at and pause.paused_until, and keeps answering while you are paused.

{ "error": { "code": "insufficient_balance", "message": "balance is below the service price", "request_id": "1f0495b3…" } }
HTTPcodeMeaning
401unauthorizedMissing, invalid or revoked key
402insufficient_balanceTop up your wallet
409no_stockNo mailbox available for that service/domain right now. An address is closed for a service once it has received a code there and stays usable for every other service, so GET /v1/domains?service=<slug> for the real count
503maintenanceTask creation is paused for maintenance; retry in a few minutes
409cancel_too_earlyWait until cancellable_at
409mailbox_busySame-address retry: that mailbox is in use right now; retry shortly
429api_pausedToo many failed requests in one minute — access pauses for a while and the message says until when
400validation_errorBad JSON, or a service that is unknown, inactive or not priced yet
403forbiddenKey lacks the scope, caller IP is not on the key allowlist, or the account is suspended
404not_foundNo such task on this account — another account&#39;s task id returns 404 as well
409conflictTask is no longer in a cancellable state
409not_repeatableOnly received, timeout or cancelled tasks can be repeated
429rate_limitedA per-second limit was set on this key (off by default); Retry-After says when to retry
500internalOur fault — retry and quote request_id if it persists