Gmail OTP API in Node.js: Order and Poll
Turning a sign-up form and a verification code into three fetch calls is most of what this guide covers: create an order, poll until a code shows up, and cancel if you give up. Everything below runs on Node 18 or newer, which ships a global fetch, so there is nothing extra to install.
What the script needs to do
Every account has its own API key on the Account page, sent as Authorization: Bearer <api_key> on every call. The flow itself is three endpoints:
POST /v1/orders— rent an inbox for a chosen service.GET /v1/orders/{orderId}— poll every few seconds untilotphas an entry.POST /v1/orders/{orderId}/cancel— refund in full if you stop waiting before any code arrives.
Leave an order alone with no code and it cancels and refunds itself automatically after 25 minutes, so step 3 only matters when you want the refund sooner.
Creating the order
import { randomUUID } from "node:crypto";
const HOST = "otpgmail.net";
const HEADERS = { Authorization: `Bearer ${process.env.OTPGMAIL_API_KEY}` };
async function rent(service, domain = "gmail.com") {
const res = await fetch(`https://${HOST}/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 — one entry per rented inbox
}
domain defaults to gmail.com; pass "icloud.com" for services that offer it — iCloud inboxes are usually cheaper than Gmail, depending on the service. The Idempotency-Key header is what makes retries safe: generate a fresh UUID for every new order, and reuse the same one only when resending an order that may have already gone through — the API returns the existing order instead of creating a duplicate.
Polling for the code
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function waitForCode(orderId, maxWaitMs = 600_000, intervalMs = 4000) {
const deadline = Date.now() + maxWaitMs;
while (Date.now() < deadline) {
const res = await fetch(`https://${HOST}/v1/orders/${orderId}`, { headers: HEADERS });
const { data: order } = await res.json();
if (order.otp.length) return order.otp.at(-1).code; // newest code is last
if (order.status === "cancelled") return null;
await sleep(intervalMs);
}
await fetch(`https://${HOST}/v1/orders/${orderId}/cancel`, { method: "POST", headers: HEADERS });
return null;
}
Every 3–5 seconds is the practical interval — quick enough to catch the code promptly, slow enough to stay well inside the rate limit. Because otp is ordered oldest first, .at(-1) always returns the latest code, which matters if the same order later collects a second one (a login code, a device confirmation) while it can still receive codes (the service's code limit and the source's window apply).
Full example
let order;
try {
order = await rent("dr", "icloud.com"); // "dr" = ChatGPT
} catch (e) {
if (["DOMAIN_UNAVAILABLE", "NO_MAILS_AVAILABLE"].includes(e.message)) {
order = await rent("dr", "gmail.com"); // fall back to Gmail
} else {
throw e;
}
}
console.log("Address:", order.email);
const code = await waitForCode(order.orderId);
console.log("Code:", code ?? "none received");
Fill in the target sign-up form with order.email before you start polling — there is nothing to poll for until the form has actually been submitted. Service codes such as "dr" come from GET /v1/services; a Discord or GitHub sign-up follows exactly the same shape with a different code.
Handling errors
rent() throws whenever success is false, with the error code as the message, so a check on e.message covers the common cases:
NO_MAILS_AVAILABLE— no inbox available for that service right now; nothing was charged. Back off and retry, or switchdomain.DOMAIN_UNAVAILABLE— the service has no iCloud option, or iCloud is briefly disabled. Retry with"gmail.com".INSUFFICIENT_BALANCE— the account needs a top-up before it can rent again.VALIDATION_ERROR— double-check the service code againstGET /v1/services.- HTTP 429,
RATE_LIMITED— past the rate limit (20 requests/second or 900/minute by default); the response carries aRetry-Afterheader with the number of seconds to wait.
The API docs list the full response shape and every error code; the Python example walks through the identical flow with requests if that fits your stack better. A ready-to-run version of this script — plus Python, PHP, C# and curl clients — is in the public otpgmail-email-otp-api repository. Building this into an actual test suite, with teardown and parallel orders, is covered in renting Gmail for testing sign-up flows; the Gmail OTP API overview covers the rest of the endpoints.
Frequently asked questions
Why store the API key in an environment variable instead of the script?
So it never ends up in source control or a shared file. Treat OTPGMAIL_API_KEY like any other secret — a leaked key lets someone else spend your balance.
What if fetch is not available in my Node version?
fetch is global from Node 18 onward. On an older version, install a polyfill such as node-fetch and import it the same way, or upgrade Node — nothing else in the example changes.
Can waitForCode return before a code actually exists?
No — it only returns once order.otp.length is truthy or the order is cancelled. Before that, it keeps polling until maxWaitMs runs out, at which point it cancels the order itself.
How do I pick up a second code on the same order?
Keep calling GET /v1/orders/{orderId}. New codes are appended to the end of otp while the order can still receive them (the service's code limit and the source's window apply); at most sources no extra call is needed. One source needs POST /v1/orders/{orderId}/next-code with confirmFee: true outside its no-charge window (50% of the rental price, refunded if no new email arrives within 20 minutes), and private iCloud inboxes use POST /v1/orders/{orderId}/rerent to rent the same inbox again at 50% of the price.
Grab your API key on the rent page and keep the API docs open while you wire this into your own project.
