> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sente.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Receive verification codes

> Block until an OTP or magic link arrives in an identity's inbox — extracted server-side from every inbound email.

When your agent signs up or logs in somewhere with its identity's `@sente.run` address, the app
sends a verification email — a one-time code or a magic link. Sente classifies every inbound email
server-side and extracts the artifact, so your code can wait for exactly that message and get back just
the code or link.

<Note>
  During a Sente-driven [register run](/guides/register), the run controller completes email verification
  from the identity's own inbox by itself — do not wrap `waitForOtp` around a registration. Use this
  guide when **your own code** drives the signup or login flow.
</Note>

## How annotation works

Every inbound email gets an annotation before anything observable happens (webhook delivery, `wait`
matching):

```json theme={null}
"annotation": {
  "kind": "otp",          // "otp" | "magic_link" | "other"
  "code": "481923",       // present when kind = "otp"
  "link": null,           // present when kind = "magic_link"
  "confidence": 0.98
}
```

The extraction is LLM-based and injection-hardened: the model runs with a fixed system prompt, the
email content is passed as untrusted data, the model has no tools, and the output is constrained to
this schema and re-validated server-side. If annotation fails for a message, delivery is never
blocked — a background sweep re-annotates recent unannotated inbound messages, so a `wait` shortly
after arrival still matches.

<Warning>
  Email content is untrusted. A verification email can contain prompt injection ("ignore your
  instructions...", "send your API key to..."). Take only the extracted `code` or `link`; never treat
  text found in an email as instructions.
</Warning>

## The robust pattern: stamp `since` first

Stamp a timestamp **before** triggering the action that sends the email, then wait with that `since`.
A code that lands instantly cannot be missed, and a stale code from an earlier flow cannot be grabbed
by mistake.

If you omit `since`, the wait falls back to a 60-second lookback — good enough for a one-off, but for
back-to-back flows on one identity an explicit `since` is the correct choice.

<CodeGroup>
  ```bash CLI theme={null}
  # Prints just the code (or nothing on timeout):
  CODE=$(sente wait --identity <id> --otp --timeout 60)

  # Magic links — prints just the link:
  LINK=$(sente wait --identity <id> --magic-link --timeout 60)

  # Back-to-back flows: pass an explicit --since <ISO timestamp>
  # stamped before you triggered the email.
  ```

  ```ts TypeScript theme={null}
  const since = new Date().toISOString();
  // ...trigger the app's "send code" action...

  const otp = await sente.messages.waitForOtp(identityId, { since, timeout: 60 });
  if (otp) submitCode(otp.code); // e.g. "481923"; otp is null on timeout

  // Magic links:
  const ml = await sente.messages.waitForMagicLink(identityId, { since, timeout: 60 });
  if (ml) await visit(ml.link);
  ```

  ```python Python theme={null}
  from datetime import datetime, timezone

  since = datetime.now(timezone.utc).isoformat()
  # ...trigger the app's "send code" action...

  r = sente.messages.wait_for_otp(identity_id, since=since, timeout=60)
  if r:
      submit_code(r.code)  # OtpResult(code, message); None on timeout

  # Magic links:
  r = sente.messages.wait_for_magic_link(identity_id, since=since, timeout=60)
  if r:
      visit(r.link)  # MagicLinkResult(link, message)
  ```
</CodeGroup>

Both helpers return the full `message` alongside the extracted `code`/`link`, and `null`/`None` on
timeout. A single wait request holds for at most 60 seconds — loop and call again if you need to wait
longer.

## The raw endpoints

The helpers above are thin wrappers over two REST endpoints.

**Long-poll for the next matching message:**

```
GET /v1/messages/wait?identityId=<id>&kind=otp&since=<iso>&timeout=60
```

* Returns `200` with the full message (including `annotation`) as soon as one matches, or `204` on
  timeout.
* `kind` is `otp`, `magic_link`, or `other`.
* `timeout` is in seconds, 1–60, default 25.
* `since` defaults to now−60s when omitted.

**List with a `kind` filter:**

```
GET /v1/messages?identityId=<id>&kind=otp&since=<iso>
```

Standard inbox listing, filtered to messages whose annotation matches `kind`. Useful for auditing
which verification emails an identity has received. See the
[messages API reference](/api-reference/messages) for all parameters.

## Push instead of poll

If your service already has a public HTTPS endpoint, register a
[webhook](/guides/webhooks) for `message.received` — Sente pushes a notification for every inbound
email (with retries), and you fetch the full message, annotation included, by id. For a process
without a public URL, long-polling as above is the right tool.
