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:
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" }
}
}
| Field | Type | Description |
|---|
status | string | Current KYC status (see KYC Statuses) |
applicant_id | string or null | Provider applicant ID. Null if verification not started |
verified_at | integer or null | Approval timestamp (epoch milliseconds) |
rejected_at | integer or null | Rejection timestamp (epoch milliseconds) |
reject_reason | string or null | Human-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" }
}
}
| Field | Type | Description |
|---|
access_token | string | Token to pass to the verification SDK |
expires_in | integer | Token validity in seconds (default 600) |
user_id | string | External user ID associated with this token |
Check KYC Status
Query parameters:
| Parameter | Type | Default | Description |
|---|
refresh | boolean | false | Fetch 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
Returns GDPR-compliant verification details including personal information (name only), document submission status, and rejection details.
Query parameters:
| Parameter | Type | Default | Description |
|---|
refresh | boolean | false | Bypass 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" }
}
}
| Field | Type | Description |
|---|
status | string | Current KYC status |
verified_at | integer or null | Approval timestamp (epoch milliseconds) |
rejected_at | integer or null | Rejection timestamp (epoch milliseconds) |
started_at | integer or null | When verification was started (epoch milliseconds) |
personal_info | object or null | Name and nationality extracted from documents |
documents | array | Submitted documents with type, country, and status |
verification_complete | boolean | Whether all required documents have been submitted |
verification_passed | boolean | Whether verification passed all checks |
rejection | object or null | Rejection details (see below) |
Rejection object:
| Field | Type | Description |
|---|
can_retry | boolean | true if the user can resubmit, false for terminal rejections |
message | string | Human-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:
- Check current status with
GET /v0/kyc/status
- If
pending or retry, generate a token with POST /v0/kyc/access-token
- Initialize the verification SDK with the token
- When the SDK signals completion, poll
GET /v0/kyc/status?refresh=true
- 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:
- Webhook-driven: The verification provider sends a webhook with an approval, and the API advances onboarding automatically
- 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"
}
| Status | Error Code | Meaning | Action |
|---|
| 401 Unauthorized | KYC-401-001 | Webhook signature validation failed | Internal: check webhook secret configuration |
| 403 Forbidden | KYC-403-001 | KYC verification required for this action | Complete KYC verification first |
| 404 Not Found | KYC-404-001 | No KYC applicant record found | Start verification with POST /v0/kyc/access-token |
| 500 Internal Server Error | KYC-500-001 | Internal error processing KYC | Retry; contact support if persistent |
| 500 Internal Server Error | KYC-500-002 | Failed to generate SDK access token | Retry; contact support if persistent |
| 502 Bad Gateway | KYC-502-001 | Verification provider unavailable | Retry after a short delay |
Reference Tables
KYC Statuses
| Value | Description | Terminal? |
|---|
pending | User has not started verification | No |
in_progress | Verification started, awaiting result | No |
retry | Additional information requested — user can resubmit | No |
approved | Verification passed | Yes |
rejected | Verification failed — user cannot retry | Yes |
Document Types
The verification SDK accepts the following document types:
| Value | Description |
|---|
PASSPORT | International passport |
ID_CARD | National identity card |
DRIVERS_LICENSE | Driver’s license |
RESIDENCE_PERMIT | Residence permit |
These fields are populated on the user object after KYC verification:
| Field | Type | Description |
|---|
first_name | string or null | First name from identity document (set on approval) |
last_names | string or null | Last name(s) from identity document (set on approval) |
kyc_verified_at | integer or null | Approval timestamp (epoch milliseconds) |
kyc_rejected_at | integer or null | Rejection timestamp (epoch milliseconds) |
Next Steps