Skip to main content

Identity Verification (KYC)

Overview

The API provides a full KYC (Know Your Customer) identity verification flow powered by an embedded third-party SDK. Your frontend initializes the SDK with a short-lived access token, the user completes document upload and selfie capture inside the SDK, and the API receives status updates asynchronously via webhooks. The KYC system is:
  • SDK-driven — document capture and liveness checks happen inside the embedded SDK, not through your own upload forms
  • Webhook-updated — verification results arrive asynchronously; your client polls for the final outcome
  • GDPR-compliant — only the user’s name is stored locally; all identity documents and PII are held by the verification provider
  • Onboarding-integrated — when KYC is approved while the user is on the kyc_verification onboarding step, onboarding auto-advances to the next step

Quick Start

1. Generate an SDK access token

curl -X POST /v0/kyc/access-token \
  -H "x-juno-jwt: <token>"
{
  "access_token": "sbx:abc123def456...",
  "expires_in": 600,
  "user_id": "usr_abc123",
  "_links": {
    "self": { "href": "/v0/kyc/access-token", "method": "POST" },
    "status": { "href": "/v0/kyc/status" }
  }
}
The token is valid for 600 seconds (10 minutes). Generate a new one if it expires before the user finishes.

2. Initialize the embedded SDK

Pass the access_token to the verification SDK in your frontend. The SDK handles document upload, selfie capture, and liveness detection. Refer to the SDK provider’s documentation for framework-specific integration guides.

3. Poll for verification status

Once the user completes the SDK flow, poll the status endpoint until a terminal state is reached:
curl /v0/kyc/status?refresh=true \
  -H "x-juno-jwt: <token>"
{
  "status": "approved",
  "applicant_id": "65abc123def456",
  "verified_at": 1704067200000,
  "rejected_at": null,
  "reject_reason": null,
  "_links": {
    "self": { "href": "/v0/kyc/status" },
    "user": { "href": "/v0/user/me" },
    "refresh": { "href": "/v0/kyc/status?refresh=true" }
  }
}

4. Onboarding auto-advances

If the user is on the kyc_verification onboarding step when their KYC is approved, onboarding automatically advances to the next step. Re-fetch onboarding state to see the updated step:
curl /v0/user/me?expand=onboarding \
  -H "x-juno-jwt: <token>"

KYC State

Reading Status

Fetch the current KYC status at any time:
GET /v0/kyc/status
Add ?refresh=true to pull the latest result from the verification provider instead of relying on the locally cached state. Use refresh=true during active verification when you need the most current status.

Response Shape

{
  "status": "in_progress",
  "applicant_id": "65abc123def456",
  "verified_at": null,
  "rejected_at": null,
  "reject_reason": null,
  "_links": {
    "self": { "href": "/v0/kyc/status" },
    "user": { "href": "/v0/user/me" },
    "refresh": { "href": "/v0/kyc/status?refresh=true" },
    "start-verification": { "href": "/v0/kyc/access-token", "method": "POST" }
  }
}
FieldTypeDescription
statusstringCurrent KYC status (see KYC Statuses)
applicant_idstring or nullProvider applicant ID. Null if verification not started
verified_atinteger or nullApproval timestamp (epoch milliseconds)
rejected_atinteger or nullRejection timestamp (epoch milliseconds)
reject_reasonstring or nullHuman-readable rejection or retry reason
The _links object includes a start-verification link when the status is pending or retry, guiding the client to generate a new SDK token.

KYC Lifecycle

Status transitions happen asynchronously. The API receives webhook events from the verification provider and updates the user’s KYC state. Your client discovers the new state by polling GET /v0/kyc/status?refresh=true.

Endpoints

Generate SDK Access Token

