KYC flow
Verify a user's identity with Sumsub before they can transact — link generation, status callbacks, and what each KYC outcome means.
KYC flow
Every BlendFi end user must complete identity verification before they can move money. This is a regulatory requirement — without an approved KYC, attempts to create a quote or transaction return 403 missing_capability for the user. BlendFi handles KYC through Sumsub; you don't talk to Sumsub directly.
How it fits together
Your app BlendFi Sumsub
│ │ │
├─ POST /v1/users ─────►│ │
│◄──── user created ────┤ │
│ (kyc: not_started) │ │
│ │ │
├─ POST /v1/users/{id}/kyc ─► │
│ ├── create applicant ────►│
│ │◄──── applicant id ──────┤
│ ├── generate websdk link ►│
│ │◄──── signed url ────────┤
│◄──── kyc submission ──┤ │
│ (kyc: pending, │ │
│ websdk_url) │ │
│ │ │
├─ redirect end user to websdk_url ───────────────►
│ │ (user submits docs) │
│ │◄────── webhook ─────────┤
│ │ (review result) │
│◄── webhook to you ────┤ │
│ (kyc: approved) │ │You only ever call BlendFi. BlendFi calls Sumsub on your behalf and reflects results back to you.
Five outcomes
`not_started`
User exists. KYC submission has not been initiated yet. Cannot transact.
`pending`
WebSDK link delivered. Sumsub is reviewing the documents and selfie. Cannot transact.
`approved`
User passed KYC. Can transact. The default validity is 12 months; we'll renew on expiry.
`rejected`
Sumsub rejected the documents. User cannot transact. They must re-submit (a new submission resets to `pending`).
`expired`
An `approved` KYC has aged past its validity window. User must re-verify before transacting.
The full path
1. Create the user
curl -X POST $BLENDFI_BASE/v1/users \
-H "Authorization: Bearer $BLENDFI_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "content-type: application/json" \
-d '{
"external_id": "your-user-001",
"name": "Ada Lovelace",
"cpf": "52998224725"
}'The user is created with kyc_status: not_started. The CPF is stored encrypted; we never echo it back in plaintext.
2. Start the KYC submission
This call generates a signed Sumsub WebSDK URL. It's safe to expose to your end user (single-use, short TTL — typically 30 minutes).
curl -X POST $BLENDFI_BASE/v1/users/01J.../kyc \
-H "Authorization: Bearer $BLENDFI_KEY" \
-H "Idempotency-Key: $(uuidgen)"Response:
{
"user_id": "01J...",
"status": "pending",
"websdk_url": "https://websdk.sumsub.com/start/eyJ...",
"websdk_url_expires_at": "2026-04-29T13:30:00Z"
}The user's kyc_status flips to pending immediately, before any docs are submitted. This is the "we have an in-progress submission" state.
WebSDK link lifetime
The websdk_url is valid until websdk_url_expires_at. If the end user doesn't open it in time, call POST /v1/users/{id}/kyc again to get a fresh one. The KYC submission state stays pending across link regenerations — you don't lose progress.
3. Redirect the end user to the WebSDK
Open websdk_url in a browser tab, an iframe, or a mobile webview. Sumsub guides the user through document capture (CPF + ID document + selfie), then closes the flow.
The user does not return to you with a status — Sumsub doesn't know your URLs. You learn the result via webhook (next step) or by polling GET /v1/users/{id}/kyc.
4. Receive the result via webhook
When Sumsub finishes the review, it pings BlendFi. BlendFi updates the user's kyc_status and forwards a webhook to your registered URL.
{
"type": "user.kyc.approved",
"user_id": "01J...",
"external_id": "your-user-001",
"kyc_status": "approved",
"approved_at": "2026-04-29T13:08:00Z",
"expires_at": "2027-04-29T13:08:00Z"
}Possible event types: user.kyc.approved, user.kyc.rejected, user.kyc.expired. There is no event for pending → it's the state on submission start.
If you don't have webhooks wired up yet, fall back to polling:
curl $BLENDFI_BASE/v1/users/01J.../kyc \
-H "Authorization: Bearer $BLENDFI_KEY"{
"user_id": "01J...",
"status": "approved",
"approved_at": "2026-04-29T13:08:00Z",
"expires_at": "2027-04-29T13:08:00Z"
}5. Transact
With kyc_status: approved, the user can be the subject of a quote, transaction, or any other money-movement endpoint. Until then, those endpoints respond with a clear error pointing back to the KYC step.
Driving KYC outcomes in sandbox
Sandbox uses a Sumsub mirror. To avoid waiting for real document review, use the test helper to force any outcome:
curl -X POST $BLENDFI_BASE/v1/test_helpers/users/01J.../kyc \
-H "Authorization: Bearer $BLENDFI_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "content-type: application/json" \
-d '{"status": "approved"}'Acceptable values: "approved", "rejected", "expired". The webhook fires exactly as it would in production, so your handler logic is exercised end-to-end.
Sandbox-only
/v1/test_helpers/* does not exist in production — calls return 404. Make sure your code paths that drive KYC outcomes are flagged as test-only and never run against api.blendfi.com.
Renewal and expiration
Approved KYCs are valid for 12 months by default. We watch the expiration and downshift the user to kyc_status: expired on the boundary day. You receive a user.kyc.expired webhook 7 days before the expiration so you can prompt the user to re-verify ahead of time.
A re-verification is a fresh POST /v1/users/{id}/kyc call — the user goes through the same WebSDK flow again. There's no separate "renewal" endpoint.
Rejections
A rejected outcome includes a rejection_reason:
{
"user_id": "01J...",
"status": "rejected",
"rejected_at": "2026-04-29T13:08:00Z",
"rejection_reason": "document_unreadable"
}Common reasons:
rejection_reason | What happened | What to tell the user |
|---|---|---|
document_unreadable | Photos too blurry, glare, or cut off | Re-take in better lighting, full document in frame |
document_expired | The submitted ID is past its validity date | Submit a current document |
liveness_failed | The selfie didn't match the document or was a photo of a photo | Re-attempt with the user's actual face on camera |
pep_match | The user matched a politically-exposed-persons list | We escalate; you'll hear from us |
sanctions_match | The user matched a sanctions list | The user cannot be onboarded |
data_mismatch | Document data doesn't match the CPF / name in our system | Verify your external_id, name, cpf inputs |
A rejected user can re-submit (POST /v1/users/{id}/kyc again). The new submission supersedes the rejection; status returns to pending.
pep_match and sanctions_match are terminal in our flow — re-submission won't help. We handle these case-by-case with you.
What to read next
Getting started
Create your first user. KYC starts immediately after that.
Transaction lifecycle
An approved KYC is the prerequisite for every step of the lifecycle. See how a `kyc_blocked` failure propagates.
Errors and retries
What `403 missing_capability` looks like when a user with `kyc_status: pending` tries to transact.
FAQ
Can the same end user re-submit if rejected?
Yes — for most rejection reasons. Call POST /v1/users/{id}/kyc again to get a new WebSDK link; the user goes through the flow afresh. The two terminal cases are pep_match and sanctions_match — those require a human review on our side.
What happens if the user closes the WebSDK halfway through?
Their kyc_status stays pending. The WebSDK keeps the partial state for ~24 hours; if they reopen the same link within that window, they pick up where they left off. After expiration, generate a new link.
Do I need to verify the webhook signature?
Yes. Production webhooks from BlendFi carry an X-BlendFi-Signature header (HMAC-SHA-256 over the body, with a per-organization secret). Reject any webhook whose signature doesn't verify. We provide reference verification snippets in the webhooks reference.
Why does not_started exist as a state if I always start KYC right after creating the user?
Some integrations create users early (e.g. waitlist signup) and only trigger KYC when the user takes a money-relevant action. The two-step shape supports that pattern. If you always start KYC immediately, just treat not_started as a transient state your UI never displays.
Can I see the rejected document images? No. Sumsub holds the documents; BlendFi only sees the verdict and the rejection reason. This is a privacy boundary by design.
The user passed KYC in another product I run — can I skip BlendFi's KYC for them? No. We re-verify per BlendFi organization, regardless of whether the same end user has been verified elsewhere. This is a regulatory constraint.
