BlendFi

KYC flow

Verify a user's identity with Sumsub before they can transact — link generation, status callbacks, and what each KYC outcome means.

Compliance

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_reasonWhat happenedWhat to tell the user
document_unreadablePhotos too blurry, glare, or cut offRe-take in better lighting, full document in frame
document_expiredThe submitted ID is past its validity dateSubmit a current document
liveness_failedThe selfie didn't match the document or was a photo of a photoRe-attempt with the user's actual face on camera
pep_matchThe user matched a politically-exposed-persons listWe escalate; you'll hear from us
sanctions_matchThe user matched a sanctions listThe user cannot be onboarded
data_mismatchDocument data doesn't match the CPF / name in our systemVerify 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.

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.

On this page