Email OTP API for Testing: Receive Codes in E2E Tests
Most "email OTP API" products send codes. This guide covers the other side: receiving a verification code inside an automated test. Mail catchers and sandbox SMTP servers only see email your own app sends. When your end-to-end suite signs up for a third-party service, the code goes to a real domain, and temp-mail domains are often refused. The OTPGmail API lets a test rent a genuine @gmail.com or @icloud.com inbox, read the code and cancel for a refund with a few HTTP requests.
The standard flow
POST /v1/ordersrents an address for the target service. You are charged only if one is issued.- Your test types the address into the sign-up form.
- Poll
GET /v1/orders/{orderId}every 3–5 seconds until theotparray has an item. - Use the code. For 24 hours, later codes (logins, device confirmations) are appended to the end of
otp[]. - No longer need it?
POST /v1/orders/{orderId}/cancelrefunds at once, as long as no code has arrived. Left alone, an order without a code is cancelled and refunded after 30 minutes.
Authentication and response envelope
- Base URL:
https://otpgmail.net; the full reference is in the API docs. - Each account has its own API key on the Account page. Send
Authorization: Bearer <api_key>(or?api_key=where headers cannot be set). - Every response is wrapped:
{ "success": true, "data": ... }, or{ "success": false, "error": { "code", "message" } }with a matching HTTP status.
Step 1: list services and stock
GET /v1/services returns each service with code, name, price, stock and an icloud field ({ price, stock }, or null if the service has no iCloud option). Use it to look up codes — git for GitHub, dr for ChatGPT — and to check stock before a large run.
Step 2: create an order
POST /v1/orders
Authorization: Bearer <api_key>
Idempotency-Key: <a new UUID per order>
Content-Type: application/json
{ "service": "git", "domain": "icloud.com" }
domain is gmail.com or icloud.com and defaults to gmail.com. data is an array of orders, because one request can rent several inboxes with quantity. Each order carries orderId, service, domain, email, status, price, otp: [], codeDeadlineAt and rentExpiresAt.
Send an Idempotency-Key with a fresh UUID for every new order. If a request times out, retry with the same key and the API returns the order it already created instead of renting a second inbox. Creation normally takes a fraction of a second but can reach 10 seconds at peak, so allow a client timeout of at least 15 seconds.
Step 3: poll for the code, cancel if you must
GET /v1/orders/{orderId} returns status waiting_code, completed (a code has arrived; more may follow for 24 hours) or cancelled. Codes look like [{ "code": "123456", "receivedAt": ... }], oldest first, so take the last element. Polling faster than every 3–5 seconds does not speed up the email; it only eats into your rate limit.
Cancelling works only while the order is waiting_code with no code; after a code arrives, the API answers ORDER_NOT_CANCELLABLE.
Python example
import time
import uuid
import requests
BASE = "https://otpgmail.net"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
def rent(service, domain="gmail.com"):
r = requests.post(
f"{BASE}/v1/orders",
json={"service": service, "domain": domain},
headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())},
timeout=30,
)
body = r.json()
if not body.get("success"):
raise RuntimeError(body["error"]["code"])
return body["data"][0] # data is an array of orders
def wait_code(order_id, max_wait=600, interval=4):
deadline = time.time() + max_wait
while time.time() < deadline:
body = requests.get(f"{BASE}/v1/orders/{order_id}", headers=HEADERS, timeout=30).json()
order = body["data"]
if order["otp"]:
return order["otp"][-1]["code"]
if order["status"] == "cancelled":
return None
time.sleep(interval)
# Out of patience: cancel for an immediate refund instead of waiting for the 30-minute auto-cancel
requests.post(f"{BASE}/v1/orders/{order_id}/cancel", headers=HEADERS, timeout=30)
return None
try:
order = rent("git", domain="icloud.com")
except RuntimeError as e:
if str(e) in ("DOMAIN_UNAVAILABLE", "NO_MAILS_AVAILABLE", "OUT_OF_STOCK"):
order = rent("git", domain="gmail.com") # fall back to Gmail
else:
raise
print("Email:", order["email"])
code = wait_code(order["orderId"])
print("OTP:", code)
Node.js example
// Node 18+, run as an ES module
import { randomUUID } from "node:crypto";
const BASE = "https://otpgmail.net";
const HEADERS = { Authorization: `Bearer ${process.env.OTPGMAIL_API_KEY}` };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function rent(service, domain = "gmail.com") {
const res = await fetch(`${BASE}/v1/orders`, {
method: "POST",
headers: { ...HEADERS, "Content-Type": "application/json", "Idempotency-Key": randomUUID() },
body: JSON.stringify({ service, domain }),
});
const body = await res.json();
if (!body.success) throw new Error(body.error.code);
return body.data[0]; // data is an array of orders
}
async function waitCode(orderId, maxWaitMs = 600_000, intervalMs = 4000) {
const deadline = Date.now() + maxWaitMs;
while (Date.now() < deadline) {
const res = await fetch(`${BASE}/v1/orders/${orderId}`, { headers: HEADERS });
const { data: order } = await res.json();
if (order.otp.length) return order.otp.at(-1).code;
if (order.status === "cancelled") return null;
await sleep(intervalMs);
}
await fetch(`${BASE}/v1/orders/${orderId}/cancel`, { method: "POST", headers: HEADERS });
return null;
}
let order;
try {
order = await rent("git", "icloud.com");
} catch (e) {
if (["DOMAIN_UNAVAILABLE", "NO_MAILS_AVAILABLE", "OUT_OF_STOCK"].includes(e.message)) {
order = await rent("git", "gmail.com"); // fall back to Gmail
} else throw e;
}
console.log("Email:", order.email);
console.log("OTP:", await waitCode(order.orderId));
Rate limits
Limits apply per API key: 10 requests per second and 300 requests per minute. Beyond that you receive HTTP 429 with RATE_LIMITED and a Retry-After header giving the seconds to wait. A 4-second polling interval keeps even a parallel suite well below the limit. Waiting orders are capped too (WAITING_LIMIT_REACHED); cancel those you no longer need.
Errors worth handling
- NO_MAILS_AVAILABLE (503) or OUT_OF_STOCK (409): no inbox for that service right now, and nothing is charged. Try the other
domainor retry with backoff. - DOMAIN_UNAVAILABLE (409): the service has no iCloud option (Apple ID, for example) or iCloud is temporarily off. Fall back to
gmail.com. - INSUFFICIENT_BALANCE (402): top up, in USDT if you like, and let the suite warn you when the balance runs low.
- VALIDATION_ERROR (400): wrong service code or
domain. Take codes fromGET /v1/services. - Your own timeout: pick a wait that fits the service — GitHub usually delivers in about 30 seconds, ChatGPT in 3–4 minutes — then cancel for an instant refund.
Gmail or iCloud in a test suite
iCloud inboxes cost about 10% less, so at volume request icloud.com first and fall back to gmail.com on NO_MAILS_AVAILABLE, OUT_OF_STOCK or DOMAIN_UNAVAILABLE. iCloud stock is a shared pool that can empty and refill, so treat the fallback as normal behavior. 114 of 115 services offer iCloud; see Gmail or iCloud for verification codes.
Practical tips
- Store the
orderIdimmediately, so you can poll for login codes within 24 hours and reconcile costs. - In Python, use
requests. The built-inurllibsends a defaultPython-urllibUser-Agent, which the CDN in front of the API rejects with HTTP 403. - Full clients for Python, Node.js, PHP, C# (.NET) and curl, plus the table of service codes, are in the public repo otpgmail-email-otp-api, all run against the live API.
- Respect the target service's terms on automated accounts. Test accounts for development and QA are legitimate; bulk accounts meant to cause harm are not, and OTPGmail is not responsible for misuse.
Frequently asked questions
Where do I get an API key?
Log in to OTPGmail and open the Account page; each account has one key. Treat it like a password: keep it in an environment variable or CI secret, never in a repository.
Do I get a refund if I cancel after the code arrives?
No. Only an order still waiting for its first code can be cancelled for a 100% refund.
How do I receive a second code on the same order?
Keep polling GET /v1/orders/{orderId} while the rental is valid (24 hours, see rentExpiresAt). New codes are appended to the end of the otp array at no extra cost.
Create an account and get your key on the rent page, check per-service pricing on the price list, and read the full API docs. For a concrete target, see the GitHub inbox page or the overview of receiving email OTPs online.