API v1

Quickstart

Base URL https://api.mailotp.org. Everything is JSON over HTTPS and there is no SDK to install. Three calls take you from an empty account to a delivered code.

1 · keyCreate a key in Panel → API keys. It starts with mo_live_; Show on that page brings it back later.
2 · taskPOST /v1/tasks with a service slug — or any site's domain — returns a real mailbox address.
3 · codeGET /v1/tasks/{id}?wait=30 blocks until the code lands, then returns it.
curl
$ curl -X POST https://api.mailotp.org/v1/tasks \
  -H "Authorization: Bearer mo_live_…" \
  -H "Idempotency-Key: order-8812" \
  -d '{"service":"instagram","domain":"hotmail.com"}'

Authentication

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

headers
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.

Tasks that never get a code are limited. If fewer than 10% of your last 500 finished tasks for a service received a code, you can hold at most 10 waiting tasks for that service; one more gets 429 low_success_limit with Retry-After. Honour it: unlike other 429s these count as failed requests, so retrying more than 500 times a minute triggers the failure pause. The limit lifts by itself as codes arrive again.

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.

A second code on the same address is free for 10 minutes. Platforms often send a second email for the same signup. Call /again within 10 minutes of the last charged code on that address and the new task costs nothing ("price": "0", no hold). A free code does not extend that time; after it, /again is charged like any task. Not for the other catch-all service.

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.

Site not in the catalog? Send the site's domain as service ("service": "nusuk.sa"). If the domain belongs to a catalog service you get that service. Otherwise the task opens under other, bound to that site: "site": "nusuk.sa" in the response, an address that site has never seen, and mail from another catalog service is not delivered to it. Codes, links or both arrive as usual. You don't have to wait for a site to appear in GET /v1/services before ordering it — order it by domain any time. Once a code has been delivered for a site, it joins the catalog automatically (within a few minutes) as its own service with its own slug.

Still using service: "other"? It keeps working as before: the first code from any sender. Add site and we hand you an address that site has never seen, but the task is not bound to it.

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

Same address again

Need a code on an address you used before, such as a login check or a password reset days or weeks later? Call POST /v1/tasks/{id}/again with the id of an earlier task on that address. It opens a new task on the same address. As with any task, only mail that arrives after the call counts: call it first, then have the site send the code.

It works for as long as we keep the task — 180 days — and its mailbox is still active. The earlier task must be finished (received, timeout or cancelled); a task that is still waiting is already listening on the address. The new task is charged like any task, except within 10 minutes of the last charged code on that address (see Task lifecycle).

Lost the id? GET /v1/tasks?email=<address> lists your tasks on that address, newest first. Without email it lists all of them. A page holds up to 100 (limit, default 50); send next_cursor back as cursor for the next, older page, until it is null.

find the task, then reopen the address
$ curl "https://api.mailotp.org/v1/tasks?email=quiet.harbor42@hotmail.com" -H "Authorization: Bearer mo_live_…"

{
  "tasks": [ { "id": "c1d5cac4-…", "status": "received", "email": "quiet.harbor42@hotmail.com", … } ],
  "next_cursor": null
}

$ curl -X POST "https://api.mailotp.org/v1/tasks/c1d5cac4-…/again" -H "Authorization: Bearer mo_live_…"

When it cannot open, you get 409 mailbox_busy. The message says why: the address is serving another task or has reached today's limit (retry later), or it is no longer active (create a new task instead). 404 not_found for an id you had means the task is older than 180 days and was deleted.

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. Keep one wait or stream per task: a task accepts at most 8 open at once, and one more gets 429 too_many_waiters — it does not count toward the failure pause.

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, key scopes and the pause counters
GET/v1/servicesKnown services with price, window and code format — a large, auto-growing list, not the limit of what you can order (see Create a task)
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/tasksYour tasks, newest first · ?status= ?email=<address> ?limit=1..100 (default 50) ?cursor=<next_cursor> for the next page
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, minutes or weeks later (tasks are kept for 180 days): a new task pinned to that mailbox (free — price "0" — when started within 10 minutes of the last charged code on that address for the same service, not for other; a free code does not extend that time; charged again after it). 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.json
{ "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 (/again): the address is serving another task or has reached today's limit — retry later — or is no longer active — create a new task. The message says which
429api_pausedToo many failed requests in one minute — access pauses for a while and the message says until when
400validation_errorBad JSON, a service that is unknown, inactive or not priced yet, or a malformed email or cursor on GET /v1/tasks
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’s task id returns 404 as well, and so does a task older than 180 days (tasks are deleted then)
409conflictTask is no longer in a cancellable state
409not_repeatableOnly received, timeout or cancelled tasks can be repeated; a waiting task is already listening on the address
429rate_limitedA per-second limit was set on this key (off by default); Retry-After says when to retry
429low_success_limitFewer than 10% of your last 500 finished tasks for this service received a code: at most 10 waiting tasks for it until codes arrive again. Counts toward the failure pause — honour Retry-After
429too_many_waitersMore than 8 waits or streams open on one task; let one return. Not counted toward the failure pause
500internalOur fault — retry and quote request_id if it persists