extracted object with what the document reader pulled off
the images (name, date of birth, address, document number), so the details form
becomes a confirm step you prefill — not a page of empty fields. Asking the
user to type their details before uploading the ID gets you worse data (typos
that then mismatch the document) and a longer flow.
Every call is made from your backend with your platform access token, and names
the user with the user_id you received when they connected. Each step below
links to its page in the API reference — full
parameters, responses, error codes, and a live playground.
Instructions for your agent
Paste this into your coding agent to implement identity verification. It assumes the user is already connected (you have theiruser_id — see
user authentication).
Instructions for your agent
The flow
1
Upload the ID
Send the front, then the back, as base64-encoded images. Two optional
fields ride along:
document_type (drivers_license, state_id, or
passport) and issuing_country (ISO 3166-1 code). Omitted, the document
is auto-detected and defaults to a US driver’s license — so for any non-US
document, send issuing_country (and document_type if you know it) on
both sides.The back response is the branch point — it tells you what comes next:
more information, the face scan, or a rejection. If an image can’t be
used, the upload fails with a 422 (document_rejected or
document_expired) so you can build a retake flow, and any upload
response may carry a warnings array with actionable feedback.Some warnings are addressed to you, not the user: when a warning says
the document reads as a different document_type (or a different
issuing_country) than it was uploaded as, resubmit the same images
with those fields set. Don’t ask the user to retake the photo — the image
is fine, only the classification needs correcting.Both upload responses also carry an extracted object — the fields the
document reader pulled off the images (name, date of birth, address,
document_number, document_type, issuing_country). That’s your prefill
data for the next step, and it’s why the upload comes first.→ Upload the front ·
Upload the back2
Confirm extra information (only if asked)
A
needs_information status lists exactly the required_fields to
collect — possible fields are first_name, last_name, date_of_birth,
national_id_number, phone_number (E.164), and the address_* fields.
Show a form for only those, prefilled from extracted where the keys
overlap, so the user confirms instead of typing. One caveat:
document_number is the number printed on the document — for a US document
it is not the SSN, so never prefill it into national_id_number when
issuing_country is US. Submit, and handle invalid_fields by surfacing
its field_errors.→ Submit information3
Scan the face
When a response includes an Two things to know about
iframe_url, show it to the user in an iframe
or webview. They complete a short face scan there, and the window reports
back when it’s done.The face scan uses the device camera, so the embedding iframe must grant
it:iframe_url:- It’s short-lived. Always render the most recent one — from the
latest API response, a fresh status poll, or the latest
identity.verification.updatedevent. Don’t store it for later. - Record consent at connect — before verification starts. If you
record the user’s authorization with
Record consent (part of the
connect flow), it also covers the card issuer’s terms, and
iframe_urlis the verification provider’s own scan, full screen, with no extra steps and no Agentcard branding — it reads as part of your flow. Without a recorded consent, the page has to pause on a one-checkbox terms confirmation before the scan.
4
Get the result
Listen for the webhook, or poll.
approved means verified; rejected
means the user did not pass.→ Get verification statusLearning the outcome
Verification finishes after the face scan. Agentcard sends anidentity.verification.updated event to your configured
webhook endpoints whenever the status changes:
required_fields (on needs_information), iframe_url (on
requires_verification), and reason — a short, end-user-safe explanation of
what the review asked for.
Or poll GET /api/v2/kyc with the
user_id.
Status values
Non-terminal statuses can also carry a
reason — an end-user-safe sentence
explaining what the review asked for (for example, “Enter your full name
exactly as it appears on your identity document.”). Show it to the user
verbatim; it’s already scrubbed of anything internal.
When a review sends the user back
Statuses are not one-way. A review that finds a problem it can describe returns the user to an earlier step instead of rejecting them — the status tells you which one:
Rejections that can’t be retried (a terminal review) come back as
rejected,
with a reason when one can be shared. One of them resolves itself:
duplicate documents, marked by reason_code. Drive
your UI from the current status on every webhook or poll; never assume a
step is done because it succeeded once.
Duplicate documents
One rejection is special: the person’s documents are already verified on another Agentcard account (usually their own earlier signup under a different email or phone). It comes back asrejected with a machine-readable
class alongside the usual reason:
reason_code: "duplicate_identity"onGET /api/v2/kycresponses and on theidentity.verification.updatedwebhook.reasonCode: "duplicate_identity"plus aresolutionobject on the v1 cardholder endpoint,GET /api/v1/cardholders/:id/kyc/status:
- It is not retryable. Re-submitting documents can never fix it, so don’t send the user back through the upload flow.
- The user resolves it themselves. Relay
resolution.message(or your own copy of it). On this rejection the verification page shows a Link that account flow inline: the user enters the other account’s email or phone, proves control with a one-time code, and the two accounts merge. The identity-verified account survives, and both identifiers sign in afterwards. - You are never told which account conflicts. For privacy, the API only says a conflict exists; only the user, by proving control of the other identifier, can resolve it.
approved (verified on the v1 endpoint),
with nothing else for you to do and no support contact needed.
Testing in test mode
With a test-mode credential, the whole flow runs against a test environment: no real identity check happens, and no real personal data is needed. One thing changes — test verifications never complete on their own, because there is no reviewer behind them. Two ways to finish one:-
From your backend — force a verdict with
Simulate an outcome:
The status flips instantly and
identity.verification.updatedfires — exactly the signal a real review produces, delivered to your test-mode webhook endpoints. Simulaterejected(optionally with a customreason) to test your failure state, andrequires_inputto test the “send new documents” retry loop from When a review sends the user back. -
From the browser — in test mode,
iframe_urlopens a test-mode page with the same two choices: complete instantly with a chosen outcome, or preview the document-and-face-scan flow an end user would see. Handy when you’re testing the embedded iframe step itself.
403 sandbox_only,
and live verifications are decided by the identity provider.