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

# Headless KYC

> Submit a user's verification data from your server and own the KYC interface

With headless KYC, your app's server submits a user's verification data to Privy, and Privy forwards it to Bridge. No hosted Bridge link is involved, so your app owns the interface end to end and controls how each field is collected.

Use [hosted KYC](/kyc-kyb/kyc) instead when Bridge should collect the data, including document capture and liveness checks.

At a high level, headless KYC involves three steps:

<Steps>
  <Step title="Accept terms of service">
    The user accepts Bridge's terms of service, which produces an agreement ID.
  </Step>

  <Step title="Submit verification data">
    Your app's server submits the user's identity data, along with that agreement ID.
  </Step>

  <Step title="Track status">
    Bridge reviews the submission asynchronously. Your app polls status or consumes webhooks.
  </Step>
</Steps>

Before verifying users, [register a Bridge API key with Privy](/kyc-kyb/setup) and enable server-side flows for the environment the requests target.

## Accept terms of service

Bridge requires a signed terms of service agreement before it will create or update a customer. Generate an acceptance link with [`POST /v1/users/{user_id}/kyc/tos`](/kyc-kyb/kyc-tos), pass it to your frontend, and keep the agreement ID Bridge returns when the user accepts.

Pass that ID as `client_agreement_id` on the submission. Privy stores it, so later submissions for the same user may omit it and Privy replays the stored one.

## Submit verification data

<View title="NodeJS" icon="node-js">
  Use the `submit` method from the `kyc` resource on `users()`.

  ```ts {skip-check} theme={"system"}
  import {PrivyClient} from '@privy-io/node';

  const privy = new PrivyClient({
    appId: process.env.PRIVY_APP_ID!,
    appSecret: process.env.PRIVY_APP_SECRET!
  });

  const status = await privy.users().kyc.submit('did:privy:xxxxx', {
    provider: 'bridge',
    environment: 'sandbox',
    endorsements: ['base'],
    client_agreement_id: '<agreement-id-from-tos-acceptance>',
    data: {
      first_name: 'Alex',
      last_name: 'Doe',
      email: 'user@example.com',
      date_of_birth: '1990-01-31',
      residential_address: {
        street_line_1: '123 Market Street',
        city: 'San Francisco',
        subdivision: 'CA',
        postal_code: '94103',
        country: 'USA'
      },
      identifying_information: [
        {
          type: 'ssn',
          issuing_country: 'USA',
          number: '000-00-0000'
        }
      ]
    }
  });

  // status.kyc.status is Bridge's verification status
  // status.requirements_due lists what Bridge is still waiting on
  ```
</View>

