Verification & Endorsements
Each endorsement is one currency's review of the customer: an explicit first submit, documents read at submit, and every ask a bare documented code.
The model
The endorsement is the verification. You request an endorsement by
name (endo_usd for USD services), and the review of the customer for that
currency is the whole verification. There is no separate identity check in
front of it. Everything, from progress to asks to approval to adverse
decisions, surfaces on that endorsement's status and requirements.
You share everything over the API. Profile fields, identity documents, and supporting documents ride the customer object on create and PATCH. We hold them, and nothing is reviewed until you submit. See Customers.
Each submission opens one verification round (ver_...) per
endorsement it sends to review. Rounds are history. Build your UI on the
endorsement.
Documents we ask for
We ask up front for every document the review needs. Once you request an
endorsement, its missing bucket lists each document still owed, so you
can collect them all before you submit. The submit returns
422 verification_incomplete while any is owed. Each code below is also
the purpose you send the document under (see
Customers),
except identity, which rides identifying_information, and the
controller_ codes, which ride each controller's own
identifying_information and documents.
Individuals
| Document | Owed |
|---|---|
identity | Always |
selfie | Always |
proof_of_address | When the customer lives outside the US, or where they live is not known yet. We go by address.country, else residence_country. A US resident may still send one, and we read it |
Businesses, by legal form (business_kind):
business_kind | Documents owed |
|---|---|
corporation | registration_document, articles_of_association, ownership_information, proof_of_signatory_authority, proof_of_address |
llc | registration_document, articles_of_association, operating_agreement, ownership_information, proof_of_signatory_authority, proof_of_address |
partnership | registration_document, operating_agreement, ownership_information, proof_of_signatory_authority, proof_of_address |
sole_prop | registration_document, proof_of_tax_identification, proof_of_address |
trust | registration_document, ownership_information, proof_of_signatory_authority, proof_of_address |
cooperative | registration_document, articles_of_association, proof_of_signatory_authority, proof_of_address |
| Not known yet | registration_document. We read the legal form from it, and the rest of the row follows |
Every business also owes, for each controller, an identity document
(controller_identity) and a proof of address
(controller_proof_of_address).
To see a business's whole list from the start, send business_kind when
you create it. Without it, missing names registration_document first,
and the rest of the list appears once the submit reads the legal form from
that document.
What each business document is:
| Code | Send |
|---|---|
registration_document | The certificate of incorporation (corporation, cooperative), the certificate of formation or registration (LLC, partnership), the business license (sole proprietorship), or the trust agreement (trust) |
articles_of_association | The articles of incorporation or organization, the bylaws, or the memorandum of association |
operating_agreement | The LLC operating agreement, or the partnership or limited-partnership agreement |
ownership_information | A document showing who owns the business, such as a shareholder register or an ownership chart. For a trust, its ownership and formation documents |
proof_of_signatory_authority | The list of the people authorized to represent or sign for the business |
proof_of_address | A recent proof of the business's operating address |
proof_of_tax_identification | A document showing the business's taxpayer identification number, such as a tax filing |
These are the documents every customer of a kind or legal form needs. The
review can ask one customer for more, for example under enhanced due
diligence. Those asks arrive in the requirement buckets like any other, and
each has its own purpose:
| Code | Send |
|---|---|
proof_of_source_of_funds | Evidence of where the funds come from, such as a bank statement, payslip, or sale agreement. Either kind |
evidence_of_good_standing | A certificate of good standing, or the registry's equivalent |
directors_registry | An official record of the directors and officers, such as a certificate of incumbency |
regulatory_license_registration | The license or registration for a regulated activity |
aml_policy | The business's anti-money-laundering policy |
annual_report | The most recent annual report or statement filed with the registry |
proof_of_tax_exemption | A document showing the business is exempt from tax |
trade_name_registration | The registration of the business's trading (doing-business-as) name |
Submitting for review
The first submission is explicit. Its body takes an optional
intake_method, which says how the customer's details and documents reach
us:
intake_method | Meaning |
|---|---|
api | The default. We submit everything you sent through POST and PATCH /api/customers |
hosted | One standing link per customer, valid for 90 days, that collects everything still owed, terms included. Not available yet: it returns 400 hosted_intake_unavailable |
An empty body is the same as {"intake_method": "api"}:
curl -s $BASE/api/customers/cus_.../verifications \
-H "Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" -d '{}'
redirect_url belongs to hosted collection only. Sent without
"intake_method": "hosted", it returns 422 invalid_parameter. Any other
field, requested_info included, also returns 422 invalid_parameter.
The response is a list with one round for each endorsement the call sent
to review:
{
"data": [
{
"id": "ver_033y1FhTOmoW9W2jAuSvQx",
"object": "verification",
"customer_id": "cus_033y1FhTOmoW9W2jAuSvQx",
"endorsement": "endo_usd",
"purpose": "full",
"requested_info": null,
"intake_method": "api",
"status": "under_review",
"url": null,
"expires_at": null,
"rejection_reasons": null,
"created_at": "2026-08-26T09:15:32+00:00"
}
],
"has_more": false,
"next_cursor": null
}
A submit sends every requested endorsement that is incomplete or in
issues. Endorsements already under review or decided are left alone.
| Refusal | Retry class | Meaning |
|---|---|---|
400 customer_tos_required | actionable | The customer has no terms acceptance on record |
400 endorsement_required | actionable | No endorsement is requested. Request one on create or PATCH ("endorsements": ["endo_usd"]), then submit |
400 hosted_intake_unavailable | actionable | Hosted collection is not available yet. Returned ahead of every other refusal in this table. Omit intake_method, or send api, and share the customer's details and documents through the API |
409 verification_not_needed | terminal | Every requested endorsement is already under review or decided |
422 verification_incomplete | actionable | Something is still owed, or a document was not accepted. The message names each document we could not accept and why, then everything else owed, and param names the first |
503 document_reading_unavailable | retryable | We could not read the documents right now. Nothing was submitted. Retry |
Documents are read at submit
Send the images, and we read the rest. At submit we read the newest
document of each kind below, for the customer and for each controller of a
business, then fill the fields you left empty. Selfies are not read, and
neither are the business documents that carry no profile field
(articles_of_association, operating_agreement,
ownership_information, and proof_of_signatory_authority).
| Document | Fills |
|---|---|
Identity document (identifying_information) | first_name, middle_name, last_name, date_of_birth, nationality, the document's kind, number, and issuing country, and address when no proof of address gives one |
proof_of_address | address. For a business, its own proof of address fills physical_address, the operating address |
registration_document | legal_name, business_kind, registration_number, incorporation_date, and the registered address |
proof_of_tax_identification | tax_id and tax_residence_country |
A controller's identity document, proof of address, and proof of tax
identification fill that person's fields the same way. A value you sent
is never overwritten. It changes only through your own PATCH. Only what
the documents could not supply is asked, in missing and in the submit's
422 verification_incomplete.
Each document is read once, and a newer document's reading replaces a value an older one filled. An address always comes from one document, so lines from two documents never mix. We read identity and registration documents first, so a proof of address or a tax document is checked against the name they carry, or the name you sent.
Contact data appears on no document, so requesting endo_usd needs it
already on the customer or in the same request: email and phone for an
individual, or email for a business. Missing ones return 422 endorsement_profile_required.
Tax data is not needed to request endo_usd, but the review needs it. In
live, until you send tax_id, and tax_residence_country for an
individual, or a proof_of_tax_identification that carries them, the
endorsement lists them in missing and the submit's
422 verification_incomplete names them. Send them on create so the first
submit is not held up.
A document we cannot accept is asked for again
Reading also checks the document. When the newest document for a
requirement has one of these problems, that requirement moves to the
issues bucket and the submit returns 422 verification_incomplete
before anything is sent to review:
| Problem | Fix |
|---|---|
| It could not be read | Send a clear, complete image or file |
| It has expired | Send a valid document |
| It is older than we accept, for a proof of address | Send a recent one |
| It carries a different name than the one we hold | Send a document in the customer's or controller's name |
| It failed its authenticity checks, or is not the document required | Send a genuine document of the kind asked for |
The message names the document and the problem:
{
"error": {
"kind": "invalid_request_error",
"code": "verification_incomplete",
"message": "The proof of address was not accepted: it is older than we accept. Supply them via POST /api/customers or PATCH /api/customers/{id}, then submit.",
"param": "proof_of_address",
"request_id": "req_033xhsr1AZZxC8AOkK7y3W"
}
}
Supply a new document with a PATCH, then submit. After the first submit,
the same check runs on every PATCH that resubmits: a document we cannot
accept is listed in issues, and nothing is sent to review until it is
replaced.
After the first submit
You do not submit again to send a change. A PATCH that changes
something resubmits by itself: a new document, a corrected value, or a
newly requested endorsement. Every endorsement that is incomplete or in
issues and owes nothing goes back to review in a new resubmission round,
and the PATCH response shows the outcome.
- A PATCH that changes nothing does nothing. The same file again or the same value again opens no round. Files are deduplicated by content hash, so send a new image when a document was refused.
- A document added after approval is held, not sent. It shows in
uploaded, and the approval stands. - Until the first submit succeeds, submit explicitly. A PATCH before then only updates what we hold.
Decisions arrive on the endorsement
| Event | Meaning |
|---|---|
endorsement.approved | The review passed. The currency's features open |
endorsement.request_for_information | Retryable. The asks are in the buckets. Also fires when the endorsement returns to incomplete because something only you can supply is owed |
endorsement.rejected | Final adverse decision |
Each event carries the endorsement object GET /api/customers/{customer_id}
embeds, plus object: "endorsement" and the customer_id. There are no verification.* webhooks.
The review's word stands in both directions. It can ask for more on an
endorsement it already approved. The endorsement then moves from approved
to issues and endorsement.request_for_information fires. Anything that
needs the endorsement approved, such as creating a virtual account in its
currency, is refused until it is approved again.
When we cannot reach the review, the endorsement shows issues with
the single code unavailable. No webhook fires, the round is marked
expired because it never reached the review, and we retry automatically
with a new round. You need not act.
The one channel: requirement buckets
Every ask is a bare snake_case code in exactly one of five buckets.
| Bucket | Meaning |
|---|---|
complete | Done. Nothing owed |
uploaded | Held by us until the next submission to review. Do not re-send |
pending | Submitted and under review |
missing | Never supplied. You owe it |
issues | Received but not usable, or a verdict blocking the endorsement |
Before the first submit, missing holds compliance_review: the submit
itself is owed. A requested endorsement with everything held reads:
{
"name": "endo_usd",
"status": "incomplete",
"requirements": {
"complete": ["terms_of_service"],
"uploaded": ["identity", "proof_of_address", "selfie"],
"pending": [],
"missing": ["compliance_review"],
"issues": []
}
}
missing lists everything the review needs that you have not supplied,
with three rules for documents:
- A missing document is listed itself, such as
identity, never the fields it would carry. - A field a held document will supply is not listed while that
document waits to be read. Whatever the reading cannot yield is listed
after the submit, and the submit's
422 verification_incompletenames it. A document we cannot accept moves toissues. - Optional documents are not owed.
proof_of_tax_identificationappears in a bucket only once you send one, except for a sole proprietorship, which owes it. Until then, the tax fields are asked as fields. Which documents are owed is in Documents we ask for.
For the endorsement's own statuses and transitions, see Statuses & Lifecycles.
Requirement codes
Buckets carry only the codes below: never prose, a field path, or a message from anyone else. A field code is satisfied by the customer field of the same name unless the table says otherwise.
Agreements and the review
| Code | Meaning |
|---|---|
terms_of_service | The customer has accepted our terms of service |
compliance_review | The review of the customer for this currency: owed (missing) until submitted |
verification | The identity verification of the customer. Appears as one entry, in place of the document codes, on customers onboarded before documents were tracked one by one |
Documents
| Code | Meaning |
|---|---|
identity | A government-issued identity document, sent in identifying_information |
selfie | A selfie that matches the identity document, sent as {"purpose": "selfie"} |
proof_of_address | A recent proof of the customer's address, or a business's operating address, sent as {"purpose": "proof_of_address"} |
registration_document | The business's registration document, sent as {"purpose": "registration_document"} |
articles_of_association | The business's articles of incorporation or organization, bylaws, or memorandum of association, sent as {"purpose": "articles_of_association"} |
operating_agreement | The LLC operating agreement, or the partnership or limited-partnership agreement, sent as {"purpose": "operating_agreement"} |
ownership_information | A document showing who owns the business, sent as {"purpose": "ownership_information"} |
proof_of_signatory_authority | The list of the people authorized to represent or sign for the business, sent as {"purpose": "proof_of_signatory_authority"} |
controller_identity | A government-issued identity document for every controller, in that controller's identifying_information |
controller_proof_of_address | A recent proof of address for every controller, in that controller's documents |
proof_of_tax_identification | A document showing the customer's taxpayer identification number, sent as {"purpose": "proof_of_tax_identification"}. Owed by a sole proprietorship. Otherwise optional: it appears once sent |
controller_proof_of_tax_identification | A document showing every controller's taxpayer identification number, in that controller's documents. Optional: it appears once sent |
proof_of_source_of_funds | Evidence of where the customer's funds come from, such as a bank statement, payslip, or sale agreement, sent as {"purpose": "proof_of_source_of_funds"}. Asked per risk or on request |
evidence_of_good_standing | A certificate of good standing, or the registry's equivalent, showing the business is active, sent as {"purpose": "evidence_of_good_standing"} |
directors_registry | An official record of the business's directors and officers, such as a certificate of incumbency, sent as {"purpose": "directors_registry"} |
regulatory_license_registration | The license or registration under which the business carries on a regulated activity, sent as {"purpose": "regulatory_license_registration"} |
aml_policy | The business's anti-money-laundering policy, sent as {"purpose": "aml_policy"} |
annual_report | The business's most recent annual report or statement filed with its registry, sent as {"purpose": "annual_report"} |
proof_of_tax_exemption | A document showing the business is exempt from tax, such as a tax-exemption determination letter, sent as {"purpose": "proof_of_tax_exemption"} |
trade_name_registration | The registration of the business's trading (doing-business-as) name, sent as {"purpose": "trade_name_registration"} |
Customer profile. In missing the field is absent. In issues it was
supplied and refused. address is the residential address, or a
business's registered one.
| Code | Meaning |
|---|---|
first_name | The customer's first name |
last_name | The customer's last name |
date_of_birth | The customer's date of birth |
nationality | The customer's nationality |
email | The customer's email address |
phone | The customer's phone number, in E.164 |
address_street_line_1 | First street line of the customer's address |
address_street_line_2 | Second street line of the customer's address |
address_city | City of the customer's address |
address_state | State or province of the customer's address |
address_postal_code | Postal code of the customer's address |
address_country | Country of the customer's address |
id_document_number | The identity document's number: id_document.number, or number on the identifying_information entry |
id_document_kind | The identity document's kind (passport, id_card, drivers_license, or residence_permit), when it could not be read from the document: id_document.kind, or kind on the identifying_information entry |
issuing_country | The country that issued the identity document, on the identifying_information entry |
tax_id | The customer's taxpayer identification number |
tax_residence_country | The customer's country of tax residence |
Business profile
| Code | Meaning |
|---|---|
legal_name | The business's registered legal name |
trade_name | The business's trading (doing-business-as) name |
website | The business's website, as an absolute URL |
business_kind | The business's legal form |
business_industry | The business's industry, as NAICS codes |
incorporation_date | The date the business was incorporated |
physical_address | The business's principal operating address |
account_purpose_other | What the account is for, when account_purpose is other |
source_of_wealth | Where the business's wealth comes from |
estimated_annual_revenue_usd | The business's estimated annual revenue, in USD |
expected_monthly_payments_usd | The business's expected monthly payments, in USD |
ownership_structure | How the business is owned |
controllers | The business's owners and control persons, at least one with control (has_control) |
controller_name | Every controller's first and last name |
controller_email | Every controller's email address |
controller_date_of_birth | Every controller's date of birth |
controller_nationality | Every controller's nationality |
controller_address | Every controller's complete residential address |
controller_tax_id | Every controller's taxpayer identification number |
controller_tax_residence_country | Every controller's country of tax residence |
controller_id_document_number | The number on every controller's identity document |
controller_id_document_kind | The kind of a controller's identity document, when it could not be read from the document: kind on that controller's identifying_information entry |
controller_issuing_country | The country that issued a controller's identity document, when it could not be read: issuing_country on that controller's identifying_information entry |
source_of_wealth_description | Where the business's wealth comes from, described in words |
Enhanced due diligence. The review asks for it per customer, based on its own risk assessment.
| Code | Meaning |
|---|---|
enhanced_due_diligence | The review requires enhanced due diligence for this customer: employment_status, occupation, source_of_funds, account_purpose, and expected_monthly_volume_usd. The codes beside it name the fields still absent |
occupation | The customer's occupation |
employment_status | The customer's employment status |
source_of_funds | Where the customer's funds come from |
account_purpose | What the customer uses the account for |
expected_monthly_volume_usd | The customer's expected monthly volume, in USD |
source_of_funds_description | Where the customer's funds come from, described in words |
Asks the review could not pin to one field
| Code | Meaning |
|---|---|
profile_incomplete | The customer profile is missing information the review needs |
profile_invalid | A value on the customer profile was not accepted |
passport_required | This customer's residence onboards with a passport, and the identity document supplied is not accepted for it. Send a passport in identifying_information |
Verdicts. Nothing you send fixes these.
| Code | Meaning |
|---|---|
region_unsupported | We do not serve this customer's region for this currency |
declined | The customer cannot be approved for this currency |
contact_support | The review is blocked for a reason we cannot name. Contact support |
unavailable | The review could not be completed. It is retried automatically; you need not act |
Verification rounds
Each submission opens one round per endorsement it sends.
GET /api/customers/{customer_id}/verifications lists them, newest first. Their
statuses are in
Statuses & Lifecycles.
{
"id": "ver_033y1FhTOmoW9W2jAuSvQx",
"object": "verification",
"customer_id": "cus_033y1FhTOmoW9W2jAuSvQx",
"endorsement": "endo_usd",
"purpose": "full",
"requested_info": null,
"intake_method": "api",
"status": "request_for_information",
"url": null,
"expires_at": null,
"rejection_reasons": [{"requirement": "proof_of_address", "retryable": true}],
"created_at": "2026-08-26T10:02:11+00:00"
}
| Field | Meaning |
|---|---|
endorsement | The endorsement this round was submitted to, such as endo_usd. null only on a partner's own KYB |
purpose | full for the first submission to that endorsement's review, resubmission for every later one. Older rounds can carry rfi |
requested_info | The asks this round answers, as requirement codes. null on a first submission |
intake_method | How the round's material reached us: api or hosted. Every round you can create today is api |
status | submitted, under_review, request_for_information, approved, rejected, or expired. expired marks a round that will never be decided: an old one, or one that never reached the review. awaiting_input is reserved for hosted collection |
url | Reserved for hosted collection: the customer's standing link. Always null today |
expires_at | Reserved for hosted collection: when that link stops accepting input. Always null today |
rejection_reasons | On an adverse decision, what to fix: requirement is a code from this page, or null for a verdict, and retryable says whether supplying something can fix it. Otherwise null |
What to ask your user for:
requirement | Typical fix |
|---|---|
identity | A clear, uncropped photo of the valid physical document, both sides, no screenshots |
selfie | A new selfie taken live, matching the submitted document |
proof_of_address | A recent utility bill or bank statement showing the customer's address |
registration_document | A current official company registration extract |
null, with retryable: false | A verdict such as declined. Route the customer 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 carry no extracted identity data. The endorsement carries readiness, and your own records carry the dossier.
Fixing an ask
The fix is a PATCH, and the PATCH is the resubmission. There is no special endpoint and no second call.
curl -s -X PATCH $BASE/api/customers/cus_... \
-H "Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"documents": [{"purpose": "proof_of_address", "file": "<base64>"}]}'
The endorsement in the response reads pending again, and a new round
opens with purpose: "resubmission" and requested_info naming the asks it
answers. If the endorsement still owes something, it stays in issues and
the buckets say what.
What to render, per state
Endorsement status | Buckets | Render |
|---|---|---|
incomplete | missing or issues non-empty | Your collection flow for the listed codes. When compliance_review is the only code left, submit |
pending | any | A verification-in-progress waiting state |
issues | only unavailable | The waiting state. We retry automatically |
issues | any other code | A fix-it screen for the listed codes. Your PATCH resubmits |
approved | anything | Done. Open the currency's features. Codes in uploaded are held documents, not asks |
rejected | any | A terminal state with your support path |
Launch scope. Document sharing over the API (intake_method: "api")
is the only intake today. Hosted collection, one standing link per
customer that collects everything still owed, is coming. Reusable verification through token sharing returns after launch.
In sandbox you make the decisions, including
"decision": "request_for_information" to rehearse the fix-it loop. See
Sandbox Testing.