POST /v0/kyc/access-token
Returns a short-lived access token for initializing the verification SDK in your frontend. Response: 200 OK
{
  "access_token": "sbx:abc123def456...",
  "expires_in": 600,
  "user_id": "usr_abc123",
  "_links": {
    "self": { "href": "/v0/kyc/access-token", "method": "POST" },
    "status": { "href": "/v0/kyc/status" }
  }
}
FieldTypeDescription
access_tokenstringToken to pass to the verification SDK
expires_inintegerToken validity in seconds (default 600)
user_idstringExternal user ID associated with this token

Check KYC Status

GET /v0/kyc/status
Query parameters:
ParameterTypeDefaultDescription
refreshbooleanfalseFetch latest status from the verification provider
Response: 200 OK — see Response Shape above. When refresh=true:
  • If the user has no applicant record, returns 404 Not Found
  • If the provider is unreachable, returns 502 Bad Gateway
  • If KYC is approved and the user is on the kyc_verification onboarding step, onboarding auto-advances

Get KYC Details

GET /v0/kyc/details
Returns GDPR-compliant verification details including personal information (name only), document submission status, and rejection details. Query parameters:
ParameterTypeDefaultDescription
refreshbooleanfalseBypass the 5-minute cache and fetch from the provider
Response: 200 OK
{
  "status": "approved",
  "verified_at": 1704067200000,
  "rejected_at": null,
  "started_at": 1704060000000,
  "personal_info": {
    "first_name": "Jane",
    "last_name": "Smith",
    "date_of_birth": "1990-05-15",
    "nationality": "GBR",
    "country_of_residence": "GBR"
  },
  "documents": [
    {
      "document_type": "PASSPORT",
      "country": "GBR",
      "status": "verified",
      "submitted_at": 1704061000000
    }
  ],
  "verification_complete": true,
  "verification_passed": true,
  "rejection": null,
  "_links": {
    "self": { "href": "/v0/kyc/details" },
    "refresh": { "href": "/v0/kyc/details?refresh=true" },
    "status": { "href": "/v0/kyc/status" },
    "user": { "href": "/v0/user/me" }
  }
}
FieldTypeDescription
statusstringCurrent KYC status
verified_atinteger or nullApproval timestamp (epoch milliseconds)
rejected_atinteger or nullRejection timestamp (epoch milliseconds)
started_atinteger or nullWhen verification was started (epoch milliseconds)
personal_infoobject or nullName and nationality extracted from documents
documentsarraySubmitted documents with type, country, and status
verification_completebooleanWhether all required documents have been submitted
verification_passedbooleanWhether verification passed all checks
rejectionobject or nullRejection details (see below)
Rejection object:
FieldTypeDescription
can_retrybooleantrue if the user can resubmit, false for terminal rejections
messagestringHuman-readable rejection reason
The personal_info fields other than name (date_of_birth, nationality, country_of_residence) are fetched from the provider on each request and are not stored locally. They are only available while the provider retains the applicant data.

Integration Patterns

Building a KYC Flow

A typical frontend integration follows this sequence:
  1. Check current status with GET /v0/kyc/status
  2. If pending or retry, generate a token with POST /v0/kyc/access-token
  3. Initialize the verification SDK with the token
  4. When the SDK signals completion, poll GET /v0/kyc/status?refresh=true
  5. If approved, proceed to the next step. If retry, show the rejection message and let the user try again
status = GET /v0/kyc/status

if status == "pending" or status == "retry":
    token = POST /v0/kyc/access-token
    launch_sdk(token.access_token)
    await sdk_completion()

    while status not in ["approved", "rejected"]:
        wait(3 seconds)
        status = GET /v0/kyc/status?refresh=true

if status == "approved":
    proceed_to_next_step()
elif status == "rejected":
    show_rejection_message(status.reject_reason)
elif status == "retry":
    show_retry_prompt(status.reject_reason)

Polling for Approval

After the user completes the SDK flow, the verification result arrives asynchronously via webhook. Poll the status endpoint with refresh=true at a reasonable interval until a terminal state is reached:
# First poll — may still be in_progress
curl /v0/kyc/status?refresh=true \
  -H "x-juno-jwt: <token>"

