---
name: verihop-integration
description: Integrate a product with Verihop's personal identity Sessions API. Use when adding or reviewing a Verihop test or live integration, creating server-side sessions, launching web, QR, Android, or native iOS App Clip flows, handling callbacks and one-time results, or producing secure Node.js, Python, Swift, Kotlin, Java, Go, PHP, Ruby, or cURL implementation guidance.
---

# Verihop Integration

Use Verihop to request only the personal identity fields a service needs, let the person complete verification in Verihop, and retrieve the user-approved result from the partner backend. Keep identity verification, documents, portraits, biometrics, API keys, session tokens, and result tokens out of client logs and analytics.

## Use the production contract

- Use `https://api.verihop.com` and `/v1/...` routes for every new integration.
- Treat `https://www.verihop.com/SKILL.md` as the public, versioned implementation guide. Use the customer Portal for the customer's key, callback domains, test setup, and live approval.
- Keep the partner API key on the partner backend. Never ship it in JavaScript, an iOS app, an Android app, a desktop app, a QR code, or a mobile configuration file.
- Request only fields that are necessary for the stated user journey. Do not invent request tokens or rely on unpublished response fields.
- Build the standard Sessions API flow unless Verihop has explicitly approved a different product profile for the customer and use case.

## Follow this end-to-end flow

1. Create a Verihop test account in the Portal and create a test API key.
2. Configure a callback host that the customer controls. Use a backend HTTPS endpoint whenever possible.
3. From the partner backend, create a session with `POST /v1/sessions`, a UUID `Idempotency-Key`, a clear user-facing header, the minimum fields, and the callback URL.
4. Return only the safe launch URLs and session ID to the partner client.
5. Launch the returned URL using the route matrix below. Do not reconstruct launch URLs or extract their tokens.
6. Receive the callback, validate its scheme/host, and forward `session_id`, `result_token`, and optional `callback_jwt` to the partner backend.
7. On the partner backend, exchange the one-time `result_token` through `GET /v1/sessions/{id}/result`.
8. Store or use only the approved result fields needed for the agreed purpose. Mark the local checkout or workflow complete only after the backend has accepted the result.
9. Before live traffic, complete the customer Portal go-live review, confirm the production callback allowlist and production key, and run the acceptance checklist below.

## Create the session on the backend

Send this request from a trusted server only:

```http
POST https://api.verihop.com/v1/sessions
Authorization: Bearer <api_key>
Content-Type: application/json
Idempotency-Key: <new-uuid-for-this-logical-request>
```

```json
{
  "app": "Partner App",
  "header": "Confirm your age",
  "fields": ["legalName", "over18"],
  "callback": "https://partner.example/verihop/callback"
}
```

Use the same idempotency key only when safely retrying the exact same payload. Generate a new key for a new user action or changed request. A reused key with a different body returns `409 idempotency_key_conflict`; an in-flight request can return `409 idempotency_key_in_progress`.

Expect a `201` response containing only orchestration values:

```json
{
  "session_id": "sess_123",
  "expires_at": 1730000000,
  "launch_url": "https://www.verihop.com/verify.html?session_id=sess_123&token=h_abc123",
  "app_clip_url": "https://appclip.apple.com/id?p=KycBox.VerifyBox.Clip&session_id=sess_123&token=h_abc123",
  "qr_url": "https://www.verihop.com/verify.html?session_id=sess_123&token=h_abc123",
  "status_url": "https://api.verihop.com/v1/sessions/sess_123"
}
```

Do not expect a JWT, identity data, document image, portrait, MRZ, raw NFC data, or biometric evidence in this response.

## Choose the correct launch URL

| Surface | Open | Implementation rule |
| --- | --- | --- |
| Website, email, SMS, QR, kiosk, support link | `launch_url` or `qr_url` | Redirect/open it unchanged. `qr_url` is presently equivalent to `launch_url`. |
| Android app | `launch_url` | Use an ordinary `ACTION_VIEW` intent. Keep the API key and result exchange on the backend. |
| Native iOS app, iOS 17+ | `app_clip_url` | Open it unchanged to invoke Apple’s native App Clip card. Do not parse its token or hard-code the App Clip bundle ID. |
| Native iOS app, fallback | `launch_url` | Use it if the App Clip surface is not applicable. |

