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

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

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

ScopeGrants
mail:readlist mailboxes, read messages (incl. extracted codes), /wait, failures
mail:writecreate / update / delete mailboxes, delete messages
mail:sendsend 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.