Skip to main content
Verify a connected user’s identity in three steps: upload the front and back of their ID, confirm any extra details we ask for, and show the user a short face scan. You build the screens; Agentcard handles the checks and tells you what to collect next. Order matters: ask for the ID photos first, before any form. Both upload responses return an 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 their user_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 back
2

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 information
3

Scan the face

When a response includes an 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:
Two things to know about 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.updated event. 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_url is 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 status

Learning the outcome

Verification finishes after the face scan. Agentcard sends an identity.verification.updated event to your configured webhook endpoints whenever the status changes:
When the new status needs something from you, the event carries it: 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 as rejected with a machine-readable class alongside the usual reason:
  • reason_code: "duplicate_identity" on GET /api/v2/kyc responses and on the identity.verification.updated webhook.
  • reasonCode: "duplicate_identity" plus a resolution object on the v1 cardholder endpoint, GET /api/v1/cardholders/:id/kyc/status:
Three things to know:
  • 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.
Once the user links the accounts, the rejection self-heals: the next poll of the same verification reads 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.updated fires — exactly the signal a real review produces, delivered to your test-mode webhook endpoints. Simulate rejected (optionally with a custom reason) to test your failure state, and requires_input to test the “send new documents” retry loop from When a review sends the user back.
  • From the browser — in test mode, iframe_url opens 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.
Re-simulating overwrites the previous verdict, so one test user can walk approved → rejected → approved without reconnecting. Simulated outcomes only exist in test mode: on a live token the endpoint returns 403 sandbox_only, and live verifications are decided by the identity provider.