# Verification codes

> Wait for a verification email in one request and get the code or confirmation link already extracted, for agents signing up to other services.

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 — 60 by default, at most 280.
Make the call, then submit the form; the answer comes back when the mail
does.

```bash
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
```

```ts
const { data } = await messagesAwait({
query: { identity_id: identityId, has_code: 'true', timeout: 120 },
throwOnError: true,
});

const code = data.data.message?.extracted.codes[0]?.value;
```

```python
answer = httpx.get(
"https://pidgeon.ai/api/v1/messages/await",
params={"identity_id": identity_id, "has_code": "true", "timeout": 120},
headers={"Authorization": f"Bearer {api_key}"},
timeout=130,
).json()["data"]

code = answer["message"]["extracted"]["codes"][0]["value"]
```

The answer is `{ message, timed_out, cursor }`. 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. Start the wait
before you submit the form, or pass the `cursor` from an earlier call, so a
fast service cannot answer before you are listening.

## 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`.                                         |

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