apumail / blog

Email and SMS verification for AI agents

The agent doesn't care whether the code arrived over SMTP or over a carrier network. It cares that it's six digits and it has ninety seconds.

July 14, 2026

Verification splits roughly down the middle. Developer tools and SaaS mostly send an email code. Anything touching money, identity or a phone-shaped account sends an SMS. An agent that can only do one of them stalls on half the internet.

The usual answer is two vendors: an email API and an SMS API. That's two auth models, two response shapes, two polling strategies, two failure modes, and a pile of glue whose only job is to make them look the same to the agent. The glue is the problem — it's the part nobody wants to own and everybody has to write.

Same shape, both channels

apumail runs both through one layer. The email flow:

# provision an inbox — no auth
INBOX=$(curl -sS -X POST https://api.apumail.com/api/v1/inboxes)
ADDR=$(echo "$INBOX" | jq -r .address)
TOK=$(echo "$INBOX" | jq -r .token)

# block until it lands, read the extracted code
curl -sS -H "Authorization: Bearer $TOK" \
  "https://api.apumail.com/api/v1/inbox/$ADDR/wait?timeout=120" \
  | jq -r '.messages[0].otp'

And the SMS flow:

# buy a real dedicated number, on demand
PHONE=$(curl -sS -X POST https://api.apumail.com/api/v1/phones \
  -H "Authorization: Bearer $TOK" \
  -d '{"country_code":"US"}')

# same wait, same extract, different channel

Provision, wait, extract. Same verbs, same auth, same JSON. In MCP the symmetry is literal — wait_for_mail / extract_latest_otp next to wait_for_sms / extract_latest_sms_otp. An agent that learned one learned the other.

On-demand numbers

The thing that makes SMS-for-agents awkward elsewhere is acquisition. Most flows assume a human: pick a number from a pool, wait for availability, email support for a new region. One POST /api/v1/phones with a country_code buys a real dedicated number then and there — no pool to wait on, no human to email. It's the agent's number for as long as it's paid for, not a shared one someone else's code lands in.

That's also the honest tradeoff: it's a paid tier. A number is $18/yr ($14.40 in $PROWL) and sending costs $0.07/SMS from prepaid credit. Email inboxes are free; phone numbers cost money because carriers charge money. If you only need email codes, you never touch this and you never pay.

Sending, not just receiving

Both channels are two-way, which matters more than it sounds. Verification isn't always a code — sometimes it's a reply-to-confirm, and sometimes the flow you're automating continues over the same channel after the code. An agent that can only receive has to hand off to a human at the first reply. send_mail and send_sms keep it in the loop, with the from locked to the agent's own address or number so a leaked token can't be used to impersonate anyone.

Where this leaves you

One vendor for a problem that's usually two isn't a virtue by itself — it's a virtue when the two halves genuinely share a shape, and here they do. Start with the email flow, which is free and takes one POST. Add a number when a flow demands one.

Try it — no signup

One unauthenticated POST returns a working address and its token.

curl -sS -X POST https://api.apumail.com/api/v1/inboxes