> ## 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.

# Build a Connect ProofAge button

> Let your users connect their own ProofAge workspace without copying keys: the device flow step by step, what to store, and the redirect flow.

If your software is used by many people who each have their own ProofAge account, such as a hosting billing panel, a marketplace plugin or a CMS module, you can give them a **Connect ProofAge** button instead of asking them to paste keys. The user signs in to ProofAge, picks a workspace and clicks **Allow**. Your software receives that workspace's public key and a secret key made for this connection, and from then on it calls the [API](https://docs.proofage.net/api-reference/overview.md) like any other integration.

This page is for platform developers. If you only want to use ProofAge from your own site, see the [quick start](https://docs.proofage.net/getting-started/quick-start.md) and the [server SDKs](https://docs.proofage.net/integration/sdks.md).

## Get a client ID

Connecting needs a `client_id` that ProofAge registers for your app. There is no self-service sign-up: write to [partners@proofage.net](mailto:partners@proofage.net), say what you are building and whether it runs on your users' own servers or as a hosted service, and we send you the `client_id`. If you would like to talk about a partnership, mention it in the same message.

| Your software | Flow | Client |
| - | - | - |
| Installed on your users' own domains (a billing panel module, a self-hosted plugin) | [Device flow](#the-device-flow) | Public: no secret, because the code is shipped to your users |
| A hosted service with one fixed address (an integration platform) | [Redirect flow](#the-redirect-flow-for-hosted-services) | Confidential: you also get a `client_secret` and register one redirect URI |

Everything below runs on the **app host**, `https://app.proofage.net`, not on `api.proofage.net`. The device endpoints refuse requests to any other host. Only the verification calls you make afterwards go to `https://api.proofage.net/v1`.

## The device flow

Your software asks ProofAge for a short code, shows it to the user, and polls until the user has approved it. No redirect URI is involved, so one `client_id` works on every install.

**1. Ask for a code**

When the user clicks **Connect ProofAge**, call:

```bash theme={null}
curl -X POST https://app.proofage.net/oauth/device/code \
  -d client_id=YOUR_CLIENT_ID \
  -d scope=workspace:connect
```

The scope must be exactly `workspace:connect`. ProofAge answers `200`:

```json theme={null}
{
  "device_code": "44f7f6b7b9de21627fee…",
  "user_code": "HTVJSXCV",
  "verification_uri": "https://app.proofage.net/oauth/device",
  "verification_uri_complete": "https://app.proofage.net/oauth/device?user_code=HTVJSXCV",
  "expires_in": 600,
  "interval": 5
}
```

Keep `device_code` to yourself: it is what you poll with. A code lives for `expires_in` seconds (10 minutes).

A single address may ask for 20 codes a minute. Beyond that the answer is `429` with `{"error": "slow_down", …}`: wait a minute before asking again. One code per click on **Connect ProofAge** is plenty.

**2. Show the code and the plain link**

Show the user the `user_code` and a link to `verification_uri`, and ask them to type the code there. **Do not link to `verification_uri_complete` and do not open it for them.** ProofAge ignores a code in the address on purpose: the person must type the code that your software is showing them, so a link cannot send them to somebody else's request. Say so on your screen, for example: *Open this page and enter the code. Only enter a code you see here.*

The user opens the page, signs in to ProofAge (or creates an account and then clicks **Connect ProofAge** in your software again, because the code lives only 10 minutes), types the code, picks a workspace and clicks **Allow**. ProofAge shows them the name of your app, the publisher name we registered for it, and a notice headed *Check before you allow*: allow only if they started this connection in your app themselves, just now. After **Allow**, the page tells them to go back to your software, which finishes connecting on its own.

**3. Poll for the token**

While the user does that, call the token endpoint every `interval` seconds:

```bash theme={null}
curl -X POST https://app.proofage.net/oauth/token \
  -d grant_type=urn:ietf:params:oauth:grant-type:device_code \
  -d client_id=YOUR_CLIENT_ID \
  -d device_code=DEVICE_CODE
```

Until the user has decided, the answer is an error. Handle these:

| Status | `error` | What to do |
| - | - | - |
| 400 | `authorization_pending` | The user has not approved yet. Wait `interval` seconds and ask again. |
| 400 | `slow_down` | You asked too soon. Add 5 seconds to your interval and carry on. |
| 400 | `expired_token` | The code is older than 10 minutes. Start again from step 1. |
| 401 | `access_denied` | The user clicked **Cancel**. Stop. |
| 400 | `invalid_grant` | The code is not valid any more: it was already used, or the same person approved another connection of your app after it. Start again from step 1. |

When the user has approved, the answer is `200`:

```json theme={null}
{
  "token_type": "Bearer",
  "expires_in": 3600,
  "access_token": "eyJ0eXAiOiJKV1Qi…",
  "refresh_token": "def50200…"
}
```

**4. Collect the keys**

The access token only proves who connected your app. Use it once, to fetch the keys:

```bash theme={null}
curl -X POST https://app.proofage.net/oauth/connect/credentials \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json theme={null}
{
  "api_key": "pk_live_…",
  "secret_key": "sk_live_…",
  "workspace": {
    "id": "01a1d4b3-cefd-73bb-80fb-d6ca3d6eaec5",
    "name": "Hosting Live",
    "mode": "live",
    "flow_type": "kyc"
  }
}
```

The response is `Cache-Control: no-store`. The call works **once**: whatever the outcome, ProofAge then revokes the access token and the refresh token, so do not keep them. Errors come as `{"error": {"code": "…"}}`:

| Status | `code` | Meaning |
| - | - | - |
| 401 | | The token is missing, wrong, or already used. Start again from step 1. |
| 403 | `CLIENT_NOT_ALLOWED` | The token does not belong to a connect app that is switched on. |
| 403 | `CONNECT_NOT_PERMITTED` | The person can no longer connect this workspace (their role changed, or the workspace was suspended). |
| 409 | `GRANT_NOT_FOUND` | There is no approval to collect: it is older than 10 minutes, or was already collected. Start again from step 1. |
| 409 | `KEY_LIMIT_REACHED` | The workspace already has 5 secret keys. The user must delete one in the console, then connect again. |

## What to store

Store both keys, **per user of your platform** (per install, if one install serves one account): the `api_key` (public) and the `secret_key`. Treat the secret key like a password. Show the user which workspace is connected, by `workspace.name`, and say whether it is a **test** or **live** workspace with `workspace.mode`: the user picks the workspace, and a test workspace verifies nothing for real and bills nothing (see [Sandbox testing](https://docs.proofage.net/integration/sandbox-testing.md)).

The secret key is made for this connection and belongs to your software:

* ProofAge names it *Your app · the user's email · date* and marks it *Connected via Your app* in the workspace's key list in the console.
* The console does not show its secret, so it cannot be copied into another system by accident. Only your software has it.
* It is not the workspace's active key and cannot be made active. The webhook URL the user set in the console is still signed with their own active key, which your software does not have.
* ProofAge e-mails the administrators of the user's organization whenever your app gets a new key, with the workspace, the person who connected and the key's name.
* **The user disconnects your software by deleting the key in the console.** Your next call to `/v1` then answers `401`. Treat that `401` as *disconnected*: tell the user and let them connect again. The old key never works again.

A connection is one workspace, your app and one ProofAge user. Connecting the same workspace again with the same ProofAge account **returns the same key**. A different ProofAge user of the same organization who connects that workspace gets a key of their own. If the user connects again and picks another workspace, you receive that workspace's keys; the key for the first workspace keeps working until the user deletes it in the console.

## Call the API

Sign every `/v1` request with the secret key, exactly as in [API authentication](https://docs.proofage.net/getting-started/api-authentication.md), with the `api_key` in `X-API-Key`. The [server SDKs](https://docs.proofage.net/integration/sdks.md) do the signing for you.

**Create verifications from your server, with a signed request.** A create with the public key alone, for example from a browser, has no signature: ProofAge then drops `external_id` and `callback_url`, which your software needs to match a verification to its own customer and to bring the person back to your page afterwards. A signed create from your server is also what ties the verification to your app.

```bash theme={null}
curl -X POST https://api.proofage.net/v1/verifications \
  -H "X-API-Key: pk_live_…" \
  -H "X-HMAC-Signature: SIGNATURE" \
  -H "Content-Type: application/json" \
  -d '{"external_id": "order-4711"}'
```

Nothing else has to be passed with a request.

## Receive results

When the user connects, create a [webhook subscription](https://docs.proofage.net/integration/webhooks.md#webhook-subscriptions) with your keys: `POST /v1/webhook-subscriptions`, signed like any other request, with the URL where your software listens. Its deliveries are signed with your secret key, so you can verify them as in [Verifying the signature](https://docs.proofage.net/integration/webhooks.md#verifying-the-signature), with your key in place of the active one. Before creating one, list the workspace's subscriptions with `GET /v1/webhook-subscriptions`; if one already points at your URL, keep it. Two subscriptions to the same URL deliver every decision twice.

> [!WARNING]
>
> Do not ask the user to paste your URL into the workspace's webhook setting in the console. That webhook is signed with the workspace's active key, which your software does not have, so you could not verify its deliveries.

* **Decisions only.** A subscription receives `status.updated`, not `data.updated`. If an administrator corrects a document field in the console, fetch the verification again with `GET /v1/verifications/{id}` when you need the current data.
* **No document data by default.** The body leaves out `document`, `fingerprint_signals` and who moderated. Send `include_document_data: true` when you create the subscription if you need them.
* **When your key is deleted.** Deliveries to your subscription continue, signed with the workspace's active key. Answer `410` to a delivery you cannot verify, and ProofAge deletes the subscription. If the same user connects the workspace again before that, the subscription moves to the new key and you can verify its deliveries again.
* **A reachable URL.** ProofAge must be able to reach your URL from the internet. If your software runs where it cannot be reached, [retrieve results](https://docs.proofage.net/integration/retrieving-results.md) by polling instead.

## The scope

The only scope is `workspace:connect`. With the keys it yields, your software can do what any integration with that workspace's keys can do through the [API](https://docs.proofage.net/api-reference/overview.md): create verifications, read their results, subscribe to webhooks, block a face from a verification and erase a verification's personal data. It cannot reach the user's other workspaces or their console. Only administrators and developers of an organization can connect an app; the connect screen refuses anyone else.

## The redirect flow, for hosted services

A hosted service with one fixed address (an integration platform such as Zapier) uses the standard OAuth 2.0 authorization code flow instead. You register one redirect URI with us and receive a `client_id` and a `client_secret`.

1. Send the user's browser to `https://app.proofage.net/oauth/authorize` with `response_type=code`, your `client_id`, your registered `redirect_uri`, `scope=workspace:connect` and a `state` you check on return. The scope is the only one allowed.
2. The user signs in, picks a workspace and clicks **Allow**. ProofAge redirects to your `redirect_uri` with `code` and `state`.
3. Exchange the code at `https://app.proofage.net/oauth/token` with `grant_type=authorization_code`, your `client_id` and `client_secret`, the `redirect_uri` and the `code`.
4. Call `POST https://app.proofage.net/oauth/connect/credentials` with the access token, as in step 4 of the device flow. Everything under [What to store](#what-to-store) applies unchanged.

A client of one flow cannot use the other: asking a redirect client for a device code, or a device client for an authorization code, is refused.

## Test it

Connect a [test workspace](https://docs.proofage.net/integration/sandbox-testing.md) while you build. Create a verification with the keys you receive, and set its outcome from the console or with `POST /v1/verifications/{id}/test-outcome`.

## Troubleshooting

| Message or problem | What to do |
| - | - |
| `unauthorized_client` from `/oauth/device/code` | The `client_id` is not a connect app that is switched on, or it is a redirect client. Check the id with us. |
| `invalid_scope` | Ask for `workspace:connect` and nothing else. |
| `slow_down` | Poll no faster than `interval`, and add 5 seconds each time you get this. |
| `429` with `slow_down` from `/oauth/device/code` | Your address asked for more than 20 codes in a minute. Wait a minute, and request one code per click on **Connect ProofAge**. |
| The device endpoints answer `404` with `invalid_request` | You called the API host. Use `https://app.proofage.net`. |
| "This code is not valid or has expired" on the page | The code was mistyped, already used, or older than 10 minutes. Start again in your software to get a new one. |
| "This workspace already has 5 secret keys" | The user deletes a key they no longer use in the console, then connects again. |
| `401` from `/v1` after it worked | The user deleted the key in the console. Ask them to connect again. |


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