NEW AI agents now first-class: authorize · audit · revoke in one click — your agents submit cleanly, bots stay blocked. Read agent docs →
agent mail

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 identityNo, random & disposable
Spam-filtered?Yes (clean inbox)No (a test must see everything)
Custom (BYO) domain?Yes, on paid plansNo
Use it foran agent that emails real peopleCI / asserting on a signup email

Hosts & auth

ThingHost
The API you callhttps://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.

ScopeGrants (on its mode's mailboxes only)
mail.test:readMail Testing — list/read disposable test inboxes (incl. extracted codes), /wait, failures
mail.test:writecreate / clear / delete test inboxes + messages
mail.test:sendsend / reply from a test inbox
mail.agent:readAgent Mail — list/read persistent agent identities + messages
mail.agent:writecreate / manage agent identities (handles)
mail.agent:sendsend 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.

freesoloteam
Mailboxes1520
Received / month1005,00050,000
Sent / month502,00020,000
Max retention (days)33090
  • Hitting a cap returns 400 with a clear message; errors are {success:false, error:{code,message}} (VALIDATION_ERROR, AUTHENTICATION_ERROR for 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.