---
title: "Confirm a phone, an email or an identity"
description: "Confirm a phone number with a six-digit code, an email address with a link, and a person's identity at the level a market requires, with no answer that tells who has an account."
section: "guides"
status: "Building for 0.7"
spec: "verify"
canonical: "https://mainsdk.dev/docs/guides/use-verify"
updated: "2026-10-28"
---

# Confirm a phone, an email or an identity

> Confirm a phone number with a six-digit code, an email address with a link, and a person's identity at the level a market requires, with no answer that tells who has an account.

Status: Building for 0.7 · Canonical: https://mainsdk.dev/docs/guides/use-verify · Updated: 2026-10-28

You will add `verify` to your app, send a phone code and check it, confirm an email address, and ask an identity question for one market. It takes about 15 minutes, and everything runs on the local driver with no provider account.

## You need

- A MainSDK app with a `main.yaml` (see [Thirty-minute path](https://mainsdk.dev/docs/getting-started/thirty-minutes)).
- For the email step, `email` set up and an origin for the link: `links.base` or `verify.email.base`.

## 1. Declare `verify`

Start on the local driver. Codes stay in an outbox that tests read, so nothing is sent.

```yaml title="main.yaml"
verify:
  driver: local
  email: { base: https://app.example.com }
```

## 2. Send a phone code and check it

`verify.phone()` normalises the number, makes a six-digit code and answers `{ sent: true, id, expiresAt }`, the same for every number. `verify.phone.check()` answers `{ ok: true, number }` once, and `{ ok: false }` for a wrong, expired or used code.

```ts title="app/account/phone.ts"
import { verify } from "mainsdk/verify";

export async function confirm(number: string, typed: (id: string) => Promise<string>) {
  const started = await verify.phone({ number });
  const check = await verify.phone.check({ id: started.id, code: await typed(started.id) });
  return check.ok ? check.number : null;
}
```

Pass the incoming `req` to `verify.phone({ number, req })` so the bot-defence door and the per-IP limit see the caller.

## 3. Confirm an email address

`verify.email()` sends a link through `email`. Your route at `email.path` reads the token and calls `verify.email.confirm(token)`, ideally on a POST.

```ts title="app/verify/email/route.ts"
import { verify } from "mainsdk/verify";

export async function POST(req: Request) {
  const { token } = (await req.json()) as { token: string };
  return Response.json({ confirmed: (await verify.email.confirm(token)).ok });
}
```

## 4. Ask an identity question for one market

Name the driver for each market and the level it requires, then ask for the claims you need. `age_over_18` is enough to sell age-restricted goods.

```yaml title="main.yaml"
verify:
  driver: local
  identity: { in: digilocker, default: local }
  require: { in: substantial }
```

```ts title="app/kyc.ts"
import { verify } from "mainsdk/verify";

export async function ageCheckUrl(user: string) {
  const started = await verify.identity({
    level: "substantial",
    market: "in",
    claims: ["age_over_18"],
    purpose: "Sell age-restricted items",
    user,
    consent: { policy: "privacy-2026-10" },
  });
  return started.url;
}
```

## 5. Go live

Set `driver` to `twilio-verify` or `bird` and give it `secret:` credentials under `drivers`. Set `verify.key` so every instance can check a code another sent. Run one send in the provider's sandbox before launch.

## Check it worked

Run `npx mainsdk check`. It passes when every market in `verify.require` is served by a driver that reaches the level it requires; otherwise it reports `MAIN-VERIFY-0004` with the line.
