Read an OTP From Gmail: Gmail API, IMAP or a Rented Inbox
Yes, Gmail has an API. The Gmail API, or plain IMAP, can read an OTP from a Gmail account you already control, once OAuth or an app password is set up. Neither creates an address or "delivers" codes. A rented-inbox API works the other way round: your script receives a new @gmail.com or @icloud.com address and the extracted code.
Written for developers who searched "does Gmail have an API" or "rent Gmail API" and want to pick a route before coding. Google's rules below come from its developer and help pages, read on 13 October 2026.
Three routes, side by side
| IMAP + app password | Gmail API + OAuth | Rented-inbox API (OTPGmail) | |
|---|---|---|---|
| Whose mailbox | Yours | Yours | A real inbox rented per order |
| One-time setup | 2-Step Verification on, create an app password | Cloud project, consent screen, OAuth client, token storage | Copy your API key |
| Ongoing upkeep | App password revoked when the account password changes | Refresh token can expire or be revoked | Keep the key secret |
| Finding the code | Search, fetch and parse the MIME body yourself | Query, fetch, decode and parse yourself | otp[] holds the extracted code or link |
| New address per sign-up | No, every code lands in one mailbox | No | Yes |
| Parallel runs | Runs share one mailbox, so codes can collide | Same | Each run has its own order |
| Cost | No charge | No charge within quotas | Per inbox; refunded if no code arrives |
The first two routes suit codes for an account you own and keep; the third suits a script that needs an address it did not have.
Route 1: read the OTP from Gmail over IMAP
Gmail's IMAP server is imap.gmail.com on port 993, SSL required, and since January 2025 IMAP access is always on. Google's developer guide describes signing in with OAuth 2.0 credentials through the SASL XOAUTH2 mechanism (Google's IMAP and SMTP guide). For a quick script, many developers use an app password instead: a 16-digit passcode that works only with 2-Step Verification on, may be missing on work or school accounts and with Advanced Protection, and is revoked whenever the account password changes. Google says app passwords "aren't recommended and are unnecessary in most cases", so keep this to a dedicated test mailbox.
import email, email.policy, imaplib, re, time
def read_otp_imap(user, app_password, sender, since_ts, timeout=300):
end = time.time() + timeout
while time.time() < end:
with imaplib.IMAP4_SSL("imap.gmail.com", 993) as box:
box.login(user, app_password)
box.select("INBOX")
day = time.strftime("%d-%b-%Y", time.gmtime(since_ts))
_, ids = box.search(None, "FROM", sender, "SINCE", day)
for num in reversed(ids[0].split()):
_, data = box.fetch(num, "(RFC822)")
msg = email.message_from_bytes(data[0][1], policy=email.policy.default)
if msg["Date"].datetime.timestamp() < since_ts:
continue # older mail from the same sender
part = msg.get_body(preferencelist=("plain", "html"))
text = re.sub(r"<[^>]+>", " ", part.get_content()) if part else ""
m = re.search(r"code\D{0,40}(\d{4,8})", text, re.I)
if m:
return m.group(1)
time.sleep(5)
return None
What usually goes wrong:
- Stale codes. IMAP
SINCEcompares dates only and ignores the time of day, so it also returns the code from your previous run. Compare each message's time with the moment you submitted the form, as the loop does. - The wrong number. Emails also carry years and order numbers. Anchor the pattern on words near the code and test it on real emails from that sender.
- HTML-only messages. Some senders include no plain-text part, so strip the tags before matching.
- Filtered mail. The junk folder is a separate IMAP folder; look there before blaming the sender.
Route 2: read the OTP with the Gmail API and OAuth
- Create a Google Cloud project, enable the Gmail API, set up the OAuth consent screen and create an OAuth client.
- Request the narrowest scope that works,
https://www.googleapis.com/auth/gmail.readonly. Google classes it as restricted: an app used by other people needs restricted-scope verification, plus a security assessment if it stores or transmits that data on its servers. - Run the consent flow once, store the refresh token, and let the client library exchange it for access tokens.
- Search with the same syntax as the Gmail search box, then fetch and decode the message.
from googleapiclient.discovery import build
def find_code_messages(creds, sender, since_ts):
gmail = build("gmail", "v1", credentials=creds)
query = f"from:{sender} after:{int(since_ts)}" # Unix seconds, not a date
res = gmail.users().messages().list(userId="me", q=query, maxResults=5).execute()
return [gmail.users().messages().get(userId="me", id=m["id"], format="full").execute()
for m in res.get("messages", [])]
# then base64url-decode payload.parts[].body.data and run the same pattern as Route 1
Details from Google's documentation that bite OTP scripts:
- Dates in
after:andbefore:are read as midnight PST. Passing Unix seconds, as above, gives an exact cut-off and is the cleanest stale-code guard. - An app whose consent screen is set to External with the publishing status "Testing" gets refresh tokens that expire after 7 days, so a nightly job stops after a week. Tokens carrying Gmail scopes also stop working when the user changes the password, or after six months unused (refresh token expiration).
- Quota is rarely the issue: 6,000 units per user per minute, with
messages.listat 5 units andmessages.getat 20, so one list plus one fetch every 5 seconds uses about 300 quota units a minute. messages.listleaves out the junk and trash folders unless you setincludeSpamTrash.users.watchwith Cloud Pub/Sub can push changes instead, but the watch must be renewed at least every 7 days; for a few codes, polling is simpler.
Route 3: what “rent Gmail API” gets you
People who search "rent Gmail API" usually want this route: an API that hands the script an address it did not have. With OTPGmail, the job the two snippets above do looks like this:
import time, uuid, requests
HOST = "otpgmail.net"
H = {"Authorization": "Bearer YOUR_API_KEY"}
body = requests.post(f"https://{HOST}/v1/orders", json={"service": "git"}, # git = GitHub
headers={**H, "Idempotency-Key": str(uuid.uuid4())}, timeout=15).json()
if not body["success"]:
raise SystemExit(body["error"]["code"])
order = body["data"][0] # data is a list, one entry per inbox
print("Type this address into the form:", order["email"])
while True:
o = requests.get(f"https://{HOST}/v1/orders/{order['orderId']}", headers=H, timeout=15).json()["data"]
if o["status"] == "completed":
print("Code:", o["otp"][-1]["code"]) # newest code is last
break
if o["status"] == "cancelled":
print("No code before codeDeadlineAt, refunded")
break
time.sleep(4)
What disappears: OAuth, MIME parsing and the stale-code filter, because everything in otp[] belongs to this order. What you take on: a charge per inbox, refunded in full when no code arrives before the order's codeDeadlineAt (25 minutes at most; see the rental refund rules). You never log in to the inbox, and it receives codes only within the order window. Calls are rate limited per key, by default 20 requests per second and 900 per minute; current values are on the docs page.
The full walkthrough with error handling is Gmail OTP API in Python, and the Gmail OTP API overview lists every endpoint. Service codes come from GET /v1/services; a site that is not listed uses ot (Any service), priced higher.
Which route fits your job
| Your situation | Route | Why |
|---|---|---|
| Login or reset codes for an account you keep, such as your bot's own login | IMAP or Gmail API on a mailbox you own | The address must outlive any rental |
| A one-off script | IMAP with an app password on a separate test mailbox | Fastest setup; Google discourages it beyond that |
| A tool other people install to read their own mail | Gmail API with OAuth | Google's supported route; plan for verification |
| Your own sign-up flow tested by many parallel runs | A rented inbox under ot |
One address per run, no collisions |
| A new account on a third-party service for a project | A rented inbox per sign-up, within that service's terms | A fresh real address; move the account to one you control if you keep it |
If you are not sure whether you need to send codes or receive them, sending vs receiving email OTP APIs sorts that out first.
Keep mailboxes and keys safe
- Never point a script at your personal Gmail. IMAP and
gmail.readonlyboth expose every message in the account, including other services' password resets. - Keep app passwords, refresh tokens and API keys in a secret store, never in a repository or front-end code.
- On OTPGmail, Generate new key on the API docs page revokes the old key at once; use it if a key leaks.
- Whatever you automate, stay within the target service's terms.
Frequently asked questions
Is there a Gmail “OTP API key”?
No. Google has no product for reading OTPs; the Gmail API uses an OAuth client from the Google Cloud console plus a token for each user. The "key" in many tutorials is either an app password or a third-party API key, such as the OTPGmail key shown on the API docs page once you are logged in.
Can the same IMAP code read iCloud Mail?
Yes, with Apple's settings: server imap.mail.me.com, port 993, SSL required, and an app-specific password generated for your Apple Account. Apple says the username is usually the part before @icloud.com, and iCloud Mail does not support POP.
Does the Gmail API cost money?
Normal use is not billed. Google's quota page (updated 10 September 2026) lists a daily threshold of 80,000,000 quota units per project before charges apply, far above what an OTP poller uses.
Why does my Gmail API script say no OTP was received?
Check in this order: whether the sender really sent it (resend once), your q filter (a wrong from: or a date read as midnight PST hides the message), the junk and trash folders that messages.list skips by default, and whether the refresh token still works.
