An agent that runs more than once needs to remember what happened last time: which services it signed up for, who it talked to, what it promised, which messages it already dealt with. Most memory designs start with a vector store and a summarizer. An agent with its own inbox already has a large part of that record, written by the rest of the world as a side effect of the agent doing its job, and it costs nothing extra to keep.
This post covers what an inbox records on its own, the four ways to read it back through the Lumbox API, how to write to it, and the places where it is a poor memory.
What the inbox records without extra work
Every message that arrives is stored with its sender, recipients, subject, text, a received_at timestamp, a thread_id when it answers something the inbox already holds, and fields parsed on arrival: otp_codes, verification_links, category and code_expiry. Mail the agent sends through the API is stored in the same inbox with category outbound, so both sides of a conversation are in one place.
Nothing in it expires. Lumbox has no automatic retention job, so messages stay until you delete them or delete the inbox, and deleting the inbox deletes its messages. For an agent that signs up for services, that means "which accounts do I have?" can be answered from the verification emails alone, months later.
Recall by exact fields
The cheapest and most predictable recall is a filtered list. GET /v1/inboxes/:id/emails takes from (a substring of the sender address), since, category, label and unread=true, returns up to 100 messages per page newest first, and pages with a cursor.
curl "https://api.lumbox.co/v1/inboxes/inb_.../emails?category=verification&since=2026-09-01T00:00:00Z&limit=100" \
-H "X-API-Key: $LUMBOX_API_KEY"
For a word you remember but not where it appeared, GET /v1/emails?q=invoice&inbox_id=inb_... runs a case-insensitive substring match over subject, body text and the parsed summary. It returns 20 matches by default and at most 100, newest first, with no cursor, and it leaves spam out unless you pass include_spam=true. It matches characters, not meaning: q=invoice finds "invoiced" but not "your bill".
Both work on mail the moment it lands, which is why an agent checking whether something has happened yet should use them.
Recall by meaning
For questions like "did anyone complain about the onboarding flow?", Lumbox has semantic search. POST /v1/search takes a query, a limit (default 10, maximum 50), an optional min_score and an optional inbox_id. It embeds the query with @cf/baai/bge-m3 on Cloudflare Workers AI (1,024 dimensions) and ranks stored message vectors by cosine similarity in Postgres with pgvector.
curl -X POST https://api.lumbox.co/v1/search \
-H "X-API-Key: $LUMBOX_API_KEY" -H "Content-Type: application/json" \
-d '{"query": "customer unhappy with onboarding", "inbox_id": "inb_...", "limit": 5}'
{ "ok": true, "query": "customer unhappy with onboarding", "model": "@cf/baai/bge-m3",
"hits": [ { "email_id": "eml_...", "thread_id": "...", "inbox_id": "inb_...",
"subject": "...", "from": "...", "snippet": "...",
"received_at": "...", "score": 0.61 } ] }
Search only sees messages that have an embedding, and today production does not embed mail as it arrives. Embeddings are created in batches. GET /v1/orgs/me/embeddings reports embedded_count and pending_count, and POST /v1/orgs/me/embeddings/backfill embeds the newest pending messages, up to 50 per call; both reject project-scoped keys. If your organization has no embeddings credentials configured, search returns 400 with "Embeddings not configured for this organization".
The same limit applies to POST /v1/inboxes/:id/ask, which retrieves messages the same way and answers a question with your own LLM key (it needs a BYOK AI config). When nothing relevant has been embedded yet, it answers "I couldn't find any matching emails." even if the message is sitting in the inbox. In practice, semantic search is for older history, and the exact filters above are for anything recent.
Recall a conversation
When the memory the agent needs is a whole exchange, GET /v1/threads/:threadId/context?budget=2000 returns the thread as one text block sized for a prompt: newest messages in full, older ones condensed to a stored summary when one exists and dropped when one does not. The response counts how many were condensed and dropped, so the agent can tell when it is working from a partial history. Tokens are estimated at four characters each, so leave some headroom.
Writing memory back
An agent often needs to note something about a message: that it handled it, that it promised a refund, that the sender is a known customer. Labels do this without creating new mail. POST /v1/emails/:id/labels with {"add": ["refund-promised"], "remove": []} attaches up to 50 labels of up to 64 characters each, and the label filter on the list endpoint reads them back.
For small facts that do not belong to one message, such as a cursor, an account id or the date of the last run, the inbox has a metadata object. PATCH /v1/inboxes/:id merges keys into it, a null value deletes a key, and it holds up to 256 keys of up to 1 KB each.
Sending the agent an email to itself as a memory record, which is a common suggestion, works poorly here. Each one counts against the monthly send quota (100 on the Free plan) and the per-minute send limit (3 on Free), goes out through the sending provider and back in through MX, and is then stored twice: once as the outbound copy and once as the received one. Labels and metadata cost none of that.
For reference material that every agent in the organization should be able to query, POST /v1/knowledge takes a title and content, chunks and embeds it when you write it, and POST /v1/knowledge/ask answers from it. It is organization-wide rather than per inbox, and it needs embeddings configured.
Where an inbox is a poor memory
Anyone who knows the address can write to it. A memory built on "everything in my inbox" can be changed by a single email from a stranger, including one written to look like an instruction. Filter by sender before the agent acts on anything it recalls. Text fields arrive wrapped in an untrusted-content fence, and GET /v1/emails/:id/safety returns a prompt-injection score and flags for any single message on demand.
The inbox also never consolidates. If a customer changes their mind three times, all three messages remain, and nothing marks which one is current; the parsed.summary field is a template built from the subject and sender, not a model summary. Keep derived facts (the current decision, the agreed price) in labels, metadata or your own store, and treat the inbox as the evidence behind them.
Retrieval over a thread is covered in more depth in multi-turn conversations via email, and email as a state machine uses inbox metadata as the durable store for a workflow. The list and search parameters are in the email docs.