Verify API
The Verify API powers KYC verification. Organizations register, create verification sessions, and receive results through webhooks or polling. End users complete verification through a hosted flow or direct API calls.
Base URL: https://verify.solidus.network/v1
Authentication
Register
Create a new organization account.
POST /auth/register
Auth: None
// Request body
{
"email": "[email protected]",
"password": "secureP@ssw0rd!",
"name": "Acme Corp"
}// Response 201
{
"token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "usr_8Kj2mNpQxR",
"email": "[email protected]",
"name": "Acme Corp",
"organizationId": "org_3Vn9wLkDfT",
"createdAt": "2026-05-07T10:30:00Z"
}
}Login
Authenticate with email and password. If TOTP is enabled, a totpCode is required.
POST /auth/login
Auth: None
// Request body
{
"email": "[email protected]",
"password": "secureP@ssw0rd!"
}// Response 200 — success
{
"token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "usr_8Kj2mNpQxR",
"email": "[email protected]",
"name": "Acme Corp",
"organizationId": "org_3Vn9wLkDfT"
}
}// Response 200 — TOTP required
{
"totpRequired": true
}When totpRequired is true, resend the login request with the totpCode field.
// Request body with TOTP
{
"email": "[email protected]",
"password": "secureP@ssw0rd!",
"totpCode": "482901"
}Get Current User
Retrieve the authenticated user and organization details.
GET /auth/me
Auth: JWT Bearer
curl https://verify.solidus.network/v1/auth/me \
-H "Authorization: Bearer eyJhbGciOiJFZERTQSIs..."// Response 200
{
"organization": {
"id": "org_3Vn9wLkDfT",
"name": "Acme Corp",
"email": "[email protected]",
"plan": "starter",
"totpEnabled": false,
"createdAt": "2026-05-07T10:30:00Z"
}
}Setup TOTP
Generate a TOTP secret and QR code for two-factor authentication.
POST /auth/totp/setup
Auth: JWT Bearer
// Response 200
{
"secret": "JBSWY3DPEHPK3PXP",
"qrCode": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..."
}Enable TOTP
Confirm TOTP setup by providing the secret and a valid code from the authenticator app.
POST /auth/totp/enable
Auth: JWT Bearer
// Request body
{
"secret": "JBSWY3DPEHPK3PXP",
"totpCode": "482901"
}// Response 200
{
"message": "TOTP enabled successfully"
}Forgot Password
Send a password reset email.
POST /auth/forgot-password
Auth: None
// Request body
{
"email": "[email protected]"
}// Response 200
{
"message": "If an account with that email exists, a reset link has been sent"
}Reset Password
Set a new password using the token from the reset email.
POST /auth/reset-password
Auth: None
// Request body
{
"token": "rst_7Hk3mRtQZvXp2...",
"password": "newSecureP@ssw0rd!"
}// Response 200
{
"message": "Password reset successfully"
}OAuth Login
Redirect the user to Google or GitHub for OAuth authentication. After authorization, the user is redirected back with a JWT.
GET /auth/google — Redirects to Google OAuth
GET /auth/github — Redirects to GitHub OAuth
Verification Sessions
Create a Session
Create a new verification session. The response includes a sessionUrl that you can redirect users to for the hosted verification flow.
POST /verifications
Auth: API Key (x-api-key)
// Request body
{
"level": 2,
"sandbox": false,
"redirectUrl": "https://acme.com/onboarding/complete",
"subjectDid": "did:solidus:testnet:7Hk3mRtQZvXp2nBw9Lj4K",
"metadata": {
"userId": "usr_acme_12345",
"plan": "premium"
}
}| Field | Type | Required | Description |
|---|---|---|---|
level | 1 | 2 | 3 | Yes | Verification level. 1 = document only, 2 = document + liveness, 3 = document + liveness + address proof |
sandbox | boolean | No | If true, runs in sandbox mode with no real verification |
sandboxOutcome | string | No | Force a sandbox result: "pass", "fail", "timeout", "document_rejected" |
subjectDid | string | No | Associate the session with a specific DID |
redirectUrl | string | No | URL to redirect the user after completion |
metadata | object | No | Arbitrary key-value pairs stored with the session |
// Response 201
{
"session": {
"id": "ver_9Xm4pLkWnR",
"organizationId": "org_3Vn9wLkDfT",
"status": "pending",
"level": 2,
"sandbox": false,
"subjectDid": "did:solidus:testnet:7Hk3mRtQZvXp2nBw9Lj4K",
"redirectUrl": "https://acme.com/onboarding/complete",
"metadata": {
"userId": "usr_acme_12345",
"plan": "premium"
},
"sessionToken": "sess_tok_7Hk3mRtQZv...",
"createdAt": "2026-05-07T14:20:00Z",
"expiresAt": "2026-05-07T15:20:00Z"
},
"sessionUrl": "https://verify.solidus.network/v/s/sess_tok_7Hk3mRtQZv..."
}List Sessions
Retrieve all verification sessions for your organization.
GET /verifications
Auth: API Key (x-api-key)
| Query Param | Type | Description |
|---|---|---|
status | string | Filter by status: pending, processing, completed, failed, expired |
limit | number | Results per page (default: 20, max: 100) |
offset | number | Offset for pagination |
curl "https://verify.solidus.network/v1/verifications?status=approved&limit=10" \
-H "x-api-key: sk_live_abc123def456..."// Response 200
{
"sessions": [
{
"id": "ver_9Xm4pLkWnR",
"status": "completed",
"level": 2,
"subjectDid": "did:solidus:testnet:7Hk3mRtQZvXp2nBw9Lj4K",
"credentialId": "vc_3Kn8rTpXm2",
"createdAt": "2026-05-07T14:20:00Z",
"completedAt": "2026-05-07T14:25:30Z"
}
],
"total": 1
}Get Session
Retrieve a single verification session by ID.
GET /verifications/:id
Auth: API Key (x-api-key)
curl https://verify.solidus.network/v1/verifications/ver_9Xm4pLkWnR \
-H "x-api-key: sk_live_abc123def456..."// Response 200
{
"session": {
"id": "ver_9Xm4pLkWnR",
"organizationId": "org_3Vn9wLkDfT",
"status": "completed",
"level": 2,
"sandbox": false,
"subjectDid": "did:solidus:testnet:7Hk3mRtQZvXp2nBw9Lj4K",
"redirectUrl": "https://acme.com/onboarding/complete",
"metadata": {
"userId": "usr_acme_12345",
"plan": "premium"
},
"documents": [
{
"type": "passport",
"side": "front",
"status": "verified",
"extractedData": {
"firstName": "John",
"lastName": "Doe",
"dateOfBirth": "1990-03-15",
"documentNumber": "AB1234567",
"expiryDate": "2030-06-20",
"nationality": "US"
}
}
],
"liveness": {
"status": "passed",
"score": 0.97,
"completedAt": "2026-05-07T14:24:10Z"
},
"credentialId": "vc_3Kn8rTpXm2",
"createdAt": "2026-05-07T14:20:00Z",
"completedAt": "2026-05-07T14:25:30Z"
}
}Hosted Flow
These endpoints are used by the hosted verification UI. They authenticate with the session token embedded in the URL path rather than an API key.
Get Session by Token
Retrieve the session for the hosted flow.
GET /verifications/s/:sessionToken
Auth: Session token (in URL)
// Response 200
{
"session": {
"id": "ver_9Xm4pLkWnR",
"status": "pending",
"level": 2,
"organizationName": "Acme Corp",
"steps": ["document", "liveness"],
"currentStep": "document",
"expiresAt": "2026-05-07T15:20:00Z"
}
}Upload Document (Hosted)
Upload an identity document for the current session.
POST /verifications/s/:sessionToken/documents
Auth: Session token (in URL)
Content-Type: multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
file | File | Yes | The document image (JPEG or PNG, max 10 MB) |
side | string | Yes | front or back |
type | string | Yes | passport, driving_license, national_id, or residence_permit |
final | boolean | No | If true, marks the document step as complete |
curl -X POST "https://verify.solidus.network/v1/verifications/s/sess_tok_7Hk3mRtQZv.../documents" \
-F "file=@passport_front.jpg" \
-F "side=front" \
-F "type=passport" \
-F "final=true"// Response 201
{
"document": {
"id": "doc_5Wn2kLpMxR",
"type": "passport",
"side": "front",
"status": "uploaded",
"createdAt": "2026-05-07T14:22:00Z"
}
}Get Liveness Challenge (Hosted)
Request a liveness challenge for the session.
GET /verifications/s/:sessionToken/liveness-challenge
Auth: Session token (in URL)
// Response 200
{
"challenge": {
"id": "chl_8Rn3mKpWxT",
"type": "head_turn",
"instructions": "Please turn your head slowly to the left",
"expiresAt": "2026-05-07T14:30:00Z"
}
}Submit Liveness (Hosted)
Submit liveness frames for the challenge.
POST /verifications/s/:sessionToken/liveness
Auth: Session token (in URL)
Content-Type: multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
frames | File[] | Yes | Video frames captured during the liveness challenge |
// Response 200
{
"liveness": {
"status": "passed",
"score": 0.97
}
}Document Upload (API Key)
Upload documents and complete liveness checks using your API key instead of the hosted flow.
Upload Document
POST /verifications/:id/documents
Auth: API Key (x-api-key)
Content-Type: multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
file | File | Yes | The document image (JPEG or PNG, max 10 MB) |
side | string | Yes | front or back |
type | string | Yes | passport, driving_license, national_id, or residence_permit |
final | boolean | No | If true, marks the document step as complete and triggers processing |
curl -X POST "https://verify.solidus.network/v1/verifications/ver_9Xm4pLkWnR/documents" \
-H "x-api-key: sk_live_abc123def456..." \
-F "file=@passport_front.jpg" \
-F "side=front" \
-F "type=passport" \
-F "final=true"// Response 201
{
"document": {
"id": "doc_5Wn2kLpMxR",
"type": "passport",
"side": "front",
"status": "uploaded",
"createdAt": "2026-05-07T14:22:00Z"
}
}Get Liveness Challenge
GET /verifications/:id/liveness-challenge
Auth: API Key (x-api-key)
// Response 200
{
"challenge": {
"id": "chl_8Rn3mKpWxT",
"type": "head_turn",
"instructions": "Please turn your head slowly to the left",
"expiresAt": "2026-05-07T14:30:00Z"
}
}Submit Liveness
POST /verifications/:id/liveness
Auth: API Key (x-api-key)
Content-Type: multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
frames | File[] | Yes | Video frames captured during the liveness challenge |
// Response 200
{
"liveness": {
"status": "passed",
"score": 0.97
}
}API Keys
Manage API keys for your organization. Each key is scoped to live or sandbox mode.
Create API Key
POST /api-keys
Auth: JWT Bearer
// Request body
{
"name": "Production Key",
"mode": "live"
}// Response 201
{
"key": "sk_live_abc123def456ghi789jkl012mno345pqr678",
"id": "key_4Tn7mLkPxR"
}The full key is only returned once at creation. Store it securely.
List API Keys
GET /api-keys
Auth: JWT Bearer
// Response 200
{
"keys": [
{
"id": "key_4Tn7mLkPxR",
"name": "Production Key",
"mode": "live",
"prefix": "sk_live_abc123...",
"lastUsedAt": "2026-05-07T14:00:00Z",
"createdAt": "2026-05-01T09:00:00Z"
},
{
"id": "key_6Wn9pMkRxT",
"name": "Test Key",
"mode": "sandbox",
"prefix": "sk_sandbox_xyz789...",
"lastUsedAt": null,
"createdAt": "2026-05-03T11:00:00Z"
}
]
}Delete API Key
DELETE /api-keys/:keyId
Auth: JWT Bearer
curl -X DELETE https://verify.solidus.network/v1/api-keys/key_4Tn7mLkPxR \
-H "Authorization: Bearer eyJhbGciOiJFZERTQSIs..."// Response 200
{
"message": "API key deleted"
}Webhooks
Receive real-time notifications when verification events occur. Webhook payloads are signed with HMAC-SHA256 for verification.
Create Webhook Endpoint
POST /webhooks
Auth: JWT Bearer
// Request body
{
"url": "https://acme.com/webhooks/solidus",
"events": [
"verification.completed",
"verification.failed",
"verification.expired"
],
"description": "Production webhook for verification results"
}Available events: verification.completed, verification.failed, verification.expired, credential.issued, credential.revoked. Default (if events is omitted): verification.completed, credential.issued.
// Response 201
{
"endpoint": {
"id": "wh_2Kn5mTpLxR",
"url": "https://acme.com/webhooks/solidus",
"events": [
"verification.completed",
"verification.failed",
"verification.expired"
],
"description": "Production webhook for verification results",
"secret": "whsec_7Hk3mRtQZvXp2nBw9Lj4K...",
"enabled": true,
"createdAt": "2026-05-07T10:00:00Z"
}
}The secret is only returned once. Use it to verify webhook signatures.
List Webhook Endpoints
GET /webhooks
Auth: JWT Bearer
// Response 200
{
"endpoints": [
{
"id": "wh_2Kn5mTpLxR",
"url": "https://acme.com/webhooks/solidus",
"events": ["verification.completed", "verification.failed", "verification.expired"],
"description": "Production webhook for verification results",
"enabled": true,
"createdAt": "2026-05-07T10:00:00Z"
}
]
}Update Webhook Endpoint
PATCH /webhooks/:endpointId
Auth: JWT Bearer
// Request body
{
"url": "https://acme.com/webhooks/solidus-v2",
"events": ["verification.completed", "verification.failed"],
"enabled": true
}All fields are optional. Only provided fields are updated.
// Response 200
{
"endpoint": {
"id": "wh_2Kn5mTpLxR",
"url": "https://acme.com/webhooks/solidus-v2",
"events": ["verification.completed", "verification.failed"],
"description": "Production webhook for verification results",
"enabled": true,
"createdAt": "2026-05-07T10:00:00Z"
}
}Delete Webhook Endpoint
DELETE /webhooks/:endpointId
Auth: JWT Bearer
curl -X DELETE https://verify.solidus.network/v1/webhooks/wh_2Kn5mTpLxR \
-H "Authorization: Bearer eyJhbGciOiJFZERTQSIs..."// Response 200
{
"message": "Webhook endpoint deleted"
}List Webhook Deliveries
View delivery history for a specific endpoint.
GET /webhooks/:endpointId/deliveries
Auth: JWT Bearer
| Query Param | Type | Description |
|---|---|---|
status | string | Filter by status: success, failed, pending |
limit | number | Results per page (default: 20, max: 100) |
offset | number | Offset for pagination |
// Response 200
{
"deliveries": [
{
"id": "dlv_9Xm4pLkWnR",
"event": "verification.completed",
"status": "success",
"statusCode": 200,
"payload": {
"event": "verification.completed",
"data": {
"sessionId": "ver_9Xm4pLkWnR",
"status": "completed",
"credentialId": "vc_3Kn8rTpXm2"
},
"timestamp": "2026-05-07T14:25:30Z"
},
"attemptCount": 1,
"deliveredAt": "2026-05-07T14:25:31Z"
}
],
"total": 1
}Bridge Export
Verify issues credentials, and three routes copy the status of one of them to another chain through Solidus Bridge . The paths below sit under /v1/bridge. They are not part of the read-only Bridge API, which only reports what happened. Everything here runs on testnets.
An export needs proof that the caller holds the credential. Verify mints a challenge, Solidus Identity signs a presentation over it with the holder’s DID key, and Verify checks the presentation before it submits to Solidus as the issuer. A challenge works once, within five minutes. How to export a credential walks through the holder’s side.
Create a Challenge
POST /bridge/challenge
Auth: none. Limited to 10 requests a minute per client.
// Request body
{}// Response 200
{
"challenge": "<one-time challenge>"
}Export a Credential
POST /bridge/exports
Auth: none. The presentation is the proof.
// Request body
{
"presentation": "<presentation signed by Solidus Identity>",
"challenge": "<from POST /bridge/challenge>",
"credentialId": "urn:solidus:credential:EXAMPLE",
"domain": 11155111,
"holder": "0x0000000000000000000000001111111111111111111111111111111111111111",
"validUntil": 0,
"consentSignature": "0x...",
"consentExpiry": 1900000000
}Every field is required, and a field not listed here is rejected.
| Field | Meaning |
|---|---|
presentation | The proof from Solidus Identity, signed over the challenge. |
challenge | The challenge Verify issued for this presentation. |
credentialId | The credential to export. |
domain | The destination. On EVM it equals the chain ID. GET https://bridge.solidus.network/v1/routes lists them. |
holder | 0x plus 64 hex characters: the holder’s EVM address padded with zeros, or a Solana public key. |
validUntil | When the destination stops honoring the export, in Unix seconds. 0 means none. |
consentSignature | The holder’s consent: EIP-712 typed data on EVM, an ed25519 signature on Solana. exportConsentTypedData and ed25519ConsentBytes in @solidus-network/bridge build what is signed. |
consentExpiry | When the consent stops being valid, in Unix seconds. It must be in the future. |
// Response 202
{
"txHash": "0x...",
"exportId": "0x..."
}A 202 means Solidus accepted the transaction, not that the destination applied it. Follow the export with GET /v1/credentials/{exportId} on the Bridge API.
Take an Export Back
POST /bridge/unexports
Auth: none. The presentation is the proof.
// Request body
{
"presentation": "<presentation signed by Solidus Identity>",
"challenge": "<from POST /bridge/challenge>",
"credentialId": "urn:solidus:credential:EXAMPLE",
"domain": 11155111,
"holder": "0x0000000000000000000000001111111111111111111111111111111111111111"
}The fields mean what they mean for an export. An unexport needs no consent and no validUntil. It succeeds only for an active export; Solidus then queues an unbound status for the destination. The response is the same 202 with txHash and exportId.
Bridge Export Errors
Errors are application/problem+json with type, title, status, code and detail.
| Status | code | Meaning |
|---|---|---|
| 400 | invalid_request | The body is not valid. detail names the first field and the reason. |
| 400 | unknown_domain | domain is not a registered, enabled destination. |
| 400 | invalid_consent | The consent has expired, is malformed, does not verify for this holder, or is for a chain type that is not supported. |
| 401 | invalid_presentation | The presentation did not verify against the challenge. |
| 403 | not_credential_subject | The presentation is not from the credential’s subject. |
| 404 | bridge_disabled | Bridge export is switched off on this deployment. |
| 409 | issuer_not_served | This Verify instance did not issue the credential. |
| 409 | already_exported | The credential is already exported to this holder and domain. |
| 409 | chain_refused | Solidus refused the transaction. detail carries the reason. |
| 429 | More than 10 challenges a minute from one client. |
A 500 means Solidus could not be reached; the request can be repeated.