<View title="REST API" icon="terminal">
  To submit KYC data for a user, make a `POST` request to:

  ```bash theme={"system"}
  https://api.privy.io/v1/users/{user_id}/kyc/submit
  ```

  See the [API reference](/api-reference/fiat/kyc-server/submit) for the full request and response schema.

  In the body of the request, include the following fields:

  <ParamField body="provider" type="'bridge'" required>
    Provider to verify the user with.
  </ParamField>

  <ParamField body="environment" type="'production' | 'sandbox'">
    Bridge environment to use. Defaults to `production`.
  </ParamField>

  <ParamField body="endorsements" type="string[]">
    [Endorsements](https://apidocs.bridge.xyz/platform/customers/customers/endorsements) to request
    from Bridge. Each endorsement unlocks a set of rails and regions.
  </ParamField>

  <ParamField body="client_agreement_id" type="string">
    Identifier of the Bridge terms of service agreement the user accepted, between 1 and 1024
    characters. Privy replays the stored agreement ID when this is omitted.
  </ParamField>

  <ParamField body="data" type="object" required>
    Verification data for the user. Must include at least one field.

    <Expandable title="properties">
      <ParamField body="data.first_name" type="string">
        Legal first name.
      </ParamField>

      <ParamField body="data.last_name" type="string">
        Legal last name.
      </ParamField>

      <ParamField body="data.email" type="string">
        Email address for the Bridge customer. On the first submission, Privy falls back to the user's
        linked email when this is omitted. On a later submission, sending a different address replaces
        the one Bridge has on file.
      </ParamField>

      <ParamField body="data.phone" type="string">
        Phone number in E.164 format.
      </ParamField>

      <ParamField body="data.date_of_birth" type="string">
        Date of birth in `YYYY-MM-DD` format.
      </ParamField>

      <ParamField body="data.residential_address" type="object">
        Residential address.

        <Expandable title="properties">
          <ParamField body="data.residential_address.street_line_1" type="string" required>
            Street address line 1. At least 4 characters.
          </ParamField>

          <ParamField body="data.residential_address.street_line_2" type="string">
            Street address line 2.
          </ParamField>

          <ParamField body="data.residential_address.city" type="string" required>
            City.
          </ParamField>

          <ParamField body="data.residential_address.subdivision" type="string">
            ISO 3166-2 state or province code. Required for US addresses.
          </ParamField>

          <ParamField body="data.residential_address.postal_code" type="string">
            Postal code. Required for countries that use them.
          </ParamField>

          <ParamField body="data.residential_address.country" type="string" required>
            ISO 3166-1 alpha-3 country code.
          </ParamField>
        </Expandable>
      </ParamField>

      <ParamField body="data.identifying_information" type="object[]">
        Identifying documents for the user. At least one entry.

        <Expandable title="properties">
          <ParamField body="data.identifying_information[].type" type="string" required>
            Document type identifier, such as `ssn` or `passport`. See the [Bridge customer API
            reference](https://apidocs.bridge.xyz/platform/customers/customers/api) for accepted
            values.
          </ParamField>

          <ParamField body="data.identifying_information[].issuing_country" type="string" required>
            ISO 3166-1 alpha-3 issuing country code.
          </ParamField>

          <ParamField body="data.identifying_information[].number" type="string">
            Document number.
          </ParamField>

          <ParamField body="data.identifying_information[].description" type="string">
            Document description.
          </ParamField>

          <ParamField body="data.identifying_information[].expiration" type="string">
            Document expiration date.
          </ParamField>

          <ParamField body="data.identifying_information[].image_front" type="string">
            Base64-encoded front image of the document.
          </ParamField>

          <ParamField body="data.identifying_information[].image_back" type="string">
            Base64-encoded back image of the document.
          </ParamField>
        </Expandable>
      </ParamField>

      <ParamField body="data.nonresident_alien_attestation" type="boolean">
        Attests the user is a nonresident alien. See [nonresident alien
        attestation](#nonresident-alien-attestation).
      </ParamField>
    </Expandable>
  </ParamField>

  Below is a sample cURL command for this request:

  ```bash theme={"system"}
  curl --request POST https://api.privy.io/v1/users/did:privy:xxxxx/kyc/submit \
    -u "<your-privy-app-id>:<your-privy-app-secret>" \
    -H "privy-app-id: <your-privy-app-id>" \
    -H 'Content-Type: application/json' \
    -d '{
      "provider": "bridge",
      "environment": "sandbox",
      "endorsements": ["base"],
      "client_agreement_id": "<agreement-id-from-tos-acceptance>",
      "data": {
        "first_name": "Alex",
        "last_name": "Doe",
        "email": "user@example.com",
        "date_of_birth": "1990-01-31",
        "residential_address": {
          "street_line_1": "123 Market Street",
          "city": "San Francisco",
          "subdivision": "CA",
          "postal_code": "94103",
          "country": "USA"
        },
        "identifying_information": [
          {
            "type": "ssn",
            "issuing_country": "USA",
            "number": "000-00-0000"
          }
        ]
      }
    }'
  ```

  The response is the user's full KYC status snapshot, in the same shape as [track KYC
  status](/kyc-kyb/kyc-status).

  ```json theme={"system"}
  {
    "provider": "bridge",
    "environment": "sandbox",
    "status": "under_review",
    "tos": {
      "status": "approved"
    },
    "kyc": {
      "status": "under_review"
    },
    "endorsements": [
      {
        "name": "base",
        "status": "incomplete",
        "missing": ["kyc_approval"]
      }
    ],
    "capabilities": {
      "payin_crypto": "pending",
      "payout_crypto": "pending",
      "payin_fiat": "pending",
      "payout_fiat": "pending"
    },
    "requirements_due": ["id_verification"],
    "future_requirements_due": []
  }
  ```
</View>

The first submission must carry enough data for Bridge to begin verification: name, date of birth, residential address, and at least one identifying document.

<Note>
  Later submissions update the same Bridge customer. Omitted top-level fields are left untouched,
  including `email`; sending a different `email` replaces the address Bridge has on file, and since
  Bridge identifies customers by email, the old address stops resolving to this one. Nested objects
  such as a `residential_address` are replaced whole. If a response never reached you, resubmit the
  identical data.
</Note>

<Warning>
  Bridge allows one customer per email address, across individuals and businesses alike. A
  submission whose email already has a Bridge customer returns `409` — Privy will not adopt an
  existing customer, since an email in a request body is not proof the user owns it. Submit a
  different email, or contact [support@privy.io](mailto:support@privy.io) to link an existing Bridge
  customer to a Privy user.
</Warning>

## Nonresident alien attestation

`data.nonresident_alien_attestation` attests that the user is a nonresident alien, which can satisfy identification without a US tax ID.

It is one input to Bridge's nonresident alien eligibility, not all of it, and Bridge must enable the capability for your account before it is accepted. It applies to individuals only and is rejected on business customers, so it is not part of a [KYB submission](/kyc-kyb/kyb-headless).

## Next steps

<CardGroup cols={2}>
  <Card title="Track KYC status" icon="magnifying-glass" href="/kyc-kyb/kyc-status">
    Poll a user's verification status or subscribe to webhooks
  </Card>

  <Card title="Headless KYB" icon="building" href="/kyc-kyb/kyb-headless">
    Submit an organization's verification data from your server
  </Card>
</CardGroup>
