Core ConceptsVerification & Endorsements

Verification & Endorsements

One permanent applicant per customer, endorsements as the single source of readiness, and every ask a bare documented code.

The model

Every customer has exactly one permanent applicant in our verification system, created with the customer and reused for every review. Additional documents, retries, and re-checks all land on the same applicant. Each review attempt is recorded as a verification round (ver_...), so you get clean history without ever duplicating an identity.

You share documents over the API: you collect them in your UI, we relay them, and the decision engine decides. The intake rides the customer object itself. See Customers.

The endorsement is the one public voice of KYC. You request an endorsement by name (endo_usd for USD services), and everything, from progress to asks to approval to adverse decisions, surfaces on that endorsement's status and requirements. There is no second channel to reconcile.

The four calls

Submission is explicit. Documents sent on create or PATCH are archived and staged; nothing enters review until you submit.

curl -s $BASE/api/customers/cus_.../verifications/submit \
  -H "Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" -d '{}'

If required inputs are still outstanding, the call returns 422 verification_incomplete and names them, rather than failing opaquely further down. Individuals owe an identity document, a selfie, and a proof of address. Businesses owe a registration document.

Submitting again is safe: it opens a new round on the same applicant, never a second identity.

The one channel: requirement buckets

Every ask is a bare snake_case code in exactly one of five buckets.

BucketMeaning
completeDone, nothing owed
uploadedWe hold it, but nothing is reviewing it yet, because the intake is incomplete. Do not re-send
pendingSubmitted and genuinely under review
missingNever supplied, you owe it
issuesReceived but not usable, or a verdict blocking the endorsement. Fix and resubmit

uploaded exists so a partial intake reads honestly. With a passport in hand and a proof of address still owed, calling the passport "pending" would claim a review that is not running.

The endorsement's own status reads incomplete, then pending, then approved, with issues (fixable) and rejected (final) as adverse states. approved never demotes.

Codes you will act on most

CodeSatisfied by
terms_of_serviceThe customer's ToS acceptance
identityA government ID in identifying_information
selfieA {"purpose": "selfie"} document
proof_of_addressA {"purpose": "proof_of_address"} document
registration_documentBusinesses: a {"purpose": "registration_document"} document
tax_id, tax_residence_countryeconomic_profile.tin and .tax_residence_country, required up front to request endo_usd
email, phoneThe corresponding customer fields
amlNothing you can send. Screening runs on our side: it can raise an issue but is never due from you
verification, compliance_reviewOur own reviews. Informational: they move on their own

Field-level codes (date_of_birth, address_state, id_document_number, nationality, and the rest of the address parts) appear only when a review asks for them. Each is documented, and each is fixed by a PATCH supplying the field.

Two codes cover an ask a review could not pin to one field: profile_incomplete (something is missing) and profile_invalid (a value was not accepted).

Four are verdicts nothing you send will fix: region_unsupported, customer_kind_unsupported, declined, and contact_support. A fifth, unavailable, means the review could not complete and will run again on the next update.

You will never see a sentence written by someone else, a field path, or an undocumented value.

Decisions arrive on the endorsement

EventMeaningEndorsement becomes
endorsement.approvedEverything passedapproved, and the currency's features open
endorsement.request_for_informationRetryable, something needs fixingissues, with the asks in the buckets
endorsement.rejectedFinal adverse decisionrejected

Each event carries the full merged endorsement object, the same shape GET /api/customers/{id} embeds. There are no verification.* webhooks.

Verification rounds

Each attempt is a round with its own lifecycle: submitted, under_review, then approved, request_for_information, or rejected, with expired for a round that aged out.

{
  "id": "ver_033y1FhTOmoW9W2jAuSvQx",
  "object": "verification",
  "customer_id": "cus_033y1FhTOmoW9W2jAuSvQx",
  "intake_method": "direct",
  "purpose": "resubmission",
  "requested_info": null,
  "url": null,
  "status": "request_for_information",
  "rejection_reasons": [{"requirement": "selfie", "retryable": true}],
  "created_at": "2026-08-26T10:02:11+00:00"
}

requirement maps into the vocabulary above, and retryable tells you whether resupplying fixes it. What to ask your user for:

requirementTypical fix
identityA clear, uncropped photo of the valid physical document, both sides, no screenshots
selfieA new selfie taken live, matching the submitted document
proof_of_addressA recent utility bill or bank statement showing the registered address
registration_documentA current official company registration extract
amlNot fixable by documents. Usually final: route to your support path

Whether an outcome is retryable is decided per decision, on each reason plus the round's status. Never assume it from the requirement alone.

Rounds are deliberately lean: no internal names, no review-level names, and no extracted identity data. The endorsement carries readiness, and your own records carry the dossier.

Fixing an ask

The fix is a PATCH, then a submit. There is no special endpoint.

curl -s -X PATCH $BASE/api/customers/cus_... \
  -H "Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"documents": [{"purpose": "selfie", "file": "<base64>"}]}'

curl -s $BASE/api/customers/cus_.../verifications/submit \
  -H "Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" -d '{}'

Guarantees worth relying on:

  • Approved requirements are never reset by later rounds. Only a decision naming them again can move them.
  • An approved endorsement is never demoted by a scoped re-check or a retryable ask. Money keeps flowing while the re-check runs.
  • Replays are free. Re-sending the same file is deduplicated by content hash, so send a genuinely new image when a document was rejected.

Scoped rounds

Pass requested_info to open a round for specific items, for example after your compliance team asks for a fresh proof of address:

curl -s $BASE/api/customers/cus_.../verifications/submit \
  -H "Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"requested_info": ["proof_of_address"]}'

A scoped round is exempt from the completeness gate, because requested_info declares what is being asked for rather than claiming to have it. GET /api/customers/{id}/verifications lists every round, newest first, each with its purpose (full, resubmission, or rfi).

What to render, per state

Endorsement statusBucketsRender
incompletemissing non-emptyYour document-collection flow for the listed codes
pendinganyA verification-in-progress waiting state
issuesissues or missing non-emptyA fix-it screen for the listed codes
approvedall clearDone. Open the currency's features
approvedasks presentKeep features open and supply the listed codes. A scoped re-check is running
rejectedanyA terminal state with your support path

Launch scope. Document sharing over the API is the supported intake today. Hosted verification links and reusable-KYC token sharing return later, and the data model already accommodates them.

In sandbox you make the decisions, including "decision": "request_for_information" to rehearse the fix-and-resubmit loop. See Sandbox Testing.