Apple distributes the public App Clip binary. A Local App Clip Experience is only a Verihop development aid; partners do not configure one and must use the backend-returned `app_clip_url`.

## Handle the callback and result safely

Verihop opens the callback URL supplied when the session was created. A successful callback contains:

```text
https://partner.example/verihop/callback?session_id=sess_123&result_token=res_ABC123&callback_jwt=<optional>
```

- Treat callback query values as transport metadata, never as the verified personal result.
- Validate the callback URL scheme and host before handling it. Register a custom URL scheme/intent filter only if the partner deliberately uses one.
- Send the values to the partner backend over HTTPS. Do not fetch the result from the mobile app.
- When `callback_jwt` is present and enabled for the customer, validate it on the partner backend using `GET https://api.verihop.com/.well-known/jwks.json` before trusting callback metadata.
- Do not validate the internal session JWT returned for Verihop’s holder app; partners normally use only the callback JWT when enabled.

Exchange the result once:

```http
GET https://api.verihop.com/v1/sessions/sess_123/result
Authorization: Bearer <api_key>
X-Result-Token: res_ABC123
```

```json
{
  "session_id": "sess_123",
  "status": "success",
  "result": {
    "legalName": "Jane Doe",
    "over18": "yes"
  }
}
```

The result token is short-lived and single-use. A second read returns `409 result_token_used`; expired tokens return `410 result_token_expired`. Create a fresh session if a person abandons or expires a flow. Optionally poll `GET /v1/sessions/{id}` with the API key for a non-PII status snapshot; do not poll for identity data.

## Request only governed fields

Use only these V1 request tokens:

`legalName`, `dateOfBirth`, `over18`, `over21`, `passport`, `idCard`, `documentNumber`, `documentExpiry`, `documentCountry`, `documentNationality`.

- Prefer `over18` or `over21` instead of date of birth when an age threshold is sufficient.
- `passport` and `idCard` are typed document requirements. They do not share document images, portraits, MRZ images, raw NFC data, signature material, or biometric evidence.
- Use granular document fields only when the product needs them.
- Do not request compatibility-only or unpublished tokens such as `portrait`, `address`, `residence`, `nationality`, `documentType`, or `document`.

## Implement common platforms

### Node.js / TypeScript backend

```ts
const api = 'https://api.verihop.com';

export async function createVerihopSession() {
  const response = await fetch(`${api}/v1/sessions`, {
    method: 'POST',
    headers: {
      authorization: `Bearer ${process.env.VERIHOP_API_KEY}`,
      'content-type': 'application/json',
      'idempotency-key': crypto.randomUUID()
    },
    body: JSON.stringify({
      app: 'Partner App',
      header: 'Confirm your age',
      fields: ['over18'],
      callback: 'https://partner.example/verihop/callback'
    })
  });
  if (!response.ok) throw new Error(`Verihop session: ${response.status}`);
  return response.json();
}

export async function fetchVerihopResult(sessionId: string, resultToken: string) {
  const response = await fetch(`${api}/v1/sessions/${encodeURIComponent(sessionId)}/result`, {
    headers: {
      authorization: `Bearer ${process.env.VERIHOP_API_KEY}`,
      'x-result-token': resultToken
    }
  });
  if (!response.ok) throw new Error(`Verihop result: ${response.status}`);
  return response.json();
}
```

### Python backend

```python
import os, uuid, requests

API = "https://api.verihop.com"
HEADERS = {"Authorization": f"Bearer {os.environ['VERIHOP_API_KEY']}"}

def create_session():
    response = requests.post(
        f"{API}/v1/sessions",
        headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())},
        json={"app": "Partner App", "header": "Confirm your age",
              "fields": ["over18"],
              "callback": "https://partner.example/verihop/callback"},
        timeout=15,
    )
    response.raise_for_status()
    return response.json()
```

### Swift client launch

Create the session through the partner backend. Pass the returned URL to the app; do not embed an API key in iOS.

```swift
import UIKit

func openVerihop(appClipURL: URL?, launchURL: URL) {
    let destination = appClipURL ?? launchURL
    UIApplication.shared.open(destination)
}
```

