How to Receive Emails via API: Webhooks, IMAP, and Polling
Your application needs to react when an email arrives. Maybe it's parsing verification codes in an automated test. Maybe it's routing customer emails into a support ticket system. Maybe it's processing inbound data from a partner that communicates via email.
Whatever the use case, "receive email programmatically" boils down to three approaches: IMAP polling, webhooks, and API polling. Each has different tradeoffs in latency, complexity, and infrastructure requirements.
Approach 1: IMAP Polling
IMAP is the standard protocol for reading email from a server. You connect to the IMAP server, search for messages, and download them. Every email client — Thunderbird, Outlook, Apple Mail — uses IMAP under the hood.
For programmatic access, you do the same thing in code: connect, search, fetch, parse.
How It Works
- Open an IMAP connection to the mail server
- Select the inbox (or a specific folder)
- Search for messages matching your criteria (unseen, subject contains X, from Y)
- Fetch the matching messages
- Parse the content (headers, body, attachments)
- Repeat on an interval
Python Example
import imaplib
import email
import time
from email.header import decode_header
def poll_inbox(host, port, user, password, interval=5):
"""Continuously poll for new emails via IMAP."""
imap = imaplib.IMAP4_SSL(host, port)
imap.login(user, password)
while True:
imap.select("INBOX")
status, messages = imap.search(None, "UNSEEN")
for msg_id in messages[0].split():
status, msg_data = imap.fetch(msg_id, "(RFC822)")
raw = msg_data[0][1]
msg = email.message_from_bytes(raw)
subject = decode_header(msg["Subject"])[0][0]
if isinstance(subject, bytes):
subject = subject.decode()
sender = msg.get("From")
print(f"New email from {sender}: {subject}")
# Process the email here — extract codes, route to app logic, etc.
time.sleep(interval)
# Connect to a Reusable.Email managed inbox
poll_inbox(
host="imap.reusable.email",
port=993,
user="your-inbox@reusable.email",
password="your-password",
interval=10,
)
Pros and Cons
Pros:
- Standard protocol — works with any mail server, any language
- No public endpoint required (your code initiates the connection)
- Full access to all messages, folders, and metadata
- Works with every Reusable.Email managed inbox ($3 one-time)
Cons:
- Polling adds latency (seconds to minutes depending on interval)
- Persistent connections need management (timeouts, reconnects)
- Scaling to many inboxes requires connection pooling
IMAP IDLE: Reducing Latency
Most IMAP servers support the IDLE command, which holds the connection open and pushes notifications when new mail arrives. This reduces latency from your polling interval to near-real-time.
# Using imapclient for IDLE support
from imapclient import IMAPClient
client = IMAPClient("imap.reusable.email", ssl=True)
client.login("your-inbox@reusable.email", "your-password")
client.select_folder("INBOX")
# Start IDLE mode — server will notify us of new messages
client.idle()
responses = client.idle_check(timeout=300) # Wait up to 5 minutes
client.idle_done()
# Process any new messages indicated by responses
Approach 2: Webhooks
With webhooks, the email server pushes an HTTP request to your endpoint when a new message arrives. Your application doesn't poll — it listens.
How It Works
- Register a webhook URL with the email provider
- When email arrives, the provider sends an HTTP POST to your URL
- The POST body contains the message data (sender, subject, body, attachments)
- Your endpoint processes the data and returns a 200 response
- If your endpoint is down, the provider retries (with backoff)
Architecture
Email arrives → Reusable.Email → HTTP POST → Your endpoint → Application logic
This is event-driven. No polling loop, no persistent connections, no wasted cycles checking empty inboxes.
Considerations
You need a public endpoint. Your webhook URL must be reachable from the internet. In development, tools like ngrok can expose a local server. In production, this is your regular API infrastructure.
Handle retries. If your endpoint returns a non-200 response, the provider will retry. Your processing logic should be idempotent — processing the same email twice shouldn't cause problems.
Security. Validate that webhook requests actually come from your email provider. Check signatures, verify source IPs, or use webhook secrets.
When to Use Webhooks
Webhooks are the right approach when:
- You need real-time processing (no polling delay)
- You're building a product where email is a core input (support tickets, inbound parsing)
- You're already running a web server that can receive HTTP requests
- You're on Reusable.Email's whitelabel tier, which supports webhooks for email events
The whitelabel tier ($30/month) includes webhook support, a REST API, and unlimited managed inboxes. For the full breakdown, see On-Demand SMTP & IMAP Credentials.
Approach 3: API Polling
Some email providers expose a REST API for checking inboxes. Instead of connecting via IMAP, you make HTTP GET requests to an API endpoint that returns messages as JSON.
How It Works
- Make an HTTP request to the provider's API (e.g.,
GET /inboxes/{id}/messages) - Parse the JSON response
- Process new messages
- Repeat on an interval
Pros and Cons
Pros:
- Simple HTTP calls — no IMAP library needed
- JSON responses are easy to parse
- Works well with modern web frameworks and serverless functions
Cons:
- Still polling (same latency tradeoffs as IMAP polling)
- Provider-specific API — vendor lock-in
- Rate limits on API calls
- Not a standard protocol
When to Use API Polling
API polling works well for lightweight integrations where adding an IMAP library feels heavyweight — serverless functions, simple scripts, or prototypes. For anything production-grade, IMAP or webhooks are more robust.
Choosing the Right Approach
| Factor | IMAP Polling | Webhooks | API Polling |
|---|---|---|---|
| Latency | Seconds (configurable) | Real-time | Seconds (configurable) |
| Requires public endpoint | No | Yes | No |
| Standard protocol | Yes (IMAP) | No (HTTP, but universal) | No (provider-specific) |
| Works with any mail server | Yes | Provider-dependent | Provider-dependent |
| Complexity | Medium | Low (once set up) | Low |
| Best for | Testing, batch processing | Real-time products | Simple scripts |
How Reusable.Email Fits
All managed inboxes ($3 one-time) support IMAP. Connect to imap.reusable.email:993 with SSL/TLS, use any IMAP library, and read emails programmatically. This works for testing, CI/CD pipelines, and applications that can tolerate polling latency.
The whitelabel tier ($30/month) adds webhooks. When email arrives at any inbox under your domain, Reusable.Email sends an HTTP POST to your endpoint with the full message data. This is the approach for products that need real-time email processing.
Both approaches use the same underlying inboxes. You can start with IMAP polling during development and upgrade to webhooks when you need real-time delivery.
Common Patterns
Wait for Email in a Test
def wait_for_email(imap, subject_match, timeout=30):
start = time.time()
while time.time() - start < timeout:
imap.select("INBOX")
_, msgs = imap.search(None, "UNSEEN")
for mid in msgs[0].split():
_, data = imap.fetch(mid, "(RFC822)")
msg = email.message_from_bytes(data[0][1])
if subject_match in msg["Subject"]:
return msg
time.sleep(2)
raise TimeoutError(f"No email matching '{subject_match}'")
Extract a Verification Code
import re
def extract_code(msg):
body = msg.get_payload(decode=True).decode()
match = re.search(r"\b(\d{4,8})\b", body)
return match.group(1) if match else None
Route Email to Application Logic
# Webhook handler (Flask example)
@app.route("/webhook/email", methods=["POST"])
def handle_email():
data = request.json
sender = data["from"]
subject = data["subject"]
body = data["text"]
if "support" in data["to"]:
create_support_ticket(sender, subject, body)
elif "billing" in data["to"]:
process_billing_email(sender, subject, body)
return "", 200
Error Handling and Reliability
Whichever approach you choose, email reception needs to handle failure gracefully.
IMAP connection drops. Network interruptions will kill your IMAP connection. Wrap your polling loop in a reconnect handler that re-authenticates and resumes from where it left off. The UNSEEN flag on messages ensures you don't re-process emails you've already handled.
Webhook endpoint downtime. If your webhook endpoint is unreachable, the email provider will retry with exponential backoff. Design your handler to be idempotent — processing the same email twice should produce the same result, not duplicate entries in your database.
Malformed emails. Not every email follows RFC standards perfectly. Your parsing code should handle missing headers, invalid encoding, and unexpected MIME structures without crashing. Use try/except blocks around email parsing and log failures for investigation.
Rate limits. IMAP servers may limit the number of concurrent connections or operations per minute. For high-volume scenarios (many inboxes, frequent polling), use connection pooling and respect server limits.
# Robust IMAP polling with reconnection
def robust_poll(host, port, user, password, callback, interval=10):
while True:
try:
imap = imaplib.IMAP4_SSL(host, port)
imap.login(user, password)
while True:
imap.select("INBOX")
_, msgs = imap.search(None, "UNSEEN")
for mid in msgs[0].split():
try:
_, data = imap.fetch(mid, "(RFC822)")
msg = email.message_from_bytes(data[0][1])
callback(msg)
except Exception as e:
print(f"Error processing message {mid}: {e}")
time.sleep(interval)
except (imaplib.IMAP4.error, ConnectionError, OSError) as e:
print(f"Connection lost: {e}. Reconnecting...")
time.sleep(5)
What's Next
For a complete guide to email in development — testing, staging, per-user inboxes, and building email products — see Email API for Developers. For the specific use case of building test environments, read How to Build an Email Testing Environment with a Disposable Email API.