There are five practical ways for code to learn that an email has arrived: IMAP, polling a REST endpoint, long-polling, a server-sent event stream, and webhooks. They all work. They differ in how much you have to build, how quickly you hear about new mail, and what happens when something on your side is down. This post walks through each one using Lumbox's endpoints as the concrete example, because Lumbox exposes all five for the same inbox.

Option 1: IMAP

IMAP is the protocol mail clients use, and every language has a library for it (Python ships imaplib in the standard library). Its big advantage is reach: it works against almost any existing mailbox. The costs are that the connection is stateful, you track message UIDs yourself to know what is new, and what you get back is raw MIME that you decode and parse. The IDLE extension (RFC 2177) lets a server notify an open connection about new mail, but you still manage reconnects and re-issue the command when connections drop. Big providers add their own rules on top; Google, for example, steers apps toward Sign in with Google and only allows app passwords on accounts with 2-Step Verification.

Lumbox inboxes can be read over IMAP too. When IMAP is enabled, the create response includes imap_password, imap_host (imap.lumbox.co) and imap_port (993), and POST /v1/inboxes/:id/imap-password issues a new password. The Python SDK's create_inbox does not keep those fields, so read them from the raw HTTP response if you want them.

import email, imaplib

M = imaplib.IMAP4_SSL("imap.lumbox.co", 993)
M.login("signup-bot@trylumbox.com", IMAP_PASSWORD)
M.select("INBOX")
typ, data = M.uid("search", None, "UNSEEN")
for uid in data[0].split():
    typ, parts = M.uid("fetch", uid, "(RFC822)")
    msg = email.message_from_bytes(parts[0][1])
    print(msg["From"], msg["Subject"])
M.logout()

Lumbox's own docs pitch IMAP at humans: plug an agent's inbox into Apple Mail or Thunderbird to watch what it receives. For the agent itself, the HTTP options below return parsed JSON and skip the MIME work.

Option 2: REST polling

The simplest HTTP approach is a loop that lists recent mail every few seconds:

GET /v1/inboxes/{id}/emails?since=2026-09-29T10:00:00Z&unread=true&limit=20

The list returns up to 100 emails per page with a cursor, hides spam unless you pass include_spam=true, and supports filters for sender, category and label. Each item is a full email object, parsed codes included.

Polling has two costs. The first is delay: with a five second interval, a message waits two and a half seconds on average before your loop sees it. The second is rate limit. The API allows 120 requests a minute per key by default, and one inbox polled every five seconds uses 12 of them, so ten inboxes polling on the same key consume the whole budget and leave nothing for sending or anything else. Polling suits a cron job that checks a handful of inboxes every minute; it is a poor fit for an agent waiting on a specific email.

Option 3: Long-poll

A long-poll is one request that the server holds open until a match arrives or the timeout passes:

import httpx

r = httpx.get(
    f"https://api.lumbox.co/v1/inboxes/{inbox_id}/wait",
    headers={"X-API-Key": API_KEY},
    params={"timeout": 60, "from": "github.com"},
    timeout=70,  # longer than the server-side wait
)
if r.status_code == 408:
    email_obj = None  # nothing matching in 60 seconds
else:
    r.raise_for_status()
    email_obj = r.json()

/wait accepts a timeout from 1 to 120 seconds (default 30) and filters on sender, subject, category and has_otp. By default it only matches mail that arrives after the request starts, so an old message cannot satisfy it. The server checks once a second, so the added delay is at most about a second, and the whole wait counts as a single request against the rate limit. /otp works the same way but returns only the code, looking back five minutes by default. If your code runs behind a proxy with an idle timeout shorter than the wait, use a shorter timeout and loop.

Option 4: Server-sent events

GET /v1/inboxes/:id/stream keeps one connection open for up to 900 seconds (default 300) and pushes an email event for each new message, with heartbeat events in between and a final done event at the timeout. The server checks every two seconds. Use it for a process that watches one inbox continuously, such as a dashboard or a long-running agent loop; for "wait for this one email," /wait is simpler.

Option 5: Webhooks

With a webhook, Lumbox calls you. POST /v1/webhooks with a URL registers it (the default event is email.received) and returns a whsec_ secret once. Each delivery is a JSON body with event, timestamp and data; for new mail, data carries email_id, inbox_id, from, subject, category, otp_codes, a summary and the spam fields, but not the body, so you fetch GET /v1/emails/{email_id} when you need it.

The X-Lumbox-Signature header is a hex HMAC-SHA256 of the raw body using the secret. The timestamp sits inside the signed body, so you can also reject old deliveries:

import hashlib, hmac, json, os, time
from datetime import datetime

SECRET = os.environ["LUMBOX_WEBHOOK_SECRET"].encode()  # the whole whsec_... string

def verify(raw_body: bytes, signature: str) -> dict:
    expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, signature or ""):
        raise ValueError("bad signature")
    event = json.loads(raw_body)
    sent = datetime.fromisoformat(event["timestamp"].replace("Z", "+00:00"))
    if abs(time.time() - sent.timestamp()) > 300:
        raise ValueError("stale delivery")
    return event

Three details matter in production. Webhooks belong to the account, so one endpoint receives events for every inbox and your handler routes on inbox_id. Each delivery gets 10 seconds to respond, so acknowledge fast and do the work in a queue. Failed deliveries are recorded in GET /v1/webhooks/{id}/deliveries but are not retried today, so a consumer that was down should catch up by listing emails with since set to the last one it processed.

Choosing

SituationUse
A script or agent waiting for one specific emailLong-poll (/wait or /otp)
A service handling mail for many inboxesWebhooks, plus a since query to catch up after downtime
One process watching one inbox continuouslyServer-sent events
A periodic batch jobREST polling
A person reading an agent's mail in a mail clientIMAP

Most agent code ends up with two of these: long-poll inside the task that expects an email, and a webhook or a periodic list for everything that arrives unannounced. The long-poll post covers timeout handling in more depth, webhooks versus polling covers the queueing side, and the IMAP docs have client setup for Apple Mail, Outlook and Thunderbird.