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_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXA 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.
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.
$ 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.
$ 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
| Method | Path | Purpose |
|---|---|---|
| GET | /v1/me | Balance, discount and the key's scopes |
| GET | /v1/services | Catalog with price, window and code format |
| GET | /v1/domains | Domains with ready mailbox counts. Add ?service=<slug> to count only what that service can still use |
| POST | /v1/tasks | Create a task (rent a mailbox, hold the price) |
| GET | /v1/tasks | List your tasks · ?status=&limit= |
| GET | /v1/tasks/{id} | Task status · ?wait=0..30 long-poll |
| GET | /v1/tasks/{id}/stream | Server-Sent Events stream |
| GET | /v1/tasks/{id}/message | Delivered message: code, link, subject, sender, text body |
| POST | /v1/tasks/{id}/again | Another 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}/cancel | Cancel 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…" } }| HTTP | code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing, invalid or revoked key |
| 402 | insufficient_balance | Top up your wallet |
| 409 | no_stock | No 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 |
| 503 | maintenance | Task creation is paused for maintenance; retry in a few minutes |
| 409 | cancel_too_early | Wait until cancellable_at |
| 409 | mailbox_busy | Same-address retry: that mailbox is in use right now; retry shortly |
| 429 | api_paused | Too many failed requests in one minute — access pauses for a while and the message says until when |
| 400 | validation_error | Bad JSON, or a service that is unknown, inactive or not priced yet |
| 403 | forbidden | Key lacks the scope, caller IP is not on the key allowlist, or the account is suspended |
| 404 | not_found | No such task on this account — another account's task id returns 404 as well |
| 409 | conflict | Task is no longer in a cancellable state |
| 409 | not_repeatable | Only received, timeout or cancelled tasks can be repeated |
| 429 | rate_limited | A per-second limit was set on this key (off by default); Retry-After says when to retry |
| 500 | internal | Our fault — retry and quote request_id if it persists |