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

# Quick start

> Create a verification on your backend, open it in the browser, and receive the decision by webhook.

The recommended integration: your backend creates a verification, your page opens it with the Browser SDK, and your backend receives the decision by webhook.

<Tip>
  Working with an AI coding agent? The [integration prompt](/integration/integration-prompt) has it do these steps for you through the ProofAge MCP server.
</Tip>

<Steps>
  <Step title="Get a test workspace and its keys">
    In the [ProofAge console](https://app.proofage.net), create a **test** workspace with the check you need and copy its public key (`pk_test_…`) and secret key (`sk_test_…`). Test workspaces are never billed.

    Set the workspace's **webhook URL** to an endpoint on your backend. For local development, see [receiving webhooks locally](/integration/webhooks#receiving-webhooks-locally).
  </Step>

  <Step title="Create a verification on your backend">
    The check comes from the workspace, so the request only says who the person is and where their browser goes afterwards. Sign it with the secret key: a [server SDK](/integration/sdks) does that for you, or see [API authentication](/getting-started/api-authentication).

    <CodeGroup>
      ```bash curl theme={null}
      curl -X POST https://api.proofage.net/v1/verifications \
        -H "Accept: application/json" \
        -H "Content-Type: application/json" \
        -H "X-API-Key: pk_test_..." \
        -H "X-HMAC-Signature: <signature>" \
        -d '{"external_id":"user_12345","callback_url":"https://yourapp.example/verified"}'
      ```

      ```js Node.js theme={null}
      // npm install @proofage/node; reads PROOFAGE_API_KEY and PROOFAGE_SECRET_KEY
      import { ProofAgeClient } from '@proofage/node';

      const client = new ProofAgeClient();
      const verification = await client.verifications().create({
        external_id: 'user_12345',
        callback_url: 'https://yourapp.example/verified',
      });
      ```

      ```python Python theme={null}
      # pip install proofage; reads PROOFAGE_API_KEY and PROOFAGE_SECRET_KEY
      from proofage import ProofAge

      with ProofAge() as client:
          verification = client.verifications.create(
              external_id="user_12345",
              callback_url="https://yourapp.example/verified",
          )
      ```

      ```php PHP theme={null}
      // composer require proofage/php-sdk
      use ProofAge\Sdk\Client;

      $client = new Client([
          'api_key' => getenv('PROOFAGE_API_KEY'),
          'secret_key' => getenv('PROOFAGE_SECRET_KEY'),
          'base_url' => 'https://api.proofage.net',
      ]);
      $verification = $client->verifications()->create([
          'external_id' => 'user_12345',
          'callback_url' => 'https://yourapp.example/verified',
      ]);
      ```

      ```php Laravel theme={null}
      // composer require proofage/laravel-client; reads PROOFAGE_API_KEY and PROOFAGE_SECRET_KEY
      use ProofAge\Laravel\Facades\ProofAge;

      $verification = ProofAge::verifications()->create([
          'external_id' => (string) $user->id,
          'callback_url' => 'https://yourapp.example/verified',
      ]);
      ```
    </CodeGroup>

    The response (`201 Created`) is the [verification](/api-reference/data-models#verification), with the `url` the person opens:

    ```json theme={null}
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "external_id": "user_12345",
      "external_metadata": null,
      "redirect_url": "https://yourapp.example/verified",
      "status": "created",
      "reason": null,
      "duplicate_check": { "checked": false, "duplicate_count": 0, "duplicates": [] },
      "erasure": null,
      "consent_accepted_at": null,
      "created_at": "2026-09-29T10:00:00+00:00",
      "updated_at": "2026-09-29T10:00:00+00:00",
      "url": "https://idv.proofage.net/v/eyJ..."
    }
    ```

    Store `id` next to your user and return `url` to your page.

    <Warning>
      `external_id` and `callback_url` are kept only on a signed request. Without a signature the API drops them, because anyone can read your public key in the browser. Create verifications on your backend.
    </Warning>
  </Step>

  <Step title="Open it on your page">
    Load the Browser SDK and open the `url`:

    ```html theme={null}
    <script src="https://app.proofage.net/sdk-build/kyc-loader.js"></script>
    <script>
      window.KycService.init({ apiKey: 'pk_test_...', language: 'en' });

      // The person reached the final screen. This is not the outcome.
      window.KycService.onComplete(function (result) {
        console.log('Finished', result.verificationId);
      });

      async function startVerification() {
        const { url } = await fetch('/your-backend/proofage-session', { method: 'POST' }).then((r) => r.json());
        window.KycService.start({ verificationUrl: url });
      }
    </script>

    <button onclick="startVerification()">Verify</button>
    ```

    <Warning>
      **The page navigates away after the final screen.** About three seconds after the final screen, the SDK calls `onComplete` and then sends the whole page to the verification's redirect URL. A new workspace's redirect URL is a ProofAge page, so set your own: pass `callback_url` when you create the verification (as above), or set the workspace's redirect URL in the console.
    </Warning>

    `onComplete` means the person finished, not that they passed. See [Browser SDK](/integration/web/browser-sdk) for every option, or [send the person to the `url`](/integration/web/redirect) instead of adding JavaScript.
  </Step>

  <Step title="Receive the decision">
    ProofAge sends a signed webhook each time the verification reaches `review`, `resubmission_requested`, `approved`, `declined`, `abandoned` or `expired`. Verify the signature, then update your user from `status` and `reason`. See [Webhooks](/integration/webhooks).

    A test workspace does not analyse anything: the submission stops in `review` until you set the outcome, on the verification's page in the console (**Approve**, **Decline** or a resubmission with a reason) or with the MCP tool `set-test-verification-outcome`. Setting it sends the webhook. See [Sandbox and test data](/integration/sandbox-testing).
  </Step>

  <Step title="Read the result when you need it">
    <CodeGroup>
      ```bash curl theme={null}
      curl https://api.proofage.net/v1/verifications/550e8400-e29b-41d4-a716-446655440000 \
        -H "Accept: application/json" \
        -H "X-API-Key: pk_test_..." \
        -H "X-HMAC-Signature: <signature>"
      ```

      ```js Node.js theme={null}
      const verification = await client.verifications('550e8400-e29b-41d4-a716-446655440000').get();
      console.log(verification.status);
      ```

      ```python Python theme={null}
      with ProofAge() as client:
          verification = client.verifications.get("550e8400-e29b-41d4-a716-446655440000")
      print(verification.status)
      ```

      ```php PHP theme={null}
      $verification = $client->verifications('550e8400-e29b-41d4-a716-446655440000')->get();
      echo $verification['status'];
      ```

      ```php Laravel theme={null}
      $verification = ProofAge::verifications()->find('550e8400-e29b-41d4-a716-446655440000');
      ```
    </CodeGroup>

    Document data, images and the age estimate have their own endpoints: see [Reading results](/integration/retrieving-results).
  </Step>
</Steps>

## Next steps

* [Go-live checklist](/integration/go-live): switch to a live workspace.
* [Verification statuses](/integration/verification-statuses): every status and transition.
* [Decision reasons](/core-technology/decision-reasons): what `reason` can say.


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