Search the docs

Find a page, a section, or an endpoint.

Verification codes

Waiting for a sign-up email and reading the code out of it.

Your agent fills in a sign-up form with an address it owns, and the service emails a code or a confirmation link. This is how it gets that back: one request that waits for the mail, with the code already pulled out of it.

Wait for the message

GET /v1/messages/await holds the request open until a message arrives at the address, or until timeout seconds pass — 50 by default, at most 280. Make the call, then submit the form; the answer comes back when the mail does.

curl -G https://pidgeon.ai/api/v1/messages/await \
-H "Authorization: Bearer $PIDGEON_API_KEY" \
-d identity_id=$IDENTITY_ID \
-d has_code=true \
-d timeout=120

The answer is { message, timed_out, cursor, held_back }. When nothing came in time, message is null and timed_out is true — call again with after: cursor and nothing that arrives in between is missed.

A code that beat you to the call is still found. Without after, the wait first looks back two minutes and returns the newest matching message from then, so submitting the form and then waiting is the right order. Narrow it with from or has_code so an earlier email is not taken for the one you want, and pass after: now to wait only for mail that has not arrived yet.

Check held_back before deciding the code never came. A sender whose SPF and DMARC fail is filed as spam, and the wait does not return spam unless you pass include_spam: true. Instead it counts the matching message in held_back.spam and gives its id in held_back.message_ids, so you can read it with GET /v1/messages/{id}.

Narrow what counts

ParameterWaits for
identity_idThe address. Required, and the key has to be able to read it.
fromAn exact sender, or @example.com for anybody at that domain.
subject_containsA subject containing these words, ignoring case.
has_codetrue: only a message with a code or a verification link in it. Skips the welcome.
afterOnly mail that arrived after this cursor.
include_spamtrue: also a matching message filed as spam. Off by default.

What is extracted

Every message — here, on GET /v1/messages/{id} and in a list — carries extracted:

  • codes — candidates, most likely first, each with value and kind (numeric or alphanumeric). Spaces are removed, so 123 456 comes back as 123456.
  • links — verification, confirmation and sign-in links first, then the rest. Unsubscribe, preference and social links are left out.

It is a list rather than one answer because finding a code in an email is a heuristic: an order number or a phone number can sit right above it. Take the first; if the service refuses it, the second is right there, and the message itself is in the same response for a person to look at.

Codes are read from the new text of the message, never from quoted history, so a code from an earlier email in the thread is not offered again.

From an agent

The MCP server has await_message, with the same parameters. A model that has just submitted a sign-up form calls it and reads extracted.codes[0].value from the answer.

Limits

Thirty waits a minute per workspace. A held request counts once however long it waits; a caller looping on timed_out is the one that runs into it.