Email is one of the last “human-first” interfaces most software teams still have to automate. If you are building signup verification flows, QA tests, or LLM agents that need to complete real-world tasks, you eventually need to get email via API, reliably, and in a format that doesn’t require brittle HTML scraping.
A programmable inbox API solves that by turning inbound email into structured JSON, then delivering it to your code via webhooks (push) and/or polling (pull). This article explains the core design, what a practical JSON contract should look like, and how to wire it into automation that is deterministic and safe.
If you are specifically integrating with Mailhook, the canonical machine-readable integration reference is the project’s llms.txt: Mailhook API contract (llms.txt).
What “get email via API” actually means (and why JSON is the point)
When developers say “get email,” they often mean one of these:
- Fetch a message from a mailbox (IMAP/POP), then parse RFC 5322 and MIME yourself.
- Receive a message event (webhook), then store and parse it.
- Query an inbox by an ID, get normalized fields and content for automation.
For automation and agents, the hard parts are rarely SMTP delivery itself. The pain is everything after delivery:
- MIME is multi-part, nested, and full of encoding edge cases.
- Headers can be folded, duplicated, and attacker-controlled.
- Bodies can be HTML-heavy, tracking-laden, or missing
text/plain. - Timing is nondeterministic, you cannot “sleep 5 seconds” and expect reliability.
A good inbox API turns the raw email (defined by RFC 5322 plus MIME, see RFC 2045) into a stable JSON representation, so your automation can assert on fields, not fragile rendering.
The automation-friendly model: inboxes and messages, not long-lived accounts
The key shift is treating an inbox like a short-lived resource, closer to a message queue than a personal mailbox:
- You create an inbox programmatically.
- You get a routable email address for that inbox.
- Your system sends an email to that address.
- You retrieve messages as JSON (webhook or polling).
- You optionally rotate or expire the inbox to keep isolation tight.
This “inbox-first” approach is what makes parallel CI runs, multi-agent toolchains, and retry-heavy verification flows predictable.

