An agent needs its own address the moment it has to sign up for something, confirm an account, or hold a conversation with a person. The agent framework barely matters here: anything that can call a tool can make an HTTP request, and giving an agent an inbox takes two or three of them. This guide goes from nothing to an agent receiving its first code, then covers sending, cleanup and custom domains, with the real request and response at each step.

Step 1: Get an API key

A person can create a key in the Lumbox dashboard under Settings, API keys. An agent can also get one with no person involved: POST /v1/orgs needs no authentication, takes a name, and returns a new organization on the Free plan with its first key.

curl -X POST https://api.lumbox.co/v1/orgs \
  -H "Content-Type: application/json" \
  -d '{"name": "research-agents"}'

{ "id": "org_...", "name": "research-agents", "api_key": "ak_...",
  "message": "...", "created_at": "..." }

The key is shown once, so store it before doing anything else. This endpoint allows 3 new organizations per hour from one IP address. npx lumbox signup does the same from a terminal. Every later request authenticates with either X-API-Key: ak_... or Authorization: Bearer ak_....

Step 2: Create the inbox

curl -X POST https://api.lumbox.co/v1/inboxes \
  -H "X-API-Key: $LUMBOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "research-agent-01", "metadata": {"owner": "research-team"}}'

{
  "id": "inb_...",
  "address": "research-agent-01@trylumbox.com",
  "name": "research-agent-01",
  "status": "ACTIVE",
  "created_at": "...",
  "next": "GET /v1/inboxes/inb_.../emails"
}

The address is built from local_part if you pass one, otherwise from name, lowercased, with any character outside letters, digits and hyphens turned into a hyphen. Pass neither and Lumbox generates one. The default domain is trylumbox.com. When IMAP is enabled on the server, the response also includes imap_host, imap_port and a per-inbox imap_password for tools that only speak IMAP.

Four responses tell the agent to change something. A reserved name such as support, admin, billing or noreply on the shared domain returns 400. A name that looks like another company's brand returns 403 with code: "IMPERSONATION_BLOCKED". An address that already exists returns 409. Going over the plan's inbox count returns 402 with code: "PLAN_LIMIT_EXCEEDED", limit and current.

The next field only appears when Lumbox seeded a welcome message into the inbox, which it does for an organization's first inbox and no other. That message comes from hello@lumbox.co, already parsed, so the first list call has something to show. It is categorized as welcome and carries no codes, so it never answers a wait for a verification email.

Step 3: Hand the address to the agent

The agent needs two values: the address, which it types into sign-up forms or gives to people, and the id, which it uses in API calls. Put both in the system prompt, the task input, or a tool's default arguments, whichever your framework uses.

If your agent speaks MCP (Claude Code, Claude Desktop, Cursor), the Lumbox MCP server gives it the calls as tools. claude mcp add --transport http lumbox https://mcp.lumbox.co/mcp connects the hosted server, which supports OAuth sign-in; the stdio version is npx -y @lumbox/mcp-server with LUMBOX_API_KEY set. It exposes more than 80 tools, including create_inbox, get_otp and wait_for_email. The MCP docs have the config for each client.

For LangChain in Python, the lumbox package includes a helper. pip install "lumbox[langchain]", then create_langchain_tools(api_key=...) from lumbox returns a list of LangChain StructuredTools: create_inbox, wait_for_email, get_otp, list_emails, read_email, search_emails, add_domain and verify_domain. Pass that list to your agent like any other tools. For every other framework, wrap the two calls in Step 4 as tools in whatever form the framework expects.

Step 4: Receive mail

For a sign-up code, /otp is the shortest path. It holds the request open until a verification email arrives or the timeout passes (default 30 seconds, maximum 120):

curl "https://api.lumbox.co/v1/inboxes/inb_.../otp?from=github.com&timeout=90" \
  -H "X-API-Key: $LUMBOX_API_KEY"

{ "code": "481902", "all_codes": ["481902"], "from": "noreply@github.com",
  "subject": "...", "expires": null, "email_id": "eml_..." }

It looks back five minutes by default, so a code that arrived just before the call still counts. In an inbox reused across runs, pass since (an ISO timestamp taken just before the sign-up) so a code from an earlier run is not returned. When nothing arrives in time it returns HTTP 408 with {"error": "No OTP found", "timeout": 90}, and the agent can call again or report back. Codes are pulled out by regular expressions over the subject and plain-text body, which cover numeric codes of 4 to 8 digits; for a link instead of a code, use /wait below and read parsed.verification_links.

For any other mail, /wait works the same way but returns the whole parsed email, and takes from, subject, category and has_otp=true as filters. It only returns mail that arrived after the request started unless you pass an earlier since. If you would rather be pushed than wait, POST /v1/webhooks with a url subscribes to email.received, signed with HMAC-SHA256 in X-Lumbox-Signature. Lumbox does not retry a webhook your endpoint failed to accept, so keep an occasional list call as a fallback.

Step 5: Send from the address (optional)

curl -X POST https://api.lumbox.co/v1/inboxes/inb_.../send \
  -H "X-API-Key: $LUMBOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to": "someone@example.com", "subject": "Following up", "text": "...", "dry_run": true}'

With dry_run: true the API validates everything and returns what it would send without sending it; drop the flag to send for real. The Free plan allows 3 sends per minute and 100 per month, and a new Free account without a verified domain is held to 5 sends in its first hour and 25 in its first day. On the Free plan, each recipient's domain is also checked before sending (MX records, common typos, disposable domains), and undeliverable ones come back as 403 with code: "INVALID_RECIPIENT" and a list of the bad addresses. To answer a message, use /reply with its email_id so the threading headers are set.

Step 6: Clean up

Inboxes do not expire. An inbox stays, and keeps counting against the plan (3 on Free, 10 on Starter), until DELETE /v1/inboxes/:id, which also deletes its email. For a one-off task, delete the inbox when the task ends. For an agent that registered accounts it has to keep, keep the inbox: those services send password resets to it.

Using your own domain

To have the agent send and receive as agent@yourcompany.com, add the domain with POST /v1/domains and a domain field. The response lists the DNS records to create: a lumbox-verify TXT record plus the MX, SPF and DKIM records for mail. Create them at your DNS host (or, for a Cloudflare zone, call POST /v1/domains/:id/auto-setup with a Cloudflare API token), then call POST /v1/domains/:id/verify. The domain becomes ACTIVE once the verification TXT, MX and SPF records resolve, and after that POST /v1/inboxes accepts "domain": "yourcompany.com". Custom domains for agent email goes further into that setup.

The quickstart runs through Steps 2 and 4 with the TypeScript and Python SDKs, and the agent self-signup docs cover Step 1 when no person owns the key.