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.