Read sign-up codes and magic links from email
When an agent signs up for a service, signs in or resets a password, the service emails it a code or a link. mails.ai reads both by fixed rules rather than a model, so the same email always gives the same answer, on every plan.
Wait for the email
Note the time, have the agent submit the form that sends the email, then wait for a message from that service. Name the sender with from, say what you need with has, and pass the time you noted as since, so a code from an earlier attempt never answers.
GET /Wait for a received message. Needs thev1/ messages/ received/ wait readscope.
import { createClient } from "@mailsai/sdk";
const client = createClient(); // reads MAILS_API_KEY
const askedAt = new Date().toISOString();
// ...the agent fills in the sign-up form with its address...
const { message } = await client.received.wait({
from: "github.com",
has: "code",
since: askedAt,
timeout: 50,
});
const code = message?.codes?.[0]?.value;
if (!code) throw new Error("No code arrived in 50 seconds");
console.log(code);from datetime import datetime, timezone
from mailsai import Client
client = Client()
asked_at = datetime.now(timezone.utc)
# ...the agent fills in the sign-up form with its address...
result = client.wait_for_received(
from_="github.com", has="code", since=asked_at, timeout=50
)
if result["timed_out"]:
raise RuntimeError("No code arrived in 50 seconds")
print(result["message"]["codes"][0]["value"])curl -G "https://api.mails.ai/v1/messages/received/wait" \
-H "Authorization: Bearer $MAILS_API_KEY" \
--data-urlencode "from=github.com" \
--data-urlencode "has=code" \
--data-urlencode "since=2026-10-12T09:30:00Z" \
--data-urlencode "timeout=50"mails receiving wait --from github.com --has code \
--since 2026-10-12T09:30:00Z --timeout 50The answer is the newest message that matches, as soon as one arrives. When none arrives within timeout seconds (0 to 50, default 30), message is null and timed_out is true. That is not an error, so a slow service only needs a second wait. A timeout of 0 looks once and answers at once.
since is compared with the time mails.ai received the message, so take it from a clock you trust, or a few seconds early. Without it, the wait looks back five minutes.
Codes
A code is read from the subject and the text, and from the HTML when the text has none. It counts only beside a word that names it, such as code, verification, one-time, passcode or OTP, so an order number, a price, a date or a phone number in the same email is not mistaken for one. The most likely code comes first.
| Field | Description |
|---|---|
codes | object[] Required. Verification codes in the email (sign-up, sign-in, one-time and confirmation codes), most likely first, at most 5. Read by rule from the subject, the text and, when the text has none, the HTML: a code is 4 to 8 digits, a prefixed one such as G-123456 (its digits), 6 to 9 capitals and digits mixed, or 6 to 10 letters and digits straight after code is or code:, and counts only beside a word that names it (code, verification, one-time, passcode, OTP and the like), so an order number, a price, a date or a phone number in the same email is not one. Empty when there is none. |
codes[].value | string Required. The code as it is typed: digits only when it was digits (123 456 gives 123456), otherwise as written. |
Links
Each http(s) address in the email is listed once, with a kind read from the words on the link and from its address. Links that confirm, sign in or reset come first, and unsubscribe links come last, so the first verify or sign_in link is usually the one to open. The words on a link are not returned, because they are the sender’s HTML; kind says what they meant.
| Field | Description |
|---|---|
links | object[] Required. The email's http(s) links, one entry per address: verify, sign_in and reset_password first, unsubscribe last, at most 25. The words on a link are not returned (they are the sender's HTML); kind says what they meant. |
links[].url | string Required. The address, as the email had it (a tracking link stays one). |
links[].kind | string Required. What the link is for, from the words on it and in its address: verify (confirm an email address or an account), sign_in (a magic link), reset_password, unsubscribe, or other. One of: verify, sign_in, reset_password, unsubscribe, other. |
import { createClient } from "@mailsai/sdk";
const client = createClient();
const { message } = await client.received.wait({
from: "example.com",
has: "link",
});
const link = message?.links?.find(
(l) => l.kind === "sign_in" || l.kind === "verify"
);
if (link) console.log(link.url);A link is returned as the email had it. When the sender tracks clicks, the address is the tracking link, which leads on to the real page when it is opened.
Only the email you asked for
from takes an address, in any case, or a domain. A domain also matches its subdomains, so github.com matches noreply@mail.github.com, and never a look-alike such as github.com.evil.example. Mail judged forged (auth_failed) and quarantined mail never answer a wait.
dmarc is pass. Before a code unlocks something that matters, check that field. Forged mail is only marked when the sender’s domain has a DMARC policy of quarantine or reject, so mail claiming any other domain can be imitated.A key tied to one agent reads only that agent’s mail. With a workspace key, pass agent_id to wait for one agent’s mail. A workspace can start 60 waits a minute; past that the API answers 429.
On every received message
The wait is a shortcut: codes and links are on every received message the API returns, from GET /v1/messages/received and GET /v1/messages/received/{id} too. The list takes the same from and since filters. Webhook and stream events do not carry the two fields, so fetch the message an event names to read them.
From an MCP client
Through the MCP server, the same wait is the mails_wait_for_email tool, so an agent in Claude, Cursor or any other MCP client can sign up for something and finish the verification on its own. A call looks like this:
{
"name": "mails_wait_for_email",
"arguments": {
"from": "github.com",
"has": "code",
"timeout": 50
}
}Try it with a test key
With a mk_test_ key, POST /v1/test/inbound delivers a made-up email to one of your agents, which a wait then answers with:
curl -X POST https://api.mails.ai/v1/test/inbound \
-H "Authorization: Bearer $MAILS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent": "hello",
"from": "noreply@example.com",
"subject": "Your verification code",
"body_text": "Your verification code is 482913."
}'
mails receiving wait --from example.com --has code --timeout 0Next steps
Was this page helpful?