# Response: { "status": "in_progress", ... }

# Wait 3-5 seconds, then poll again
curl /v0/kyc/status?refresh=true \
  -H "x-juno-jwt: <token>"

# Response: { "status": "approved", "verified_at": 1704067200000, ... }
Avoid polling more frequently than every 3 seconds. Most verifications complete within 30 seconds to 2 minutes, but some require manual review and may take longer.

Handling Rejection

There are two rejection outcomes with different behaviors: Retry (status: "retry") — the user can fix and resubmit. The reject_reason field explains what needs correction. Generate a new access token and let the user re-enter the SDK flow. Rejected (status: "rejected") — terminal. The user cannot retry with the same identity documents. Display the reject_reason and direct them to contact support.
# Retry scenario
curl /v0/kyc/status \
  -H "x-juno-jwt: <token>"
{
  "status": "retry",
  "applicant_id": "65abc123def456",
  "verified_at": null,
  "rejected_at": null,
  "reject_reason": "Document image is blurry. Please resubmit a clear photo.",
  "_links": {
    "self": { "href": "/v0/kyc/status" },
    "user": { "href": "/v0/user/me" },
    "refresh": { "href": "/v0/kyc/status?refresh=true" },
    "start-verification": { "href": "/v0/kyc/access-token", "method": "POST" }
  }
}
When the status is retry, the _links object includes start-verification so your client can initiate a new SDK session.

Onboarding Integration

KYC verification is one step in the onboarding flow. When KYC is approved and the user is on the kyc_verification onboarding step, the step auto-advances without requiring an explicit submit call. This happens in two scenarios:
  1. Webhook-driven: The verification provider sends a webhook with an approval, and the API advances onboarding automatically
  2. Poll-driven: Your client calls GET /v0/kyc/status?refresh=true, the API detects approval, and advances onboarding
In both cases, re-fetch onboarding state afterward to render the next step:
curl /v0/user/me?expand=onboarding \
  -H "x-juno-jwt: <token>"

Error Handling

All error responses follow the RFC 7807 Problem Details format:
{
  "type": "https://api.sumvin.com/errors/kyc-500-002",
  "title": "KYC Token Generation Failed",
  "status": 500,
  "detail": "Failed to generate KYC access token",
  "instance": "/v0/kyc/access-token",
  "error_code": "KYC-500-002"
}
StatusError CodeMeaningAction
401 UnauthorizedKYC-401-001Webhook signature validation failedInternal: check webhook secret configuration
403 ForbiddenKYC-403-001KYC verification required for this actionComplete KYC verification first
404 Not FoundKYC-404-001No KYC applicant record foundStart verification with POST /v0/kyc/access-token
500 Internal Server ErrorKYC-500-001Internal error processing KYCRetry; contact support if persistent
500 Internal Server ErrorKYC-500-002Failed to generate SDK access tokenRetry; contact support if persistent
502 Bad GatewayKYC-502-001Verification provider unavailableRetry after a short delay

Reference Tables

KYC Statuses

ValueDescriptionTerminal?
pendingUser has not started verificationNo
in_progressVerification started, awaiting resultNo
retryAdditional information requested — user can resubmitNo
approvedVerification passedYes
rejectedVerification failed — user cannot retryYes

Document Types

The verification SDK accepts the following document types:
ValueDescription
PASSPORTInternational passport
ID_CARDNational identity card
DRIVERS_LICENSEDriver’s license
RESIDENCE_PERMITResidence permit
These fields are populated on the user object after KYC verification:
FieldTypeDescription
first_namestring or nullFirst name from identity document (set on approval)
last_namesstring or nullLast name(s) from identity document (set on approval)
kyc_verified_atinteger or nullApproval timestamp (epoch milliseconds)
kyc_rejected_atinteger or nullRejection timestamp (epoch milliseconds)

Next Steps