What should the JSON look like? A practical contract for QA and LLM agents
Different providers expose different schemas, but robust automation tends to need the same conceptual pieces:
| Email concept | Why you need it | JSON field examples (illustrative) |
|---|---|---|
| Stable message identifier | Deduplication, idempotency |
message_id, provider_id, rfc822_message_id
|
| Arrival timestamp | Time budgets, debugging | received_at |
| Envelope addresses | Routing and assertions |
from, to, cc, reply_to
|
| Subject | Matching and correlation | subject |
| Headers (normalized) | Debugging, correlation, auth signals |
headers (map or list, normalized) |
Text content (prefer text/plain) |
Safe parsing for agents |
text, text_plain, body_text
|
| HTML content (optional, treat carefully) | Human debugging, fallbacks |
html, body_html
|
| Attachments metadata | Security, artifact extraction |
attachments[] with filename, content_type, size
|
| Raw source (optional but valuable) | Forensics when parsers disagree |
raw or source
|
Two practical recommendations for agent and test environments:
-
Prefer
text/plainfor assertions and extraction. HTML is useful for humans, but it is a large, attacker-friendly surface for automation. - Keep a raw source available for debugging. When something breaks in CI, being able to inspect the exact delivered content can save hours.
Mailhook’s product positioning is explicitly centered on this: create disposable inboxes via API and receive emails as structured JSON, built for LLM agents and QA automation. For exact field names and payload formats, use the canonical reference: mailhook.co/llms.txt.
Retrieving messages: webhook-first, polling fallback
There are two common ways to “get email via API,” and most production setups use both.
Webhooks (push)
With a webhook, the inbox provider calls your endpoint when a new message arrives. This is ideal when you want fast, event-driven workflows.
Strengths:
- Low latency, no polling loops
- Natural event stream model
- Scales well when many inboxes are active
Operational requirements:
- You must implement retries and idempotency
- You must verify authenticity (signatures)
- You need an endpoint reachable from the public internet (or a secure tunnel)
Mailhook supports real-time webhook notifications and signed payloads for security (verify signatures before trusting content).
Polling (pull)
With polling, your code asks the provider for messages in an inbox until a match arrives or a timeout is reached.
Strengths:
- Simple to reason about in test runners
- Works even if your webhook endpoint is down
- Good fallback path for reliability
Costs:
- Higher latency (depending on interval)
- More API calls
Mailhook includes a polling API for emails, which is a practical complement to webhooks.
Quick comparison
| Mechanism | Best for | Reliability gotcha | Default advice |
|---|---|---|---|
| Webhook | Real-time automation, high concurrency | Duplicate deliveries, replay risk | Verify signatures, dedupe by message ID |
| Polling | CI tests, fallback paths | Fixed sleeps cause flakes | Poll with a timeout and backoff, match narrowly |
Deterministic retrieval: match the right message, not “the latest email”
Most flakiness comes from ambiguous matching. If you only do “fetch latest message,” then retries, parallel runs, and delayed delivery will burn you.
A deterministic strategy usually includes:
- Inbox isolation: one inbox per run, per attempt, or per agent session.
-
Correlation token: include a
run_idor nonce in a place you can match. - Narrow matchers: filter by expected sender, expected subject prefix, or a unique token.
- Explicit time budget: a single function that waits until a deadline.
For correlation, the most robust option is adding your own header such as X-Correlation-Id (when you control the sender). If you cannot, use a subject token or a unique link parameter.
A minimal “wait for email” interface (agent-friendly)
Whether you are building a test harness or an LLM tool, the cleanest abstraction is a small, deterministic API surface:
-
create_inbox()→ returns{ inbox_id, address } -
wait_for_message(inbox_id, matcher, timeout_ms)→ returns a single message JSON -
extract_verification_artifact(message)→ returns{ otp }or{ url }
This keeps the LLM or test runner from dealing with mailbox search semantics. It also reduces prompt injection risk because the agent does not need “the whole inbox history,” only a minimal artifact.
Example: retrieve an OTP from JSON (pseudocode)
The code below is intentionally provider-agnostic. It illustrates the control flow and safety checks without assuming exact Mailhook endpoints or field names.
Polling-based retrieval
import re
import time
def wait_for_message(fetch_messages, inbox_id, timeout_s=60, poll_interval_s=1.5):
deadline = time.time() + timeout_s
seen_ids = set()
while time.time() < deadline:
messages = fetch_messages(inbox_id) # returns a list of message JSON objects
for msg in messages:
msg_id = msg.get("message_id") or msg.get("id")
if msg_id and msg_id in seen_ids:
continue
if msg_id:
seen_ids.add(msg_id)
subject = (msg.get("subject") or "").lower()
sender = (msg.get("from") or "").lower()
# Narrow matcher example
if "verify" in subject and "no-reply" in sender:
return msg
time.sleep(poll_interval_s)
raise TimeoutError("No matching email arrived before timeout")
def extract_otp(message_json):
text = message_json.get("text") or message_json.get("text_plain") or ""
m = re.search(r"\b(\d{6})\b", text)
if not m:
raise ValueError("OTP not found in text body")
return m.group(1)
Webhook-based retrieval (high level)
In webhook mode, you invert the flow:
- Your webhook endpoint receives message JSON.
- You verify the signature.
- You write the message to storage keyed by
inbox_id. - Your test runner or agent waits on that storage (or an internal queue) for a matching event.
Mailhook supports signed payloads, so verification should be a first-class step. For the exact signature scheme and headers, consult: Mailhook llms.txt.
Security and safety: treat email as untrusted input (especially with LLMs)
If an LLM agent is reading emails, you must assume the content can be hostile. Even verification emails can contain unexpected content, forwarded threads, or malicious links if an attacker can trigger messages to your inbox.
Practical guardrails:
- Verify webhook signatures and reject unsigned or invalid payloads.
-
Do not render HTML in agent pipelines. Prefer
text/plainplus strict extraction. - Whitelist what you extract. For magic links, validate the hostname and path against expected patterns before following them.
- Minimize retention and logs. Emails often contain secrets, tokens, or PII.
- Constrain the tool output. Give the agent only the OTP or a validated URL, not the full raw email.
These controls are not just security hygiene, they improve reliability because your pipeline becomes deterministic and less sensitive to template drift.
Where Mailhook fits: disposable inboxes + JSON output + webhooks
If you are implementing “get email via API” for agents or CI, Mailhook is designed around the primitives you typically end up building yourself:
- Disposable inbox creation via API
- Emails delivered as structured JSON
- Real-time webhooks (with signed payloads)
- Polling API as a fallback path
- Shared domains for quick starts and custom domain support for tighter control
- Batch email processing for higher-throughput workflows
If you want the exact request/response formats (and you are building tools for LLMs), start with the canonical reference: https://mailhook.co/llms.txt, then connect it to your test harness or agent tools.
A reliability checklist before you ship
Before relying on email retrieval in automation, make sure you can answer “yes” to these:
- You create a fresh inbox per run, attempt, or session.
- Your wait logic is deadline-based, no fixed sleeps.
- You dedupe by message ID (or an equivalent stable identifier).
- Your matching rules are narrow and correlation-friendly.
- You verify webhook signatures (if using webhooks).
- You extract minimal artifacts (OTP, validated URL) rather than feeding whole emails into an agent.
If you implement these, “get email via API” becomes a predictable building block instead of the flakiest step in your pipeline.
To explore Mailhook’s programmable inboxes and JSON email retrieval, see Mailhook and keep the integration contract handy: Mailhook llms.txt.