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

# IDV Integration Guide

> Add identity verification to your onboarding: create verification sessions for profiles, deliver invites, track progress, read review outcomes, and react to webhooks.

Use this guide to add Minerva identity verification (IDV) to your customer onboarding. You create a verification session for a customer profile, deliver the verification flow to your customer, and react to the outcome in your own systems.

The guide covers the API calls end to end: creating the profile and the session, the two ways to deliver the flow, the state model, and the webhooks your production systems should subscribe to.

## How it fits together

Four pieces work together:

* A **profile** is the customer record in Minerva, created through the Profiles API.
* An **IDV session** is one verification attempt tied to a profile. It carries the capture flow (consent, liveness and document captures, questionnaire) and the review outcome.
* **Review** is the decision layer. Most sessions pass without a human; some are held for an analyst in your Minerva workspace to accept or reject.
* **Webhooks** tell your systems when a session changes state, so you do not have to poll.

```mermaid theme={null}
flowchart TD
    A[Create the profile] --> B[Create the IDV session]
    B --> C[Deliver the invite]
    C --> D[User consents and captures]
    D --> E[Submit for assessment]
    E --> F{Assessment}
    F -- Clean pass --> G[automatic_pass]
    F -- Needs review --> H[requires_review]
    H --> I[Analyst review]
    I -- accepted or rejected --> J[Final decision]
    G --> K[Webhooks notify your systems]
    H --> K
    J --> K
```

## Before you begin

* You need an application API key for your workspace. Create one from the Developers page; [API Keys](/api-reference/api-keys) has the walkthrough. Keys are server-side credentials, so never ship one in a browser or a mobile app.
* You need a [profile](/minerva-profiles) for the customer. If it does not exist yet, create it first (Step 1).
* Every endpoint in this guide uses the base URL `https://api.gominerva.com/idv/v1` and authenticates with `Authorization: Api-Key <YOUR_API_KEY>`.

<Note>
  Identity verification is rolling out across Minerva workspaces. Contact
  [support@gominerva.com](mailto:support@gominerva.com) to enable it for your workspace. The API surface may
  still change while the rollout completes.
</Note>

## Step 1: Create the profile

Every verification session belongs to a Minerva profile. You have two options:

