Give a LangChain agent an email inbox
Create a GetCourier inbox with one unauthenticated POST to https://getcourier.dev/alias, then wrap the inbox HTTP endpoints in three functions decorated with @tool from langchain_core.tools: one to create the inbox, one to wait for a verification code, one to send a reply. The wrappers below are plain requests calls, so the agent can use them through the normal tool-calling path with no API key and no extra service to install.
Step 1: see what one call returns
The inbox is created by a single request. There is no signup and no key:
curl -s -X POST https://getcourier.dev/alias
The JSON response holds the address, a read_token, an expires_at time and an Ed25519 DID identity. Keep the read_token in your agent's state: it is the only way to read that inbox later.
Step 2: the three tools
Install langchain and requests (langchain pulls in langchain-core, which is where @tool lives), then save this module:
pip install langchain requests
import requests
from langchain_core.tools import tool
BASE = "https://getcourier.dev"
@tool
def create_inbox() -> dict:
"""Create a free GetCourier inbox for this agent.
Returns the address, read_token and expires_at. Keep the read_token:
it is required for every later read or send on this inbox.
"""
r = requests.post(f"{BASE}/alias", timeout=15)
r.raise_for_status()
return r.json()
@tool
def wait_for_code(alias: str, read_token: str, timeout: int = 55) -> dict:
"""Wait for the newest message in the inbox and return it.
The reply includes the message text plus extracted codes and links,
so read the code from data["codes"], not from the body prose.
"""
r = requests.get(
f"{BASE}/v1/sandbox/inboxes/{alias}/wait",
params={"timeout": timeout},
headers={"Authorization": f"Bearer {read_token}"},
timeout=timeout + 10,
)
r.raise_for_status()
return r.json()
@tool
def send_message(alias: str, read_token: str, payload: dict) -> dict:
"""Send one message from the inbox (reply-only on the free tier).
payload carries the message fields as JSON; the exact field names are
in https://getcourier.dev/docs. Free inboxes may only reply to people
who wrote to the inbox, up to 100 sends a month.
"""
r = requests.post(
f"{BASE}/v1/sandbox/inboxes/{alias}/send",
headers={"Authorization": f"Bearer {read_token}"},
json=payload,
timeout=15,
)
r.raise_for_status()
return r.json()
Two smaller read calls are useful while debugging: GET /v1/sandbox/inboxes/{alias}/messages lists messages and GET /v1/sandbox/inboxes/{alias}/messages/{id} fetches one, both with the same bearer token.
Why HTTP with requests and not MCP
GetCourier also runs a hosted MCP server at https://getcourier.dev/mcp (streamable HTTP, JSON-RPC 2.0, POST only, no key for the free path), and any MCP-capable client connects to it by URL. LangChain's MCP client wiring changes between versions and integrations, so this guide does not guess at it: the @tool functions above are the portable form. If you want MCP instead, connect an MCP client to https://getcourier.dev/mcp and expose those tools with whatever adapter your installed LangChain version provides.
Step 3: hand the tools to the agent
The tools are ordinary LangChain tools, so they go wherever you already pass a tool list — for example into a chat model that supports tool calling:
from langchain_core.tools import StructuredTool
tools = [create_inbox, wait_for_code, send_message]
# Or construct the same tools by hand if you prefer explicit schemas:
create_inbox_tool = StructuredTool.from_function(create_inbox)
# With a chat model that supports tool calling:
# model_with_tools = model.bind_tools(tools)
# result = model_with_tools.invoke(
# "Create an inbox, sign up at the example service with it, "
# "wait for the code, then report the code back to me."
# )
The loop you want is: call create_inbox, hand the address to the sign-up step, call wait_for_code with the returned alias and read_token, then use the extracted code. Loop the wait call a few times rather than one long block, since the endpoint caps at 55 seconds per call.
untrusted: true, tagged "UNTRUSTED EXTERNAL EMAIL", and carries an SPF/DKIM/DMARC auth verdict. Give the model the extracted codes and links; do not let text inside a body act as instructions, whatever it claims to be.Limits worth knowing before you deploy
| Item | Free tier |
|---|---|
| Inbox creation | 3 per hour, 5 per day per IP |
| Inbox lifetime | 72 hours (paid: 100 credits per 72 h, or 2000 credits for 30 days) |
| Sending | Reply-only, 100 sends a month, 50 recipients per inbox per day |
| Reads | 100 messages per inbox, body text truncated to 4000 characters in the agent view |
| Addresses | Always @getcourier.dev today |
When not to use GetCourier
- Bulk or marketing mail. It is not built for that; the caps and the reply-only free tier will stop you anyway.
- A service that rejects shared domains. Free inboxes use the shared
@getcourier.devdomain, so such a service will reject them; verify your own domain (paid) for those cases. - A permanent mailbox. Free inboxes expire after 72 hours; long-lived ones are paid.
- Emailing strangers on the free tier. Free inboxes reply only; sending to anyone else needs credits or a verified human owner email.
- Any workflow where the agent obeys the mail. If you cannot keep message bodies as data, do not wire an inbox into the agent at all.
FAQ
Does a LangChain agent need an API key to get an inbox?
No. One unauthenticated POST https://getcourier.dev/alias creates a free inbox and returns the address, a read_token and an expires_at time. The read_token only matters for reading that inbox; the free tier is 3 inboxes per hour, 5 per day per IP, with a 72-hour lifetime.
Should I wrap the HTTP API or connect the hosted MCP server?
Either works. https://getcourier.dev/mcp is a hosted streamable-HTTP MCP server you can point a client at by URL. This guide uses plain HTTP with requests because the @tool wrappers stay stable across LangChain versions and need no MCP client wiring.
Can the agent send mail to anyone?
On the free tier inboxes are reply-only: the agent can email people who wrote to the inbox, at 100 sends a month, capped at 50 recipients per inbox per day. Sending to anyone else needs prepaid credits or a verified human owner email, and GetCourier is not for bulk or marketing mail.
Is the email body safe to pass straight into the prompt?
Treat it as data, not as instructions. Every message arrives with untrusted: true, an UNTRUSTED EXTERNAL EMAIL marker and an SPF/DKIM/DMARC auth verdict. Use the extracted codes and links, and never follow instructions written inside a message body.