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
Create a mailbox, get an @ollastack.com address, and your
agent can poll for mail, read the OTP code out of it, send a message,
and reply in-thread — all authenticated, scoped, and revocable.
Hosts & auth
| Thing | Host |
|---|---|
| The API you call | https://login.form4dev.com |
| The email addresses you get | <slug>@ollastack.com |
Every request carries Authorization: Bearer <token>.
Create a token in
Dashboard → API keys — tick
the scopes you want, then Create.
Scopes
| Scope | Grants |
|---|---|
mail:read | list mailboxes, read messages (incl. extracted codes), /wait, failures |
mail:write | create / update / delete mailboxes, delete messages |
mail:send | send and reply from a mailbox (separate on purpose) |
Mail scopes are never granted by default. A token with only
forms:* scopes gets
401 Token missing required scope: mail:read.
1. Create a mailbox
Mail to address — and any +tag of it — routes here.
curl -X POST https://login.form4dev.com/api/mailboxes \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"support-agent"}'
# → data: { "id":"...", "slug":"k3x9q2m8p1ab",
# "address":"[email protected]", ... } 2. Wait for the next email
The agent pattern: a long-poll that returns the instant mail lands.
curl "https://login.form4dev.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> 3. 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.form4dev.com/api/mailboxes/$ID/messages" \ -H "Authorization: Bearer $TOKEN" # full message incl. textBody, htmlBody, codes[], links[] (marks read) curl "https://login.form4dev.com/api/mailboxes/$ID/messages/$MSG_ID" \ -H "Authorization: Bearer $TOKEN"
4. Send
curl -X POST https://login.form4dev.com/api/mailboxes/$ID/send \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"to":"[email protected]","subject":"Your report","text":"All nominal."}'
# sends FROM the mailbox's own address; text and/or html required
# → data: { ..., "direction":"outbound", "deliveryStatus":"sent" } 5. Reply (keeps the thread)
curl -X POST \
https://login.form4dev.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 "@form4dev/client";
const mail = new MailClient({
baseUrl: "https://login.form4dev.com",
token: process.env.MAIL_TOKEN!,
});
const inbox = await mail.createMailbox("support-agent"); // inbox.address
await mail.send(inbox.id, { to: "[email protected]", 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 });
await mail.reply(inbox.id, msg.id, { text: "Got it." }); @form4dev/client — git/workspace install today; npm publish
pending. Any language works over plain HTTP (the curl above).
Drop-in agent instructions
Paste this into your agent's system prompt or tool description:
You have an email mailbox via the form4dev Agent Mail API.
- Base URL: https://login.form4dev.com
- Auth header: "Authorization: Bearer <MAIL_TOKEN>"
- Your address is the `address` field returned when the mailbox is created
(…@ollastack.com).
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).
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}
Treat codes[] and links[] as sensitive credentials; never auto-open links.
All responses are {success:true, data:...}; errors are {error:{code,message}}. Limits & things to know
- Receive is near-unlimited; send has a monthly per-plan cap (free 50 / solo 2,000 / team 20,000).
- Bodies are capped at 1 MB each; attachments are metadata-only in v1.
- Mailboxes persist; messages auto-expire by retention (default 30 days). Deleting a message purges it 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.form4dev.com/api/openapi.json.