Use the exact returned `app_clip_url` on iOS 17 or later when the native App Clip card is wanted. Handle the callback by forwarding its safe metadata to the partner backend.

### Kotlin Android client launch

Create the session through the partner backend. Open the returned `launch_url` unchanged:

```kotlin
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(session.launch_url)))
```

If using a custom callback scheme, declare and validate the partner app’s own intent filter. Send callback values to the backend for the result exchange.

### Java backend

Use `java.net.http.HttpClient` from a server process. Send the bearer API key and idempotency key as headers, serialize the session body as JSON, and keep the JSON response server-side until returning only `session_id` and launch URLs to the client.

### Go backend

Use `net/http` from a server process. Set `Authorization`, `Content-Type`, and a new UUID `Idempotency-Key`; decode the `201` JSON into a session struct. Use a separate authenticated server request with `X-Result-Token` to retrieve the result.

### PHP or Ruby backend

Use the server-side HTTP client already standard in the application (`Guzzle`/cURL for PHP, `Net::HTTP`/Faraday for Ruby). Follow the same two server-side requests: create session with bearer auth and idempotency, then exchange the result token once. Never place the key in browser-visible code.

### cURL smoke request

```bash
curl -X POST 'https://api.verihop.com/v1/sessions' \
  -H 'Authorization: Bearer <api_key>' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: <uuid>' \
  --data '{"app":"Partner App","header":"Confirm your age","fields":["over18"],"callback":"https://partner.example/verihop/callback"}'
```

## Handle errors and retries deliberately

| Response | Meaning | Required action |
| --- | --- | --- |
| `401 missing_bearer_token` or `invalid_api_key` | Missing or invalid server credential | Stop; verify the server secret and environment. |
| `403 customer_inactive` or `api_key_environment_mismatch` | Tenant is not live or key is for another environment | Use the correct test/live key or complete live approval. |
| `400 callback_allowlist_required` or `callback_host_not_allowed` | Callback host is not approved | Add the exact controlled host through the Portal/go-live process. |
| `400 field_not_allowed` | Requested field is outside the customer’s approved catalog | Request only governed, approved fields. |
| `409 idempotency_key_conflict` | Same key, different body | Generate a new key for the changed request. |
| `409 idempotency_key_in_progress` | Safe retry still processing | Retry the exact same request with bounded backoff. |
| `409 result_token_used` or `410 result_token_expired` | Result exchange cannot be replayed | Create a new session; do not retry with a different token. |

Do not retry invalid credentials, unapproved callback hosts, unapproved fields, or consumed tokens. Log only a request correlation ID, HTTP status, route, and error code—not keys, tokens, identity data, documents, or callbacks containing secrets.

## Move from test to live

1. Prove the complete test path: create session, launch, callback, backend result exchange, and application state update.
2. Keep test results expectedly redacted. Use the same customer account to request activation rather than creating a second integration.
3. Configure the production callback domains the customer owns. Confirm every hostname, including `www` variants, is correct.
4. Ensure the customer completes the Portal’s required account-security/MFA and go-live steps; use the approved production key only after activation.
5. Run one controlled production acceptance: web/QR or Android `launch_url`, native iOS `app_clip_url` if applicable, callback, and one-time result retrieval.
6. Monitor error codes and completion rates without retaining raw identity or biometric material. Rotate keys through the Portal if exposure is suspected.

## Finish with this acceptance checklist

- [ ] API key exists only in server-side secret management.
- [ ] Session creation uses a new UUID idempotency key and bounded retry for the exact payload only.
- [ ] Callback host is owned by the customer and approved for the right environment.
- [ ] Web, QR, Android, and iOS each use the correct returned URL without parsing it.
- [ ] Callback handling validates scheme/host and forwards metadata to the backend.
- [ ] Result exchange happens exactly once on the backend.
- [ ] Only approved fields are requested, displayed, stored, and used.
- [ ] Logs, analytics, support tooling, and crash reports omit API keys, tokens, document data, portraits, and biometric material.
- [ ] Test-to-live activation and a controlled live end-to-end pass are recorded.

