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=120The 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
| Parameter | Waits for |
|---|---|
identity_id | The address. Required, and the key has to be able to read it. |
from | An exact sender, or @example.com for anybody at that domain. |
subject_contains | A subject containing these words, ignoring case. |
has_code | true: only a message with a code or a verification link in it. Skips the welcome. |
after | Only mail that arrived after this cursor. |
include_spam | true: 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 withvalueandkind(numericoralphanumeric). Spaces are removed, so123 456comes back as123456.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.