Skip to main content

Read sign-up codes and magic links from email

Every received message lists the codes and links in it, and one call waits for the email to arrive.

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.

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);

The 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.

FieldDescription
codesobject[] 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[].valuestring Required. The code as it is typed: digits only when it was digits (123 456 gives 123456), otherwise as written.

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.

FieldDescription
linksobject[] 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[].urlstring Required. The address, as the email had it (a tracking link stays one).
links[].kindstring 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.

The From address is proven only when the message’s 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 0

Next steps

Was this page helpful?