<!-- section: Auth API · status: building · source: docs/auth-api/04-verify-email.md -->

> Confirm an address, and send the link again when the first one never arrives.


# Verify email

**Most apps never need this page.** An account is created by reading a six-digit
code we email, so the address is already confirmed the moment the account exists
— there is no pending state to resolve. See [sign up](/docs/auth-api/sign-up).

What is here is the link-based flow, which still runs for two cases: accounts
created before codes existed, and an address that changes later. Both end at the
same endpoint.

## The mail

It goes out under **your project's name** rather than ours — in the subject line
and in the From column, which is the half people actually read:

```
From: "Acme" <noreply@xplatform.dev>
Subject: Confirm your email for Acme
```

The code mail is the same in that respect, and one thing more: **the digits are
never in the subject line.** A subject is repeated on lock screens and
notification shades, and a code readable without unlocking the mailbox is a code
that did not need the mailbox.

The sending address stays ours, because it is the domain verified with our mail
provider. Only the name is yours. Someone signing up for your app has never heard
of us, and their inbox should not be where they find out.

## GET /v1/auth/verify-email

The link in the message. The person's browser follows it; you never call this
yourself.

```
$BASE/v1/auth/verify-email?token=<token>&callbackURL=<where-to-land>
```

It marks the address verified and redirects to `callbackURL`, which must be an
origin you registered. A link that could land anywhere is an open redirect with
a session attached.

The token is **signed, not stored** — there is no row to expire and no table to
clean up. It is good for 24 hours.

## POST /v1/auth/send-verification-email

Send another one. Takes a **publishable** key.

```bash Request
curl -X POST "$BASE/v1/auth/send-verification-email" \
  -H "authorization: Bearer $PK" \
  -H "content-type: application/json" \
  -d '{"email":"ada@example.com","callbackURL":"https://yourapp.com/welcome"}'
```

```json Response
{ "status": true }
```

**The answer is the same whether the address is unknown, already verified, or
rate limited.** Anything else makes this a way to test who has an account with
you. It means a `true` here is not a promise that a mail was sent.

Build a resend into your sign-in screen. The people who need it are the ones
whose first mail never arrived — and if your app offers social login, they are
the ones who will otherwise be stuck, because a social sign-in sends nothing.

## Why an unverified address can block social login

Signing in with Google links that provider to an existing account with the same
address. That link is refused while **your** record of the address is
unverified, and the refusal is `account_not_linked`.

The reasoning is worth knowing before you meet it: without that rule, anyone
could register someone else's address with a password, never confirm it, and be
handed the account the first time the real owner used Google. Verifying the
address is what proves which of them owns it.

## Errors

| Status | Code | Means |
|---|---|---|
| `400` | `INVALID_TOKEN` | The link is malformed, expired, or already used |
| `403` | `INVALID_REDIRECT_URL` | `callbackURL` is not a registered origin |
| `429` | — | Rate limited; see [limits](/docs/auth-api/limits) |
