POST /v1/users kicks off identity verification (KYC for individuals, KYB for businesses). The field set you must send depends on two values in the payload: type (individual or business) and address_country (ISO alpha-3, e.g. USA, MEX). Together they put every user into one of four categories.
Two things matter and they are not the same. A bare payload (just the required-to-create fields) returns
201 — but the user will not be product-eligible: verification does not trigger and the user stays CREATED until the field set is complete. A complete user triggers verification automatically — but approval is a manual step, not instant; per Kira it can take up to 24 hours, and the sandbox can route the user to manual review (status: REVIEW, verification_status: in_review) at any point in that window, which a Kira operator resolves. Don’t build against a “verifies in seconds” assumption — see Known limitations. The authoritative, per-user list of what is still outstanding is the missing_fields map on GET /v1/users/{id} (product_code → [field tokens]) and on the user.created webhook. Treat that map as the source of truth; the tables below tell you what to send up front so you never hit it. See Known limitations and For AI agents.Checking KYB status via the API. A client can check where a business stands without contacting Kira:
GET /v1/users/{id} returns identifying_information[] listing the documents actually submitted — each entry’s documents[] carries the real file_name and uploaded_at timestamp Kira received — plus the missing_fields map (what’s still outstanding per product) and status / verification_status (the review state). This does not mean everything you submitted round-trips: scalar questionnaire fields (account_purpose, source_of_funds, expected_monthly_volume, expected_transaction_count, employment_status) are accepted on write but not returned by GET — persist those on your own side. See Known limitations.The four user categories
type and address_country select the required field set:
International categories drop the US-only fields (
ssn, ein) and add a few of their own (immigration_status, international_entity_type, additional_info). The per-category fields are below.
Minimum to create
POST /v1/users only enforces a handful of fields. Everything else is enforced later, by verification and by per-product eligibility.
Required fields by category
To get a user verified and product-eligible, send the full set for its category. Fill scalar gaps later withPUT /v1/users/{id} and re-submit uploads for document gaps; the product flips eligible: true once missing_fields is empty.
Shared identity fields
Sent by every user, individual or business:USA individual (type: individual, address_country: USA)
USA individual (type: individual, address_country: USA)
IdentityQuestionnaire
Some banking partners additionally require
country_of_birth and an identity-document expiry date (identifying_information[].expiration). These two behave differently from the required fields described above, so send them whenever you have them:- They are reported in
missing_fields, under the entry for the product that needs them — so you can see the gap before you create anything. They do not appear under products that don’t require them, and they do not block verification: a user missing only these still reachesVERIFIED. - They do not produce a
422. If you create the account anyway,POST /v1/virtual-accountsreturns201, the account stays inactivating, and its status reason names the unmet condition — read it fromGET /v1/virtual-accounts/{id}. Backfill withPUT /v1/users/{id}and create the virtual account again. - A user already onboarded at the partner is unaffected — an existing banking relationship is reused rather than re-checked.
International individual (type: individual, address_country ≠ USA)
International individual (type: individual, address_country ≠ USA)
All USA individual fields apply, with these differences:
USA business (type: business, address_country: USA)
USA business (type: business, address_country: USA)
Entity identity
Company documents — all four groups must be covered. Verification requires a company document for each of the four groups below; a business with only the formation document stalls with the other three groups in
missing_fields. One document can cover several groups at once — articles of incorporation / bylaws (file_bylaws) count for legal presence, company details, and control structure. The tokens below are the representative doc types missing_fields reports for each group; the listed alternatives satisfy the same group.Additional compliance documents. Some banking partners require supplementary documentation beyond the four groups above. Submit these as top-level
documents[] entries ({ "type": "…", "file": … }) — they are not covered by missing_fields, so provide them proactively when known to be required:Questionnaire
Associated persons (UBOs / directors) —
associated_persons is an array; each object carries the person’s own identity:International business (type: business, address_country ≠ USA)
International business (type: business, address_country ≠ USA)
All USA business fields apply, with these differences:
The USA virtual-account products require different fields. They do not share an identical KYC set. Rather than hard-coding any one list, read the per-product
missing_fields map on GET /v1/users/{id} and fill what it reports for the product you are creating. See For AI agents and the missing_fields example in Webhooks.A product can require more than verification does
missing_fields is keyed by product, and a product may ask for fields that are not required to verify the user. This is deliberate: some sponsor banks require data the identity check itself does not.
Two consequences worth designing for:
- A user can reach
VERIFIEDwhile a product still reports missing fields. Verification completing is not a promise that every product is satisfied. Checkmissing_fieldsfor the product you intend to open, not just the user’s status. - A gap on one product never blocks another. Fields required only by product A are invisible to product B’s
missing_fields, to itseligibleflag, and to whether verification can start at all.
GET /v1/users/{id} and check missing_fields[<your product>] → fill what it reports → create the virtual account. Creating the account while that product still lists gaps is the common cause of an account that is accepted and then fails during activation at the bank.
Some products require documents that Kira does not generate for you — notably
file_fatca, the signed W-9 or W-8BEN tax self-certification. Its federal tax classification cannot be derived from the data you send us, so it must come from the account holder. If a product needs it, missing_fields will say so.Conditional field rules
missing_fields tokens encode these conditions; send the field only when the condition holds.
Biometric / liveness verification report
Some banking partners require a biometric verification report — a verification-result document proving a face match against the government ID — on a user’s record. Kira produces this itself from your identity provider; you do not upload a finished report. Two ways it is produced:-
Send a selfie (face match). Include a
selfieinidentifying_information[].documents[]alongside the ID (the same document shape asfront/back). Kira submits the selfie and the ID to the identity provider, which face-matches the selfie against the ID portrait; on approval the verification report is attached to the user automatically. No interactive session is required. -
Have the user complete a liveness session. For a higher-assurance check that includes liveness
(not possible from an uploaded image), call
POST /v1/users/{user_id}/liveness-linkto get hosted link(s), then relay each to the right end user. The request body is optional — omit it, or supply aredirect(see below) to send the user back to your own page afterward. Calling it again for a subject whose link is still active returns that same link rather than issuing a new one. The response is{ "links": [...] }with one entry for an individual user and one per beneficial owner for a business; each entry carries asubject("user"or"ubo"), aliveness_link, anexpires_at(the link is valid for 7 days), and — forubosubjects — aperson_reference_idandnameso you can route the link to the right person:When a user finishes, auser.liveness_completedwebhook fires withdata.resultof"approved"or"rejected"(and, for a beneficial owner, the matchingdata.person_reference_id), and on approval Kira attaches the generated report to the user automatically. A liveness result never changes the user’s main KYCstatus. You can request the links during onboarding — a business’s UBO liveness links are issuable once its KYB has been submitted (the business need not be fully verified first, since liveness feeds the verification). The endpoint returns422when there’s no verification to attach the check to (an individual with no verification started yet, or a business whose KYB hasn’t been submitted or has no beneficial owners on record), or when the identity provider doesn’t support liveness for the subject. -
Optional: send the user back to your page. Include a
redirectobject to land the user on your own page after the session instead of the default completion screen:success_urlis required whenever you send aredirect(reject_urlalone is rejected), and both URLs must be HTTPS. Send a field only if you mean it — an empty value is a422, not “omitted”. This is opt-in per account: each URL’s host must be on your redirect allowlist (subdomains of an allowlisted domain are accepted). A non-HTTPS or non-allowlisted URL — or any redirect at all before your allowlist is set up — is rejected with422and no link is minted. The redirect is bound to the link when it’s issued, so a repeat call for a still-active subject keeps the original redirect; supply a differentredirectto mint a fresh link with the new one.The allowlist is configured by the Kira team — there is no API or dashboard for it. To enable redirects, contact Kira support with the exact domains you want allowlisted (for exampleapp.example.com), and for which environments (sandbox, production, or both). Until the team has registered them, every request carrying aredirectreturns422. Send the same request again once you’re told the allowlist is live — no code change is needed. Reach out the same way to add or remove a domain later.
Country restrictions
All codes are ISO 3166-1 alpha-3. Rules apply toaddress_country unless noted.
Blocked
Users withaddress_country in this list are ineligible for any virtual-account product:
AFG BLR MMR CAF CIV CUB COD IRN IRQ LBR LBY PRK RUS SAU SOM SDN SYR UKR YEM VEN ZWE
Enhanced due diligence (EDD)
When an EDD trigger applies, add a proof-of-address document (identifying_information: file_proof_of_address).
Individuals (53 countries) — triggered when address_country is in:
DZA AGO ARM AZE BHS BHR BGD BMU BOL VGB BRN BFA KHM CMR CXR CCK CRI CYP EGY SLV GIB GRC GTM GGY HTI HND IDN IMN JEY JOR KAZ KEN KWT LBN LIE MYS MCO MNG MOZ NIC NGA OMN PNG PRY PHL ZAF LKA THA TTO TUN UGA UZB VNM
Businesses (51 countries) — the same list minus GTM and CCK. For businesses, proof of address is required if any one of these is true:
Example payloads
CompletePOST /v1/users bodies for each category. Adapt the values. Photos and files go in identifying_information[].documents[] as { "type": "front", "file": … } — note the key is file (not content), and government-ID photos nest under their parent ID entry rather than sitting loose in the array. Each file is a base64 data URI (data:image/jpeg;base64,…, JPEG/PNG/PDF) or — on version 2026-04-14+ — an https:// URL. See Create a user — API reference for the full document schema. The whole request body is capped at 10 MB, and inline base64 counts against it (≈ +33% overhead) — so for large or multiple files, pass file as an https:// URL (fetched server-side, up to 30 MB per file, and off the body budget) rather than base64; see Request size & large document uploads.
Troubleshooting a KYB that stalls
A business submission has two ways to stop that look nothing alike. Diagnose them in this order.1. It never triggered — verification_triggered: false
The payload is incomplete for the category. The missing_fields map lists exactly what is outstanding per product; supply it with PUT /v1/users/{id} and verification triggers on its own. Iterate until missing_fields disappears from the response.
eligible: false on every product is expected while this is happening. Product eligibility requires the user to reach VERIFIED, so it stays false throughout submission and review. The signal that your payload is complete is missing_fields disappearing together with verification_triggered: true — not eligible.2. It triggered but sits in REVIEW — a document was declined
Verification reached the provider and one of your documents was rejected as retryable. This is not a terminal REJECTED, so no user.verification.failed webhook fires and GET /v1/users/{id} shows the document as uploaded with no indication that it was declined.
The most common cause is a name mismatch:
Other frequent causes: a document that does not cover the group it was sent as (see the four groups above), an unreadable scan, or an expired certificate.
3. Beneficiaries are stuck
Each associated person is verified as its own applicant. Two failure modes are easy to hit and are covered in Known limitations: a partialPUT silently drops nationality and document_number, and omitting document_type submits the person as an ID card, which then demands a back image your passport-only upload never provides.
Bank intent (optional)
capabilities.requested_banks is an optional array recording which bank(s) this subclient plans to use. It’s intent, not authorization — sending a bank slug here does not grant your account access to it; authorization for a bank is a separate, account-level configuration. Omit capabilities entirely to use the standard verification path.
PUT /v1/users/{id} also accepts capabilities.requested_banks and verification_mode. On a PUT, requested_banks merges with whatever is already stored — sending ["austin_capital_trust"] for a subclient that already declared ["portage"] results in both being recorded; a PUT can only add a bank, never remove one by omission. Any request body key this endpoint doesn’t define is rejected with 400 instead of being silently dropped.
For most banks, declaring one this way just records the intent — no other effect. A small number of banks additionally require moving the subclient’s existing applicant to a stricter verification level; for those, a PUT that declares the bank triggers that move on the applicant already on file (no new applicant is created). When verification_mode is verification_link, a successful move returns a fresh verification_link; a subclient mid-review, or otherwise not yet eligible to move, returns 200 with a descriptive verification_link_error instead — never a 4xx/5xx for a legitimately deferred state. Re-sending the same PUT is also the retry channel: a deferred move is re-attempted on every request that declares the bank, and once the subclient is already at the target level a repeat request simply returns a fresh verification_link (useful when a previous link has expired). A returned link is not lost with the response that carried it — subsequent GET /v1/users/{id} calls keep returning it as verification_link while verification_mode is verification_link.
verification_link_error is freeform, human-readable text, not a stable enum — don’t branch your integration on its wording. verification_link_error_severity is the machine-readable field for that: it accompanies verification_link_error on the PUT response with either deferred (nothing is wrong — the move is waiting on something expected, like the applicant’s current review finishing, or was correctly skipped by a business rule) or failed (the move hit a genuine problem).
Related
- Create a user — API reference — the endpoint, full schema, and live “Try it”.
- For AI agents —
eligible_products/missing_fields, conditional tokens, getting a user toVERIFIEDin sandbox. - Webhooks — the
user.createdpayload with the per-productmissing_fieldsmap. - Known limitations — sandbox auto-verification behavior and
statusvsverification_status.