# Identity Verification

> Coming soon, upon request — this feature is turned on for your tenant by the Aurea team. [Contact us](https://www.aureahub.com/contact)

Verify who your users are — a person (KYC) or a company (KYB) — on a verification page your app opens from a link, and follow the result through the API and an event [Coming soon, upon request](https://docs.aureahub.com/docs/feature-availability.md).

## Overview

1. Your server starts a verification for one of your users: [Start a Verification](https://docs.aureahub.com/docs/identity-verification-create.md). The answer carries a `verificationUrl`.
2. Your app shows the user the verification page at that address — in the browser or in a WebView. The user completes it there: an identity document and a selfie for a person; the company's details, documents, owners and directors for a company.
3. The result comes back to your server as the event `identity.verification.updated`, and from [Get a Verification](https://docs.aureahub.com/docs/identity-verification-get.md): a `state`, and a `nextStep` that tells your app what to show.

- **Your server calls it.** Every call is signed by your tenant — the HMAC headers `x-tenant-api-key`, `x-timestamp` and `x-signature`, or a server token ([Authentication](https://docs.aureahub.com/docs/authentication.md)) —, for one of your users. Never from your app with a user's access token, and never with an administrator's.
- **Nothing sensitive comes back.** Documents, photos and what the user types on the page stay with the verification service: the API answers states, next steps, a reason code and the screening's status — never a document, an image or the details entered.
- **Per environment.** The sandbox (`isTestnet: true`) and production have their own verifications: a user verified in the sandbox is not verified in production ([Environments & Test Data](https://docs.aureahub.com/docs/guide-environments.md)).
- **A purpose.** `purpose` says what a verification is for. Today there is one, `bank_ramp`: the verification decides the user's verification on the bank ramp's local rails (With the Bank Ramp, below).

## Before You Start

The Aurea team turns identity verification on for your tenant, one environment at a time, the sandbox first. Tell your Aurea contact which kinds you verify — persons, companies or both —, the purposes, and the address of your app or site the page sends users back to by default. Until it is on:

- while the module is off for your tenant, its routes answer `403` `MODULE_NOT_ENABLED` with `details.module` `identity_verification` ([Feature Availability › Modules](https://docs.aureahub.com/docs/feature-availability.md));
- while it is not switched on in the environment you call, Start a Verification answers `403` `IDENTITY_DISABLED` (with `details.environment`); while that environment is not set up yet, `503` `IDENTITY_NOT_CONFIGURED`. Nothing is sent anywhere.

To receive the events, add `identity.verification.updated` — and `identity.handoff.updated` if your tenant uses it (The Events, below) — to the `eventTypes` of a webhook endpoint in each environment ([Webhooks & Events](https://docs.aureahub.com/docs/guide-events.md)): they are sent only to the addresses that name them.

## Person or Company

- `person` — KYC. The user shows an identity document and takes a selfie; the page may also ask for a proof of address or a few questions, as set up for your tenant. Many persons are decided automatically within minutes; some go to a person for review.
- `company` — KYB. The user acting for the company gives its details and documents and names its beneficial owners and directors, who each verify themselves as a person. A company is always decided by a reviewer (Companies, below).
- A user has at most one live verification per kind and purpose in each environment, and may have one of each kind.

## Start a Verification

From your server, for one of your users, signed with your tenant's API key and secret (`signedHeaders` is the helper of [Authentication › HMAC Request Signing](https://docs.aureahub.com/docs/authentication.md)):

```javascript
import { signedHeaders } from './aurea-signing.js';

const path = '/v1/identity/verifications';
const body = JSON.stringify({
  userId: '5d2c1b0a-9e8f-4a7b-8c6d-5e4f3a2b1c0d',
  kind: 'person',                      // or 'company'
  purpose: 'bank_ramp',
  isTestnet: true,                     // the sandbox; omit for production
});
const res = await fetch('https://api.aureahub.com' + path, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', ...signedHeaders('POST', path, body) },
  body,
});
const verification = await res.json(); // 201: a new verification; 200: the live one
```

```json
{
  "id": "7c1e9a24-5b3d-4f8a-9e6c-2d0b4a8f1e37",
  "userId": "5d2c1b0a-9e8f-4a7b-8c6d-5e4f3a2b1c0d",
  "kind": "person",
  "purpose": "bank_ramp",
  "state": "started",
  "nextStep": "continue_verification",
  "reason": null,
  "aml": null,
  "createdAt": "2026-11-12T10:02:11.000Z",
  "updatedAt": "2026-11-12T10:02:11.000Z",
  "verificationUrl": "https://…"
}
```

- `201`: a new verification, `started`.
- `200`: the user already has a live verification of that kind and purpose — `started`, `in_review` or `verified`. The same verification comes back: calling again never makes a second one, so a retry after a lost answer is safe.
- `verificationUrl` is a fresh link to the page **only while the verification is `started`**; `null` when there is nothing for the user to complete — the verification is in review or verified, or the user has already submitted the page and its result is on its way.
- After `rejected` or `expired`, calling again starts a new verification — `201`, a new `id` —; the earlier one can still be read.
- `returnUrl`, optional, is accepted and checked — an `https` address or your app's own scheme, never `http`, up to 2048 characters — but the verification page does not send the user to it: when they are done, the page shows its own completion screen (Show the Verification Page).
- A field missing or not one of its values answers `400` `IDENTITY_INVALID`, with the fields in `details.fields`; nothing is started.

## Show the Verification Page

- **Open `verificationUrl` exactly as it is**: don't add, change or remove anything in it. It is a personal, short-lived link that acts for the user: hand it to your app, open it right away, and never store, log, email or share it. For a later visit, ask for a fresh one (Resume, below).
- **On the web**, send the user to it — a redirect, or a new tab — or embed it in an `<iframe>` that allows the camera and the microphone. Your own page must be served over `https`.
- **Another origin.** The page is served from the verification service's own web address, not from `aureahub.com`. If your site sends a Content-Security-Policy, add the origin of `verificationUrl` to `frame-src` before embedding it; if your app limits where it navigates, allow that origin.
- **When the user is done**, the page shows its completion screen and stays there; it does not send the user to another address. Bring your user back yourself — they close the tab, the iframe or the WebView, or your app offers a button that does — and then read the verification, or wait for the event. Finishing the page is not being verified.

```html
<iframe
  src="VERIFICATION_URL"
  allow="camera; microphone; fullscreen"
  style="width: 100%; height: 720px; border: 0;">
</iframe>
```

## In a Mobile App

Your server asks for the link and hands it to your app, which opens it in a WebView or in the system browser. The page needs the camera — and the microphone for some checks —, JavaScript, storage, and video that plays inside the page:

- **iOS (`WKWebView`)**: `NSCameraUsageDescription` and `NSMicrophoneUsageDescription` in `Info.plist`; on the configuration, `allowsInlineMediaPlayback = true` and `mediaTypesRequiringUserActionForPlayback = []`, so the camera preview plays inside the page instead of full screen; grant the page's camera request in your `WKUIDelegate`.
- **Android (`WebView`)**: the `CAMERA` and `RECORD_AUDIO` permissions in the manifest, asked at run time; `javaScriptEnabled` and `domStorageEnabled` on; `mediaPlaybackRequiresUserGesture = false`; grant the page's request in `WebChromeClient.onPermissionRequest`, for the page's origin only.
- **React Native (`react-native-webview`)**: the props below, and the platform permissions above.
- **Bring the user back yourself.** The page ends on its completion screen, not on an address of yours: give the WebView or the in-app browser a close button, and when the user closes it read the verification from your server.
- **Don't decide from the WebView.** The event and Get a Verification are the result, not the page's last address or a navigation callback.
- Open only `verificationUrl` there — never an Aurea API address, and never with your credentials.

```javascript
<WebView
  source={{ uri: verificationUrl }}
  javaScriptEnabled
  domStorageEnabled
  allowsInlineMediaPlayback
  mediaPlaybackRequiresUserAction={false}
/>
// Your own close button around it: close the WebView, then read the verification from your server.
```

## States and Next Step

| state | Meaning | nextStep | What your app shows |
| --- | --- | --- | --- |
| started | The user has not finished the page. | continue_verification | A **Continue** button that asks your server for a fresh link and opens it (Resume). |
| in_review | Submitted, waiting for a decision: a reviewer looks at it, or a check — such as the screening — needs a second look. | wait_for_review | That the details are being checked. Nothing for the user to do. |
| verified | Verified. | approved | The feature the verification opens. |
| rejected | Not verified. | rejected | That the verification did not succeed, and — if your rules allow — a new attempt, which starts a new verification. |
| expired | Verified once, no longer (Expiry and Re-Verification). | expired | A button that starts a new verification. |

- `nextStep` `start_verification`: there is nothing for the user to continue — start a verification.
- **A decision is not always final.** A verified user can go back to `in_review` — a later check, a new screening match, a review that is reopened —, and a rejected one can be reviewed again. Act on the current state each time, and keep following the event.
- Branch on `state` and `nextStep`. A value you don't know may come later: treat it as not verified.
- `reason`: why the state is what it is, as a code — `null` when there is none. Among them `manual_review`, `aml_review` and `kyb_review` (in review), `reopened` and `risk_reevaluated` (a decision reopened), `kyc_expired`, `failed`, `rejected`, `cancelled`, `kyb_rejected` and `kyb_cancelled`, or the code of a reviewer's decision. Use it for your logs and support; don't show it to the user as it is, and don't branch on it: more codes may come.
- `aml`: the screening's status (Screening, below), `null` until a screening has a result.

## Resume

- A user who leaves the page halfway keeps their progress. To continue, call [Start a Verification](https://docs.aureahub.com/docs/identity-verification-create.md) again with the same `userId`, `kind` and `purpose`: `200`, the same verification, and — while it is `started` — a fresh `verificationUrl` to open. An earlier link may no longer work.
- Ask for the link when the user taps **Continue**, not in advance.
- `verificationUrl` `null` on a `started` verification: the user has already submitted the page and its result is on its way — show that the details are being checked, and wait for the event.
- A rare `409` `IDENTITY_SESSION_CONFLICT`: the page could not be opened for this verification just now. Read the verification, and ask again a little later.
- [Get a Verification](https://docs.aureahub.com/docs/identity-verification-get.md) and [A User's Verifications](https://docs.aureahub.com/docs/identity-subject-verifications.md) never answer a link.

## Expiry and Re-Verification

- `expired`: the verification was valid and has lapsed — for example, the identity document it was made with has expired, or the re-verification period set for your tenant has passed. From then on the user is not verified for that purpose.
- To verify the user again, start a new verification: `201` with a new `id`. The expired one stays readable.
- A company's application left unfinished for a long time is closed and answers `rejected`: start again.

## Companies

1. Start with `kind: "company"` for the user who acts for the company, and show them the page.
2. They give the company's details — name, registration number, address, activity —, upload its documents, and name its beneficial owners and directors with their email addresses.
3. Each owner and director is invited by email to verify themselves as a person, on a verification page of their own. The company cannot be decided while one of them has not.
4. Once everything is in, the company is `in_review`: a reviewer examines the case — the documents, the ownership, the screening of the company and its people — and approves or rejects it. This takes longer than a person's verification, and happens outside the API.
5. The decision moves the verification to `verified` or `rejected`, and the event is sent.

- Only the review decides a company: until then it is `started` or `in_review`, whatever the page showed the user.
- The owners' and directors' own checks are part of the company's verification: they are not your users' verifications, and [A User's Verifications](https://docs.aureahub.com/docs/identity-subject-verifications.md) does not list them.

## Screening

- Where it is part of your tenant's verification, persons and companies are screened against sanctions and politically exposed persons lists. A match sends the verification to `in_review` until it is cleared; a later screening that finds a match can send a verified user back to `in_review`.
- `aml.status` on [Get a Verification](https://docs.aureahub.com/docs/identity-verification-get.md) and in the event, once a screening has a result: `PENDING`, `APPROVED`, `REJECTED` or `ESCALATED`; `aml` is `null` before.
- **A match opens an AML case** in your tenant, with `direction` `onboarding` and the verification's id in `screening.identityVerificationId`, sent as `aml_case.opened` to the endpoints that name it. Your compliance roles read it ([Screening & AML Cases](https://docs.aureahub.com/docs/guide-screening.md)); **Aurea's compliance decides it**: assigning, commenting or deciding it answers `403` `AML_CASE_AUREA_DECIDES`.
- While that case is open, the purpose's feature stays closed to the user — on the bank ramp, the user stays `pending` —; a confirmed match refuses it for good.
- Don't tell the user about a screening result: show that their details are being checked.

## The Events

`identity.verification.updated` is sent each time a verification's `state` or its screening's status changes, to the addresses of that environment that name it. It is signed and retried like every event ([Webhooks & Events](https://docs.aureahub.com/docs/guide-events.md)):

```json
{
  "id": "evt_4b8e2d6a1c9f3e5b7d0a2c4e6f8b1d3a",
  "type": "identity.verification.updated",
  "environment": "sandbox",
  "occurredAt": "2026-11-12T10:09:47.000Z",
  "createdAt": "2026-11-12T10:09:48.215Z",
  "data": {
    "verificationId": "7c1e9a24-5b3d-4f8a-9e6c-2d0b4a8f1e37",
    "userId": "5d2c1b0a-9e8f-4a7b-8c6d-5e4f3a2b1c0d",
    "kind": "person",
    "purpose": "bank_ramp",
    "state": "verified",
    "previousState": "in_review",
    "nextStep": "approved",
    "reason": null,
    "aml": {
      "status": "APPROVED"
    },
    "updatedAt": "2026-11-12T10:09:47.000Z"
  }
}
```

| data | Description |
| --- | --- |
| verificationId | The verification's `id`, as Start a Verification answered it. |
| userId | Your user. |
| kind | `person` or `company`. |
| purpose | `bank_ramp`. |
| state | The state after the change (States and Next Step). |
| previousState | The state before it — the same as `state` when only the screening changed. |
| nextStep | What your app shows now. |
| reason | The reason code, or `null`. |
| aml | `{ status }` of the screening, or `null` until it has a result. |
| updatedAt | When the verification last changed. |

- Events can arrive out of order or twice: dedupe on the event `id`, and when in doubt read [Get a Verification](https://docs.aureahub.com/docs/identity-verification-get.md), which is always current ([Webhooks & Events › Order and Duplicates](https://docs.aureahub.com/docs/guide-events.md)).
- Never sent: documents, images, names, dates of birth or anything else the user entered on the page.

`identity.handoff.updated` — where your tenant has its verified users introduced to a partner bank that opens an account in the user's name [Coming soon, upon request](https://docs.aureahub.com/docs/feature-availability.md): sent when that account is recorded for a verified user, so your app can show it. `data`: `verificationId`, `userId`, `purpose`, `state` (`pending`, `sent`, `account_open` or `failed`), `account` — `iban`, `bic`, `holder` and `bank` once the account is open, `null` before — and `updatedAt`.

```json
{
  "id": "evt_9d1f3b5a7c2e4d6f8a0b2c4d6e8f0a1b",
  "type": "identity.handoff.updated",
  "environment": "production",
  "occurredAt": "2026-11-14T15:20:03.000Z",
  "createdAt": "2026-11-14T15:20:03.412Z",
  "data": {
    "verificationId": "7c1e9a24-5b3d-4f8a-9e6c-2d0b4a8f1e37",
    "userId": "5d2c1b0a-9e8f-4a7b-8c6d-5e4f3a2b1c0d",
    "purpose": "bank_ramp",
    "state": "account_open",
    "account": {
      "iban": "…",
      "bic": "…",
      "holder": "Jane Example",
      "bank": "…"
    },
    "updatedAt": "2026-11-14T15:20:03.000Z"
  }
}
```

## With the Bank Ramp

Where your tenant has the bank ramp's [local rails](https://docs.aureahub.com/docs/guide-bank-local.md) [Coming soon, upon request](https://docs.aureahub.com/docs/feature-availability.md), asks for verification there, and identity verification decides it in that environment:

- [Register for the Local Rails](https://docs.aureahub.com/docs/local-verification-register.md) — called by your app with the user's access token, as before — also answers `verificationId` and `verificationUrl`: show the page as above. An `individual` is verified as a `person`, a `business` as a `company`. Starting it yourself with `purpose: "bank_ramp"` gives the same verification, not a second one.
- **The registration is always saved.** `verificationUrl` is `null` when there is nothing to complete, or when the page could not be prepared at that moment: register again with the same details — `200` — for a fresh link. In the rare case no verification could be started at all, the answer has neither field: register again a little later.
- **Register the user's details exactly as on their identity document** — the legal name and, for a person, the date of birth; a company's registered legal name. The state stays `registered`, and the details can still be corrected, until the user starts on the verification page; it is then `pending`.
- **The verified identity must match.** Before the user becomes `verified` on the local rails, the name and date of birth (a company: its legal name) that the verification confirmed are compared with the registered ones — case, accents, punctuation and word order aside. When they differ, or the confirmed details cannot be read, the user stays `pending` while Aurea reviews it, even though the verification itself answers `verified`. The answers and events never say which detail differed.
- The verification's result then moves the state: `verified` opens the flows that move money; `rejected` and `expired` follow too — after `expired`, register again for a new verification. While an AML case of the verification is open (Screening, above) the user stays `pending`; a confirmed match makes them `rejected`.
- Each change is sent as `ramp.kyc.updated` as well as `identity.verification.updated`. In `ramp.kyc.updated`, `nextStep` is then `start_onboarding` (registered, or expired), `continue_onboarding` (the user has the page to finish), `wait_for_review`, `approved` or `rejected`.
- A user the bank ramp's operations already decided keeps that decision.
- Without identity verification, Register for the Local Rails answers exactly as before, and the bank ramp's operations verify the user.

## Errors

| Answer | What happened | What to do |
| --- | --- | --- |
| `401` | The signature is missing, wrong, expired or already used. | Sign the request again ([Authentication › Signature Errors](https://docs.aureahub.com/docs/authentication.md)). |
| `403` `MODULE_NOT_ENABLED` | Identity verification is off for your tenant (`details.module` `identity_verification`). | Ask your Aurea contact. |
| `403` `IDENTITY_DISABLED` | It is not switched on for your tenant in the environment called (`details.environment`). | Check `isTestnet`; ask your Aurea contact to switch that environment on. |
| `400` `IDENTITY_INVALID` | A field is missing or not one of its values — `userId`, `kind`, `purpose`, `returnUrl`, an id in the path — listed in `details.fields`. | Fix the request: nothing was started. |
| `404` `IDENTITY_SUBJECT_NOT_FOUND` | `userId` is not one of your tenant's users. | Check the id. |
| `404` `IDENTITY_VERIFICATION_NOT_FOUND` | No verification of your tenant has that id. | Check the id; use the one Start a Verification answered. |
| `409` `IDENTITY_SESSION_CONFLICT` | The verification page could not be opened for this verification just now. | Read the verification; ask for the link again a little later. |
| `503` `IDENTITY_NOT_CONFIGURED` | That environment is not set up yet. Nothing was sent. | Ask your Aurea contact. |
| `503` `IDENTITY_PROVIDER_UNAVAILABLE` | The verification service is busy or did not answer. | Try again later, with a growing pause. A retry of Start a Verification never makes a second verification. |
| `502` `IDENTITY_PROVIDER_REFUSED` | The verification service refused the request. | Don't retry it as it is: write to [${SUPPORT_EMAIL}](mailto:${SUPPORT_EMAIL}) with the time and the verification's `id`. |
| `502` `IDENTITY_PROVIDER_UNEXPECTED_RESPONSE` | The verification service's answer could not be read. | Read the user's verifications (A User's Verifications) before trying again. |

Each code is in `details.code`; where the person can act, `details.nextStep` says what to do. The answers about the verification service carry `"error": "IdentityError"`. Every code is in [Errors & Codes](https://docs.aureahub.com/docs/errors.md).

## Testing in the Sandbox

- Have identity verification switched on in the sandbox first, call with `isTestnet: true`, and name `identity.verification.updated` on a sandbox webhook endpoint.
- **Use test identities**: test users of your tenant, with your team's own documents or the test documents your Aurea contact gives you — never a real customer's documents. The invitations to a company's owners and directors are real emails, in the sandbox too: give your own team's addresses.
- Go through every outcome your app handles: a person verified, a person rejected, a person sent to review, a company reviewed, a user who leaves halfway and continues with a fresh link, and an expired verification started again. With the bank ramp, also a registration whose name differs from the document's: the user stays `pending`.
- Test the WebView on real devices, iOS and Android: the camera permission and the inline camera preview are the usual problems.
- Sandbox results never carry over: in production the user verifies again.

## FAQ

### Does Aurea store my users' documents?

No. Documents and photos stay with the verification service; the API answers the verification's state, its next step, a reason code and the screening's status.

### Can my app call these routes with the user's access token?

No: they are your server's, signed by your tenant. Your server asks for the link and hands it to your app. On the bank ramp, Register for the Local Rails — called with the user's token — answers the link too.

### Can my app collect the documents with its own screens?

No: the user completes the verification on the verification page. Your app chooses how to show it — a redirect, a new tab, an iframe or a WebView.

### How long does a link work?

Not long. Ask for a fresh one each time the user opens the page: Start a Verification answers it while the verification is `started`.

### The user finished the page, but the state is still `started` or `in_review`.

Finishing the page is not the decision. A person can need a review, a company always does, and a screening match waits until it is cleared. Wait for the event.

### The verification is `verified`, but the bank ramp still says `pending`.

The registered details differ from the verified ones, or a screening match is being reviewed: Aurea reviews it, and `ramp.kyc.updated` is sent when it is decided (With the Bank Ramp).

### Should I poll or listen to the event?

Listen to `identity.verification.updated`, and read Get a Verification when your app opens or when the user closes the verification page. Poll only while the user waits on screen, a few seconds apart ([Rate Limits](https://docs.aureahub.com/docs/rate-limiting.md)).

### Can one user be verified as a person and as a company?

Yes: one live verification per kind and purpose, in each environment.

### Is a decision final?

Not always: a later check or screening can send a verified user back to `in_review`, and a review can be reopened. Always act on the current state.

---

Web version: https://docs.aureahub.com/#guide-identity
