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

# Start a session

> Mint a session on your server with your secret key.

With your check configured on the dashboard, mint a session for it on your
server and return the `session_id` to your frontend. There is no browser path
to a session; your secret key, and the decision to spend a scan, stay server
side.

```ts theme={null}
const VY_SK = process.env.VY_SK; // sk_test_… while building, sk_live_… to ship

const res = await fetch("https://trust.verifyyou.com/v3/initialize", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${VY_SK}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    verification_id: "<verification_id>",
    // where they land when done
    redirect_url: "http://localhost:3000/verified",
    // optionally set uniqueness to be per group, so the same person can pass once in each group
    // group: "spring-intake",
    // optionally return to redirect_url the moment the check settles, with no result screen
    // auto_return: true,
    // optionally link with your database
    // external_id: user.id,
    // link with a new person or external identifier
    // email: user.email,
  }),
});

if (!res.ok) {
  const { detail } = await res.json();
  // 400 one_identifier_only               send email OR phone, never both
  // 400 identifier_unrecognized           that email/phone isn't a shape we parse
  // 400 invalid_redirect_url              must be absolute, and https off localhost
  // 400 verification_requires_identifier  this check is set to "Provided by you"
  // 401 / 403                             wrong key, or a test-only field on a live key
  // 404 no_matching_verification          that verification_id isn't on your account
  // 409 link_not_active                   this check runs on invites, not on code
  throw new Error(`verifyyou initialize: ${res.status} ${detail}`);
}

// => { session_id: "…", url: "…" }
// use the session_id to run the verification on the frontend;
// a session's ttl is reasonable but not infinite
const { session_id } = await res.json();
```

A session carries at most one identifier, tying the run to whatever you
already use for the account:

| You have | Send | You get back |
| - | - | - |
| An existing account | `external_id` | Duplicates reported against your id |
| A new signup | `email` or `phone` | The run is bound to that identity; the person cannot swap it |
| Nothing (anonymous) | just `verification_id` | A session, and a pass with no identity attached to it |

Where the run carries an identifier, `metadata` takes what else you know about
that person — a plan, a signup date, a department code:

```js theme={null}
metadata: { plan: "pro", signup_date: "2026-01-14", seats: 12 }
```

The fields are kept against that person and show up on their profile in the
dashboard, beside the columns an uploaded list carries. They never affect the
result: nothing the person walks through reads them, and a check settles
exactly as it would without them. Send up to 50 keys, keys 1 to 64 characters
and string values 1 to 1024, as strings, numbers or booleans; an empty value
is refused rather than quietly dropped. An anonymous session has nobody to
attach them to, so one carrying `metadata` comes back
`400 metadata_requires_identifier`.

## Groups

Uniqueness covers the whole verification: once someone passes, every later run
through that check reads as the same person, and a repeat attempt reports as a
duplicate. Sometimes that is too broad. If one check gates several distinct
things, separate surveys or product drops for instance, send a `group` to
scope uniqueness to each one. The same person can then pass once in
`spring-intake` and once in `fall-intake`, but never twice in either.

Sometimes one pool needs to see another. To keep everyone who already passed
`spring-intake` out of a follow-up, mint the follow-up's sessions with its own
`group` plus `include_groups: ["spring-intake"]`. History in an included group
counts against the run exactly as its own would: whether a hit denies the
person or only flags the pass follows the verification's settings, and the
confirmation's `groups` field names the pools the hit came from. The run still
settles into its own group. An included pool is read, never written.

## Anonymous checks

A verification configured as **Anonymous** sends people straight to the check,
with no identifier step. It fits flows where nobody returns, and it carries
trade offs:

* After a person's token expires, you have no identifier to look them up by.
* Reusing the verification requires a [group](#groups). Without one, any
  repeat attempt through it is blocked.

Anonymous does not mean client side. The session is still minted on your
server, for two reasons: a browser alone cannot flood your account with
scans, and you choose when a check runs at all, so a request your own signals
flag as low quality never spends one. A scan is spent deliberately.

<Warning>
  Even with no identifier on the run, an anonymous verification meters
  **one successful pass per person, for life**. If your second test run comes back
  denied, this is why. Send a [`group`](#groups) to reuse the verification, and
  the pass counts once per group instead.
</Warning>

For full parameters and errors: [`POST /v3/initialize`](/v3/dev/spec/server-api/initialize).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.