MainSDK
Search⌘K
How-to

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.

Building for 0.7

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
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).
  • 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.

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.

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.

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.

main.yaml
verify:
  driver: local
  identity: { in: digilocker, default: local }
  require: { in: substantial }
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.

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet