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

  1. POST /v1/orders rents an address for the target service. You are charged only if one is issued.
  2. Your test types the address into the sign-up form.
  3. Poll GET /v1/orders/{orderId} every 3–5 seconds until the otp array has an item.
  4. Use the code. For 24 hours, later codes (logins, device confirmations) are appended to the end of otp[].
  5. No longer need it? POST /v1/orders/{orderId}/cancel refunds 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

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

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

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.

Get your verification code now

Sign up free. You are only charged when an inbox is issued. No code within 30 minutes — 100% refunded automatically.

Can't find the service you need? Use Request a service on the rental page and we will add it if our infrastructure supports it.