* Create the profile on its own with `POST https://api.gominerva.com/clm/v1/profiles`, then create the session yourself with the rest of this guide. See [Create a profile](https://docs.gominerva.com/api-reference/profile-management/create-a-profile).
* Create and onboard the profile in one call with `POST https://api.gominerva.com/clm/v1/onboarding/profiles`, using an `onboarding` block to enable screening, identity verification, or both. See [Create and screen a profile](https://docs.gominerva.com/api-reference/profile-management/create-and-screen-a-profile).

An onboarding block that enables both components looks like this:

```json theme={null}
{
  "name": "Alex Morgan",
  "kind": "individual",
  "email": "alex.morgan@example.com",
  "onboarding": {
    "screening": { "enabled": true, "feeds": ["Sanctions"] },
    "idv": { "enabled": true, "workflowId": "idvw_5f8c2a1b" },
    "sequence": "screening_first"
  }
}
```

A few rules for the block:

* Enabling IDV requires an email address on the profile and a workflow: either an explicit `workflowId` or your workspace's default.
* `sequence` is one of `screening_first` (the default), `idv_first`, or `parallel`.
* When IDV onboarding is enabled, Minerva starts the verification session for the profile as part of the run. The rest of this guide covers the explicit flow, where your backend creates the session and delivers the invite itself.

## Step 2: Create the IDV session

Create a verification session for the profile:

```json theme={null}
POST https://api.gominerva.com/idv/v1/sessions

{
  "profile_id": "66c391b92888a0db5cc6d3f6",
  "workflow": "liveness_and_id",
  "idempotency_key": "onboard-alex-001"
}
```

Fields to know:

* `profile_id` is required. The profile must already exist in your workspace: a missing profile returns `400 idv_profile_not_found`, and an unavailable profiles service returns `503 idv_profile_lookup_unavailable`.
* `workflow` picks a preset flow: `liveness_and_id`, `liveness_only`, or `id_only`. You can instead pass a `workflow_id` you configured, pass an inline `steps` manifest, or omit all three to use your workspace's default workflow.
* `idempotency_key` is an optional retry key (1 to 160 characters of letters, digits, and `.`, `_`, `:`, `-`). A retry with the same key and the same request returns the original session instead of creating a second one; reusing the key for a materially different request returns `409 idv_session_idempotency_conflict`.

The response is `201` with the session, a one-time `session_token`, and `token_expires_at`:

```json theme={null}
{
  "data": {
    "id": "idv-2cdf8b5c8b3db8a1d6f5d4ce9d46a63f",
    "profile_id": "66c391b92888a0db5cc6d3f6",
    "status": "collecting_artifacts",
    "review_status": "request_sent",
    "capture_manifest": {
      "workflow": "liveness_and_id",
      "required_kinds": ["liveness_front", "id_front", "id_back"]
    }
  },
  "session_token": "idvs_9c1f...",
  "token_expires_at": "2026-09-25T19:05:00Z"
}
```

`session_token` is the end-user capture credential for this session. It is surfaced only once, expires (15 minutes by default), and cannot be recovered or rotated; an idempotent replay returns it empty on purpose.

## Step 3: Deliver the verification flow

Choose one of two delivery modes for the session you just created.

### Email invite

```bash theme={null}
curl -X POST https://api.gominerva.com/idv/v1/sessions/idv-2cdf8b5c8b3db8a1d6f5d4ce9d46a63f/invite \
  -H "Authorization: Api-Key $MINERVA_API_KEY" \
  -H "Idempotency-Key: onboard-alex-001-invite" \
  -H "Content-Type: application/json" \
  -d '{ "delivery": "email" }'
```

* The recipient is always the email address on the linked profile, resolved at send time. The request does not accept a recipient. A profile without an email returns `422 idv_profile_email_missing`.
* Success is a `202` with `invited: true` and `invite_expires_at`. A `delivery_state` of `sent` means the email provider durably accepted the message.
* The emailed link is valid for 72 hours by default and stays reusable within that window, so a customer can reopen the email or continue on another device.
* Pass an `Idempotency-Key` header to make email retries safe (direct links ignore it; without a header Minerva generates its own key). A replay with the same key never sends a second email; it reports the delivery's durable state instead: `202` with `delivery_state: "sent"` once the provider has accepted the message, or `502 invite_delivery_failed` otherwise. While a send is still in flight, including an ambiguous provider outcome, a different key is refused with `409 idv_state_conflict`; retry once it settles. After the earlier send has settled (delivered or terminally failed), a new key can replace it and email again.
* Optional presentation fields: `locale` and `org_name` (which overrides the theme company name in the email).

### Direct link

```bash theme={null}
curl -X POST https://api.gominerva.com/idv/v1/sessions/idv-2cdf8b5c8b3db8a1d6f5d4ce9d46a63f/invite \
  -H "Authorization: Api-Key $MINERVA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "delivery": "none" }'
```

* The response is a `200` with `verify_url` and `invite_expires_at`. Hand the link to the user immediately: redirect to it or open it in a web view.
* Direct links work only with application keys. Dashboard-authenticated callers receive `403 idv_direct_invite_forbidden`.
* The link is single-use and expires within 15 minutes (900 seconds, and the lifetime cannot be raised).
* The one-time credential sits in the URL fragment (`#code=...`), which browsers never send to servers and Minerva never logs. Treat the whole URL as a secret.
* If Minerva cannot construct a secure public link, the request fails closed with `503 idv_direct_invite_unavailable`.

### Which mode should you use?

| Aspect                | Email invite                      | Direct link                              |
| --------------------- | --------------------------------- | ---------------------------------------- |
| Request               | `delivery: "email"` (the default) | `delivery: "none"`                       |
| Who receives the link | The profile's email address       | Your API response, for immediate handoff |
| Lifetime              | 72 hours by default               | Up to 15 minutes                         |
| Reusable              | Yes, within its lifetime          | No, single use                           |
| Best for              | Any user-completed flow           | A user who is already in your flow       |

<Note>
  An integration cannot mint an arbitrary long-lived link. Treat the email
  invite as the user-delivery path and the direct link as the immediate-handoff
  path for a user who is ready right now.
</Note>

## What the customer sees

Verification runs on Minerva's white-label page at `idv.gominerva.com`, styled with the theme attached to the session. The customer:

1. Opens the link from the email, or arrives through your handoff.
2. Reviews and accepts the consent notice.
3. Completes the capture steps, for example a liveness selfie and photos of an identity document.
4. Submits the session for assessment.

The customer experience shows progress and completion only. It never shows review states, flags, or analyst details; those live in your Minerva workspace and in webhooks.

## Step 4: Track progress

Read `GET /sessions/{sessionId}` with your application key, or subscribe to webhooks and let Minerva push transitions to you (recommended for production). A session carries two independent state axes.

### Flow status

| Value                  | Meaning                                                                   |
| ---------------------- | ------------------------------------------------------------------------- |
| `collecting_artifacts` | The customer is completing the capture steps.                             |
| `ready_for_assessment` | All required steps are complete and the session is ready to be submitted. |
| `queued`               | The session was submitted and is waiting for the assessment to start.     |
| `assessing`            | The assessment is running.                                                |
| `assessed`             | The assessment finished. Read `review_status` for the outcome.            |
| `failed`               | The assessment could not complete.                                        |
| `canceled`             | The session was canceled (see Step 5).                                    |

### Review status

| Value             | Meaning                                                                   |
| ----------------- | ------------------------------------------------------------------------- |
| `request_sent`    | The session was created and no submission has arrived yet.                |
| `pending`         | The customer submitted and the assessment is enqueued.                    |
| `automatic_pass`  | The assessment passed without a human decision.                           |
| `requires_review` | The assessment did not auto-decide and a reviewer must look at it.        |
| `escalation`      | A reviewer flagged the session for closer attention; the case stays open. |
| `accepted`        | A reviewer accepted the session. Terminal.                                |
| `rejected`        | A reviewer rejected the session. Terminal.                                |

<Note>
  The capture endpoint `GET /sessions/{sessionId}/status` belongs to the hosted
  verification page: it accepts only the session's capture or invite credential,
  not your application API key. Track backend progress from `GET /sessions/   {sessionId}`, `GET /sessions?profile_id=...`, or webhooks.
</Note>

## Step 5: Cancel a session

```bash theme={null}
curl -X POST https://api.gominerva.com/idv/v1/sessions/idv-2cdf8b5c8b3db8a1d6f5d4ce9d46a63f/cancel \
  -H "Authorization: Api-Key $MINERVA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Customer restarted onboarding" }'
```

* Cancellation is irreversible and moves the session to `canceled`.
* It consumes the capture token and the invite, so the link stops working for further capture.
* Re-canceling an already canceled session is a no-op, not an error.
* A session that is currently being assessed cannot be canceled: the call returns `409 idv_state_conflict` (the same code covers other terminal states).
* The optional `reason` is recorded internally and is never shown to the customer.

## Step 6: Read results and review outcomes

### Structured results

`GET /sessions/{sessionId}/data` returns the authorized, decrypted results for a session:

* The document record: type, issuing country and subdivision, and confidence.
* OCR field values with per-field confidence.
* Questionnaire answers, with the step and question each answer belongs to.
* Any mismatch details between the captured data and the profile.

Two properties to design around:

* Every access, success or denial, is written to an audit log before the response is returned. If the audit write fails, the request fails closed with no data.
* The endpoint is available to application keys and in-scope dashboard users only. It is never reachable with a session's capture credential.

### Review

Most sessions complete without a human. A clean assessment passes automatically (`automatic_pass`) and the session emits `idv.session.approved`. When the assessment will not auto-decide, the session lands on `requires_review` and waits for a reviewer in your Minerva workspace.

Reviewers can escalate a session (`escalation`, which keeps it open) or make the terminal decision: `accepted` or `rejected`. You can also record decisions from the API:

```json theme={null}
POST https://api.gominerva.com/idv/v1/sessions/idv-2cdf8b5c8b3db8a1d6f5d4ce9d46a63f/review

{
  "status": "accepted",
  "note": "Reviewed against source documents"
}
```

Decisions against a finished session return `409 idv_review_terminal`, and inadmissible transitions return `409 idv_review_invalid_transition`.

The outcome also flows to the profile: `profile_status_updated` fires with the new screening status, and when the profile has an onboarding run, `profile_onboarding.completed` fires with the outcome so your onboarding flow can finish. See [Webhooks](/api-reference/webhooks) for the exact values.

<Warning>
  A potential match is a candidate for analyst review. It is not a confirmed
  identity match, a legal conclusion, or an instruction to accept or reject a
  customer. Apply your organization's policies and human-review requirements
  when determining the final disposition.
</Warning>

## Worked example: email invite

An end-to-end walkthrough for one customer, Alex Morgan.

1. Create the profile with the Profiles API:

```bash theme={null}
curl -X POST https://api.gominerva.com/clm/v1/profiles \
  -H "Authorization: Api-Key $MINERVA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Alex Morgan",
    "kind": "individual",
    "email": "alex.morgan@example.com"
  }'
```

Response (abridged):

```json theme={null}
{
  "msg": "OK",
  "result": {
    "profile": {
      "id": "66c391b92888a0db5cc6d3f6",
      "name": "Alex Morgan",
      "status": "not_screened"
    }
  }
}
```

2. Create the IDV session for that profile:

```bash theme={null}
curl -X POST https://api.gominerva.com/idv/v1/sessions \
  -H "Authorization: Api-Key $MINERVA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profile_id": "66c391b92888a0db5cc6d3f6",
    "workflow": "liveness_and_id",
    "idempotency_key": "onboard-alex-001"
  }'
```

Response (abridged):

```json theme={null}
{
  "data": {
    "id": "idv-2cdf8b5c8b3db8a1d6f5d4ce9d46a63f",
    "profile_id": "66c391b92888a0db5cc6d3f6",
    "status": "collecting_artifacts",
    "review_status": "request_sent"
  },
  "session_token": "idvs_9c1f...",
  "token_expires_at": "2026-09-25T19:05:00Z"
}
```

3. Send the invite email:

```bash theme={null}
curl -X POST https://api.gominerva.com/idv/v1/sessions/idv-2cdf8b5c8b3db8a1d6f5d4ce9d46a63f/invite \
  -H "Authorization: Api-Key $MINERVA_API_KEY" \
  -H "Idempotency-Key: onboard-alex-001-invite" \
  -H "Content-Type: application/json" \
  -d '{ "delivery": "email", "locale": "en", "org_name": "Example Bank" }'
```

Response:

```json theme={null}
{
  "invited": true,
  "invite_expires_at": "2026-09-28T18:30:00Z",
  "delivery_state": "sent"
}
```

4. Track progress with `GET /sessions/{sessionId}` or webhooks, and read the results with `GET /sessions/{sessionId}/data` when the assessment is done.

## Worked example: direct link

Use the same first two calls as the email example, then request the link back and hand it over immediately:

```bash theme={null}
curl -X POST https://api.gominerva.com/idv/v1/sessions/idv-2cdf8b5c8b3db8a1d6f5d4ce9d46a63f/invite \
  -H "Authorization: Api-Key $MINERVA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "delivery": "none" }'
```

Response:

```json theme={null}
{
  "verify_url": "https://idv.gominerva.com/verify/idv-2cdf8b5c8b3db8a1d6f5d4ce9d46a63f#code=idvi_...",
  "invite_expires_at": "2026-09-25T19:05:00Z"
}
```

Redirect the browser to `verify_url` or open it in your app's web view right away. The link is single-use and stops working within 15 minutes, so do not queue it, email it, or store it for later.

## Web and mobile integration walkthrough

The common integration looks like this:

```mermaid theme={null}
sequenceDiagram
    participant User as Your user
    participant Backend as Your backend
    participant Minerva as Minerva IDV
    User->>Backend: Taps "Verify my identity"
    Backend->>Minerva: Create profile
    Backend->>Minerva: Create IDV session
    Backend->>Minerva: Send email invite
    Minerva-->>User: Verification email
    User->>Minerva: Consent, capture, submit
    Minerva-->>Backend: IDV webhooks
    Minerva-->>Backend: Onboarding webhook
    Backend-->>User: Verification received
```

Walking through it:

1. The customer taps a button in your web or mobile app. The tap calls your backend, never Minerva directly.
2. Your backend creates the profile (or reuses an existing one), creates the IDV session, and sends the email invite.
3. The customer receives the email, opens the verification page, consents, completes the capture steps, and submits.
4. Minerva assesses the session. Your backend receives IDV webhooks: `idv.session.completed` with the outcome (`idv.session.approved`, `idv.session.requires_review`, or `idv.session.failed`), and `idv.document.captured` for each uploaded artifact.
5. Update the customer's experience: show something like "verification received, we are finishing your setup." Never surface flags, review states, or analyst details to the customer.
6. If the session was held for review, an analyst reviews it in your Minerva workspace and marks the profile accepted or rejected. That disposition closes the loop with `profile_onboarding.completed` (and `profile_status_updated`), your signal to continue or stop the onboarding flow.

Server-side start and invite, in Node.js:

```ts theme={null}
// Server-side only. Never call Minerva from the browser or the app with an API key.
const MINERVA = "https://api.gominerva.com";

export async function startIdentityVerification(input: {
  name: string;
  email: string;
}) {
  const auth = { Authorization: `Api-Key ${process.env.MINERVA_API_KEY}` };

  // 1. Create the profile.
  const profileResponse = await fetch(`${MINERVA}/clm/v1/profiles`, {
    method: "POST",
    headers: { ...auth, "Content-Type": "application/json" },
    body: JSON.stringify({
      name: input.name,
      kind: "individual",
      email: input.email,
    }),
  });
  const { result } = await profileResponse.json();

  // 2. Create the IDV session. Keep the key stable so retries adopt the same
  // session; use a fresh key when you intentionally start a new attempt.
  const sessionResponse = await fetch(`${MINERVA}/idv/v1/sessions`, {
    method: "POST",
    headers: { ...auth, "Content-Type": "application/json" },
    body: JSON.stringify({
      profile_id: result.profile.id,
      workflow: "liveness_and_id",
      idempotency_key: `verify-${result.profile.id}`,
    }),
  });
  const session = await sessionResponse.json();

  // 3. Send the invite email to the customer.
  await fetch(`${MINERVA}/idv/v1/sessions/${session.data.id}/invite`, {
    method: "POST",
    headers: { ...auth, "Content-Type": "application/json" },
    body: JSON.stringify({ delivery: "email" }),
  });

  return session.data.id;
}
```

A webhook receiver that validates the shared key and processes deliveries idempotently:

```ts theme={null}
export async function handleMinervaWebhook(request: Request) {
  // 1. Validate the key on every delivery before reading the body.
  if (
    request.headers.get("x-webhook-key") !== process.env.MINERVA_WEBHOOK_KEY
  ) {
    return new Response("unauthorized", { status: 401 });
  }

  const { event } = await request.json();

  // 2. Deliveries are at-least-once and can arrive out of order, so process
  // every event idempotently.
  if (event.kind === "idv") {
    const { sessionId, profileId, reviewStatus } = event.meta;
    const firstDelivery = await recordEventOnce(
      sessionId,
      event.value,
      event.meta.occurredAt,
    );
    if (firstDelivery) {
      await updateVerificationState(profileId, reviewStatus);
    }
  }

  if (
    event.kind === "onboarding" &&
    event.value === "profile_onboarding.completed"
  ) {
    await finishOnboarding(event.meta.profileId, event.meta.outcome);
  }

  // 3. Acknowledge quickly, and keep the response body small.
  return new Response(null, { status: 204 });
}
```

The `idv` kind sends every `meta` value as a string, so booleans arrive quoted (`"true"`). See [Webhooks](/api-reference/webhooks) for the full event catalog, payload shapes, retry schedule, and dedupe guidance.

## Errors and limits

Errors use one envelope: `{"error": {"code": "...", "message": "..."}}`, with `Cache-Control: no-store`. Common codes:

* **400**: returned as `idv_profile_id_required` or `idv_profile_id_invalid` when `profile_id` is missing or malformed on session create.
* **400**: returned as `idv_profile_not_found` when the profile does not exist in the calling workspace.
* **400**: returned as `schema_validation_failed` when the request body fails schema validation.
* **401**: returned as `missing_authentication` when the request carries no credentials, or as `unauthorized` when the key is rejected. Responses never reveal whether an object exists.
* **403**: returned as `idv_direct_invite_forbidden` when `delivery: "none"` is requested without an application principal.
* **404**: returned as `idv_session_not_found` when the session does not exist in the calling workspace.
* **409**: returned as `idv_state_conflict` when the session is not in a state that allows the action, for example canceling while the assessment runs, or a new invite uses a different `Idempotency-Key` while a send is still in flight.
* **409**: returned as `idv_session_idempotency_conflict` when an idempotency key was reused for a materially different create.
* **409**: returned as `idv_invite_conflict` when the invite state changed before the request completed; retry the request.
* **409**: returned as `idv_review_terminal` when a review decision was submitted for a session that is already accepted or rejected.
* **409**: returned as `idv_review_invalid_transition` when the review decision is not admissible from the session's current review state.
* **422**: returned as `idv_profile_email_missing` when an email invite was requested but the linked profile has no email address.
* **502**: returned as `invite_delivery_failed` or `invite_delivery_retrying` when the email provider could not confirm delivery. The invite stays valid; retry the invite call.
* **503**: returned as `idv_profile_lookup_unavailable` when the profiles service was unavailable and the request failed closed.
* **503**: returned as `idv_direct_invite_unavailable` when no secure public verify base is available for a direct link.

Limits to design around:

* The capture token (`session_token`) lives 15 minutes by default.
* Email invites live 72 hours by default; direct links at most 15 minutes.
* The sessions list returns 50 records per page by default; the workflow and theme lists return 20. Every list endpoint caps at 100 (larger values are clamped).
* Each workspace can keep up to 10 destinations by default. Enterprise customers can request more.

## Security

* Keep application API keys server-side. The hosted verification page authenticates the customer with its own capture or invite credential, so your client apps never need a key.
* Validate `x-webhook-key` on every delivery before processing the body, and acknowledge quickly with a small `2xx` response.
* Read verification results only through the audited `GET /sessions/{sessionId}/data` endpoint, and store what you need under your own data-protection policy. Document images and OCR text are sensitive personal data.
* Never surface review states, flags, or analyst details to the customer; the customer-facing flow deliberately does not expose them.
* Use a separate application per environment, and rotate or deactivate keys when integrations change.

## Related documentation

* [API Keys](/api-reference/api-keys): create an application key and configure webhooks from the Developers page.
* [Webhooks](/api-reference/webhooks): the delivery contract, payload shapes, and retry schedule.
* [Screening Integration Guide](/api-reference/screening-integration-guide): interpret screening results alongside identity verification.
* [Profiles](/minerva-profiles): the customer record an IDV session is tied to.
