Gmail OTP API in Python: Rent and Wait
Reading a verification code inside a Python script comes down to three HTTP calls: rent an inbox, poll until a code shows up, cancel if you give up. This walkthrough builds exactly that, using nothing but requests and the standard library, against the real OTPGmail API.
What the script needs to do
Every account has its own API key on the Account page; send it as Authorization: Bearer <api_key>. The whole flow is:
POST /v1/ordersto rent an address for a specific service.- Poll
GET /v1/orders/{orderId}every few seconds untilotphas an entry. POST /v1/orders/{orderId}/cancelif you no longer want to wait — refunded in full, as long as no code has arrived.
If nothing happens on its own, the order cancels itself and refunds automatically after 25 minutes, so step 3 is a courtesy, not a requirement.
Renting an inbox
import uuid
import requests
HOST = "otpgmail.net"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
def rent(service, domain="gmail.com"):
resp = requests.post(
f"https://{HOST}/v1/orders",
json={"service": service, "domain": domain},
headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())},
timeout=15,
)
body = resp.json()
if not body.get("success"):
raise RuntimeError(body["error"]["code"])
return body["data"][0] # data is a list — one entry per rented inbox
domain is optional and defaults to gmail.com; pass "icloud.com" where a service offers it — iCloud inboxes are usually cheaper than Gmail, depending on the service. The Idempotency-Key header matters more than it looks: generate a new UUID for every new order, and if the request times out, retry with that same key so the API hands back the order it already created instead of billing you for a second one.
Use requests, not the standard-library urllib — its default Python-urllib user agent gets rejected with an HTTP 403 by the CDN in front of the API.
Waiting for the code
import time
def wait_for_code(order_id, max_wait=600, interval=4):
deadline = time.time() + max_wait
while time.time() < deadline:
resp = requests.get(f"https://{HOST}/v1/orders/{order_id}", headers=HEADERS, timeout=15)
order = resp.json()["data"]
if order["otp"]:
return order["otp"][-1]["code"] # newest code is last
if order["status"] == "cancelled":
return None
time.sleep(interval)
# Gave up before the API's own 25-minute timeout — cancel now for an instant refund
requests.post(f"https://{HOST}/v1/orders/{order_id}/cancel", headers=HEADERS, timeout=15)
return None
A 3–5 second interval is the sweet spot: fast enough that you are not the bottleneck, slow enough to stay well inside the rate limit. otp is ordered oldest first, so otp[-1] is always the most recent code — useful later if the same order picks up a second code (a login, a device confirmation) while it can still receive codes (the service's code limit and the source's window apply).
Putting it together
try:
order = rent("git", domain="icloud.com")
except RuntimeError as e:
if str(e) in ("DOMAIN_UNAVAILABLE", "NO_MAILS_AVAILABLE"):
order = rent("git", domain="gmail.com") # fall back to Gmail
else:
raise
print("Address:", order["email"])
code = wait_for_code(order["orderId"])
print("Code:", code or "none received")
git is GitHub's service code from GET /v1/services; swap it for any other code (dr for ChatGPT, ds for Discord) to target a different sign-up. Fill in the target service's own sign-up form with order["email"], submit it, and only then start polling — the code will not exist before the form is submitted.
Handling errors
rent() raises whenever success is false; catch specific codes rather than a bare exception so a script can react sensibly:
NO_MAILS_AVAILABLE— the provider is temporarily out of inboxes for that service. Nothing was charged; retry after a short backoff or switchdomain.DOMAIN_UNAVAILABLE— that service has no iCloud option, or iCloud is briefly disabled. Retry with"gmail.com".INSUFFICIENT_BALANCE— top up before renting again.VALIDATION_ERROR— check the service code againstGET /v1/services; it likely does not exist or is spelled wrong.- HTTP 429 (
RATE_LIMITED) — you are over the rate limit (20 requests/second or 900/minute by default); read theRetry-Afterheader and back off.
For the full error table, the field-by-field response shape, and how the equivalent script looks in JavaScript, see the API docs and the Node.js example. A ready-to-run version of this script, plus Node.js, PHP, C# and curl clients, is in the public otpgmail-email-otp-api repository. For patterns specific to test suites — retries, teardown, running many orders in parallel — see renting Gmail for testing sign-up flows, and the Gmail OTP API overview for the rest of the endpoints.
Frequently asked questions
Why does the request need an Idempotency-Key header?
So a retried request after a dropped connection does not rent a second inbox. Reuse the same UUID only when retrying the exact same order; generate a new one for every new order.
How long should max_wait be?
It depends on the service: a GitHub code usually arrives after about 1 minute and an AWS code after about 1 minute, while ChatGPT usually takes about 1 minute and sometimes longer. Setting max_wait above 25 minutes has no effect — the API cancels and refunds an order with no code after that point regardless.
Can I rent several inboxes in one call?
Yes, with a quantity field in the request body (default 1, up to 200). data then holds one entry per inbox instead of just data[0].
What does order["otp"][-1]["code"] return before any code has arrived?
An IndexError, since otp is an empty list until the first code lands — that is exactly why wait_for_code checks if order["otp"]: first.
Create an account and grab your key on the rent page, then keep the API docs open while you adapt this script to your own service.
