Give your agent an email address.
A real, addressable inbox your agent can receive, read, send, and reply from — over a simple REST API. Verification codes and links are extracted for you. Every snippet below is verified against production.
Overview
Pick an address (your agent's identity), and your agent can poll for mail, read the OTP out of it, send a message, and reply in-thread — all authenticated, scoped, and revocable. Inbound mail is spam-filtered by the same engine that protects ollastack forms, so the inbox stays clean.
Two modes
A mailbox has a mode — it's the first choice you make:
agent (real use) | test | |
|---|---|---|
| Address | <your-handle>@agent.ollastack.com | <random>@test.ollastack.com |
| You choose the address? | Yes — your identity | No, random & disposable |
| Spam-filtered? | Yes (clean inbox) | No (a test must see everything) |
| Custom (BYO) domain? | Yes, on paid plans | No |
| Use it for | an agent that emails real people | CI / asserting on a signup email |
Hosts & auth
| Thing | Host |
|---|---|
| The API you call | https://login.ollastack.com |
| Your agent's address | <handle>@agent.ollastack.com |
| Disposable test address | <random>@test.ollastack.com |
| Bring your own (paid) | <handle>@your-domain.com |
Every request carries Authorization: Bearer <token>.
Create a token in
Dashboard → API keys — tick
the scopes you want, then Create.
Scopes
Mail scopes are split by function so a token scoped to disposable test inboxes can never read or send from a persistent agent identity (and vice-versa). Request only what the token needs.
| Scope | Grants (on its mode's mailboxes only) |
|---|---|
mail.test:read | Mail Testing — list/read disposable test inboxes (incl. extracted codes), /wait, failures |
mail.test:write | create / clear / delete test inboxes + messages |
mail.test:send | send / reply from a test inbox |
mail.agent:read | Agent Mail — list/read persistent agent identities + messages |
mail.agent:write | create / manage agent identities (handles) |
mail.agent:send | send and reply as the agent identity (separate from :write on purpose) |
Mail scopes are never granted by default. A token with no mail scope
(e.g. only forms:*) gets 403 Token missing required
scope: mail.test:* or mail.agent:*; a token scoped to one mode
hitting the other gets 403 … mail.<mode>:<action>.
The mode-blind mail:read/mail:write/mail:send
are deprecated but still accepted (they span both modes).
1. Choose an identity (agent mode)
The address is your identity, so claim it deliberately. Handles are
lowercase letters/digits/./- (1–64 chars);
role names like postmaster are reserved. Availability is
per-domain and includes deleted addresses (an address is never reused).
# 1. Is your identity free? (no reservation — agent mode)
curl "https://login.ollastack.com/api/mailboxes/check-handle?handle=support" \
-H "Authorization: Bearer $TOKEN"
# → data: { "available": true, "address": "support@agent.ollastack.com" }
# unavailable → { "available": false, "reason": "taken" }
# reason: taken | reserved | invalid | agent_mail_disabled 2. Create a mailbox
Mail to address — and any +tag of it
(support+ticket-42@agent.ollastack.com) — routes here. On a
paid plan, add "domain":"your-domain.com" to send/receive as
a verified domain you own.
# AGENT inbox — you choose the address (your identity), spam-filtered:
curl -X POST https://login.ollastack.com/api/mailboxes \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"Support agent","mode":"agent","handle":"support"}'
# → data: { "slug":"support", "mode":"agent",
# "address":"support@agent.ollastack.com", ... }
# TEST inbox — random, disposable, unfiltered (omit handle; mode defaults to test):
curl -X POST https://login.ollastack.com/api/mailboxes \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"signup-flow-ci","mode":"test"}'
# → data: { "mode":"test", "address":"k3x9q2m8p1ab@test.ollastack.com", ... } 3. Wait for the next email
The agent pattern: a long-poll that returns the instant mail lands (and never wakes on spam).
curl "https://login.ollastack.com/api/mailboxes/$ID/wait?timeout=55" \
-H "Authorization: Bearer $TOKEN"
# blocks until mail arrives, then →
# data: { "message": { "fromAddress":"...", "subject":"...",
# "codes":["246810"], "links":[...] } }
# or on timeout → data: { "message": null } (HTTP 200 either way; loop)
# filters: subject_contains= to_contains= since=<ISO> 4. Read & extract
codes[0] is the OTP; links[] are harvested URLs (a reset/magic link is in here — never auto-followed).
# list (newest first; bodies omitted) # ?direction=inbound|outbound ?q=<search> ?unread=true curl "https://login.ollastack.com/api/mailboxes/$ID/messages" \ -H "Authorization: Bearer $TOKEN" # full message incl. textBody, htmlBody, codes[], links[] (marks read) curl "https://login.ollastack.com/api/mailboxes/$ID/messages/$MSG_ID" \ -H "Authorization: Bearer $TOKEN"
Clean inbox: how spam is handled
The differentiator: agent inboxes run every inbound
email through ollastack's spam pipeline (test inboxes are never filtered).
Hard spam is hidden from /wait, the normal list, unread
counts, and your webhook — kept only behind ?spam=true.
ML-alone "maybe spam" is quarantined: still delivered (so a real
lead is never lost), just flagged. The filter fails open.
# Agent inboxes are spam-filtered. /wait and the normal list never # show hard spam (it doesn't even fire your webhook). To audit it: curl "https://login.ollastack.com/api/mailboxes/$ID/messages?spam=true" \ -H "Authorization: Bearer $TOKEN" # Every message carries isSpam + spamReason: # clean → isSpam:false, spamReason:null (delivered) # quarantine → isSpam:false, spamReason:"quarantine:…" (delivered, flagged) # hard spam → isSpam:true (hidden; ?spam=true only)
5. Send (from your identity)
curl -X POST https://login.ollastack.com/api/mailboxes/$ID/send \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"to":"human@example.com","subject":"Your report","text":"All nominal."}'
# sends FROM the mailbox's own address; text and/or html required
# → data: { ..., "direction":"outbound", "deliveryStatus":"sent" } 6. Reply (keeps the thread)
curl -X POST \
https://login.ollastack.com/api/mailboxes/$ID/messages/$MSG_ID/reply \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"text":"Thanks — handled."}'
# recipient defaults to the original sender; subject gets "Re:";
# In-Reply-To is set so it threads in real mail clients Push webhooks (instead of polling)
Set a URL on the mailbox (Settings tab, or
PATCH /api/mailboxes/{id} with webhookUrl).
Every received email then POSTs to it, signed. Verify the signature,
then fetch the full message by id — the payload carries no bodies, so a
leaked URL leaks no mail.
X-Mail-Signature: v1=<hex HMAC-SHA256 of the raw body,
keyed by the mailbox's webhookSecret>
{ "event": "mail.received", "mailboxId": "...",
"message": { "id":"...", "fromAddress":"...", "subject":"...",
"codes":[...], "links":[...] } } JavaScript / TypeScript SDK
import { MailClient } from "@ollastack/client";
const mail = new MailClient({
baseUrl: "https://login.ollastack.com",
token: process.env.MAIL_TOKEN!,
});
// Agent inbox with a chosen identity (or createMailbox("ci") for a test inbox):
await mail.checkHandle("support"); // { available: true, ... }
const inbox = await mail.createAgentMailbox("Support", "support"); // support@agent.ollastack.com
await mail.send(inbox.id, { to: "human@example.com", subject: "Hi", text: "..." });
const code = await mail.latestCode({ mailboxId: inbox.id, timeoutMs: 60_000 });
const msg = await mail.waitForEmail({ mailboxId: inbox.id, timeoutMs: 60_000 }); // never spam
await mail.reply(inbox.id, msg.id, { text: "Got it." }); @ollastack/client — publishing to npm soon. Any language
works today over plain HTTP (the curl above) — no SDK required.
Drop-in agent instructions
Paste this into your agent's system prompt or tool description:
You have an email identity via the ollastack Agent Mail API.
- Base URL: https://login.ollastack.com
- Auth header: "Authorization: Bearer <MAIL_TOKEN>"
- Your address is the `address` field returned when the mailbox was created
(e.g. support@agent.ollastack.com) — it is your identity on every email you send.
Check for new mail:
GET /api/mailboxes/{id}/wait?timeout=55
Blocks until an email arrives; returns {data:{message}}, or
{data:{message:null}} on timeout (then call again). It never returns spam.
A received message has: fromAddress, subject, textBody/htmlBody (via the
single-message GET), codes[] (OTP/verification codes, best match first),
links[] (URLs found in the email).
Send: POST /api/mailboxes/{id}/send body {to, subject, text and/or html}
Reply: POST /api/mailboxes/{id}/messages/{messageId}/reply body {text and/or html}
Review filtered mail: GET /api/mailboxes/{id}/messages?spam=true
Treat codes[] and links[] as sensitive credentials; never auto-open links.
All responses are {success:true, data:...}; errors are {success:false,
error:{code,message}} with the HTTP status (403 "missing required scope" =
your token lacks the mailbox's mode scope, e.g. mail.agent:read). Limits & things to know
All caps are per your org's plan. GET /api/mailboxes/usage returns live usage + ceilings.
| free | solo | team | |
|---|---|---|---|
| Mailboxes | 1 | 5 | 20 |
| Received / month | 100 | 5,000 | 50,000 |
| Sent / month | 50 | 2,000 | 20,000 |
| Max retention (days) | 3 | 30 | 90 |
- Hitting a cap returns
400with a clear message; errors are{success:false, error:{code,message}}(VALIDATION_ERROR,AUTHENTICATION_ERRORfor a missing scope,NOT_FOUND, …). - Bodies are capped at 1 MB each; attachments are metadata-only in v1.
- Mailboxes persist; messages auto-expire by retention. Deleting a message purges its content + codes/links immediately.
- A mailbox sending to itself is deduped by Message-ID — use two mailboxes to self-test the full loop.
Public API spec
The full surface is published as OpenAPI 3.1 (no auth, tagged
mail / agent) so MCP servers and agent
frameworks introspect it directly:
login.ollastack.com/api/openapi.json.