Agentcard Identity verification API
Verify a connected user's identity: upload their ID, submit any extra fields we ask for, then show a short face scan.
Verify a connected user's identity: upload their ID, submit any extra fields we ask for, then show a short face scan.
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
find_apisBrowse and filter every API in the catalog.get_api_artifactsOne API's artifacts, grouped by type.get_openapiThe primary OpenAPI for this API.find_similar_apisAPIs that look like this one.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.curl "https://apis.io/api/v1/apis/agentcard:agentcard-identity-verification-api"
curl "https://apis.io/api/v1/apis?limit=25"
Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: Agentcard Identity verification API
version: 2.0.0
description: The Agentcard v2 API — connect your users and verify their identity from your own backend. Every call is authenticated with a platform access token minted from your `client_id` + `client_secret`.
servers:
- url: https://api.agentcard.sh
description: There is one base URL. Sandbox vs production is decided by the client credential you use, never by the host.
security:
- platformToken: []
tags:
- name: Identity Verification
description: 'Verify a connected user''s identity: upload their ID, submit any extra fields we ask for, then show a short face scan.'
paths:
/api/v2/kyc/documents/front:
post:
tags:
- Identity Verification
summary: Upload the front of the ID
operationId: kycUploadFront
description: 'Uploads the front of the user''s identity document as a base64-encoded image. This step acknowledges receipt; the next step (`back`) tells you what comes next.
Any upload response may include a `warnings` array with actionable feedback (for example, that the other side of the document is still needed).
The response''s `extracted` object carries what the document reader pulled off the image — use it to prefill your details form so the user confirms instead of typing.'
requestBody:
$ref: '#/components/requestBodies/KycDocument'
responses:
'200':
description: Receipt acknowledged — upload the back next. May include a `warnings` array.
content:
application/json:
schema:
$ref: '#/components/schemas/KycState'
example:
object: kyc
status: awaiting_documents
extracted:
document_type: drivers_license
issuing_country: US
first_name: Jane
last_name: Doe
date_of_birth: '1990-05-14'
'400':
$ref: '#/components/responses/KycDocumentBadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/ConnectionNotFound'
'409':
$ref: '#/components/responses/UserConflict'
'422':
$ref: '#/components/responses/DocumentUnprocessable'
'502':
$ref: '#/components/responses/VerificationError'
/api/v2/kyc/documents/back:
post:
tags:
- Identity Verification
summary: Upload the back of the ID
operationId: kycUploadBack
description: 'Uploads the back of the document. The response tells you what to do next — this is the branch point of the flow:
- `needs_information` → collect exactly the `required_fields` and post them to `/kyc/information`.
- `requires_verification` → show the user the `iframe_url` for the face scan.
- `rejected` → the document couldn''t be verified.'
requestBody:
$ref: '#/components/requestBodies/KycDocument'
responses:
'200':
description: The next step of the flow.
content:
application/json:
schema:
$ref: '#/components/schemas/KycState'
examples:
needs_information:
summary: Needs more info
value:
object: kyc
status: needs_information
required_fields:
- national_id_number
- phone_number
extracted:
document_type: drivers_license
issuing_country: US
document_number: D1234567
first_name: Jane
last_name: Doe
date_of_birth: '1990-05-14'
address_line1: 123 Market St
address_city: San Francisco
address_region: CA
address_postal_code: '94105'
address_country: US
requires_verification:
summary: Ready for face scan
value:
object: kyc
status: requires_verification
iframe_url: https://in.sumsub.com/websdk/p/…
'400':
$ref: '#/components/responses/KycDocumentBadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/ConnectionNotFound'
'409':
$ref: '#/components/responses/UserConflict'
'422':
$ref: '#/components/responses/DocumentUnprocessable'
'502':
$ref: '#/components/responses/VerificationError'
/api/v2/kyc/information:
post:
tags:
- Identity Verification
summary: Submit information
operationId: kycSubmitInformation
description: 'Submits the extra fields requested by a `needs_information` response. Send only the fields listed in `required_fields`; values are trimmed, and a blank value counts as not provided. For a fresh verification, the response returns the `iframe_url` for the face scan. For an imported verification (Reusable KYC), the submit that completes the residential address returns `pending` and files the card issuer application right after it, in the background, so the response never waits on the issuer; the verification then moves to `approved` or `rejected` through the status poll or the `identity.verification.updated` webhook. Repeating the request is safe: it returns the current status.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- user_id
properties:
user_id:
type: string
description: The connected user's id.
first_name:
type: string
last_name:
type: string
date_of_birth:
type: string
description: ISO 8601 date, `YYYY-MM-DD`.
national_id_number:
type: string
description: The user's national ID or tax identification number for the country that issued their document — for US documents this is the SSN; for any other country it is the national ID / tax number printed on (or associated with) the document. Forwarded to the verification provider only; never stored by Agentcard.
phone_number:
type: string
description: E.164 format with country code (e.g. `+15551234567`).
address_line1:
type: string
address_line2:
type: string
address_city:
type: string
address_region:
type: string
description: State, province, or region.
address_postal_code:
type: string
address_country:
type: string
description: ISO 3166-1 alpha-2 country code (e.g. `US`).
user_ip:
type: string
description: 'The end user''s IP address as your frontend saw it (public IPv4 or IPv6; private or reserved addresses are rejected). Strongly recommended: identity screening checks the applicant''s IP, and an IP belonging to a datacenter (such as your backend''s) can fail an otherwise valid verification. Pass the address your user connected to you from; without it, your server''s own address may be recorded as a last resort and can fail that screening.'
example:
user_id: usr_7g8h9i
national_id_number: '123456789'
address_line1: 123 Main St
address_city: San Francisco
address_region: CA
address_postal_code: '94105'
address_country: US
responses:
'200':
description: 'The next step: `requires_verification` with the `iframe_url` for a fresh verification, or `pending` when the submit completed an imported verification (the card issuer application is filed right after the response).'
content:
application/json:
schema:
$ref: '#/components/schemas/KycState'
example:
object: kyc
status: requires_verification
iframe_url: https://in.sumsub.com/websdk/p/…
'400':
description: '`invalid_request` — missing `user_id`, or a `user_ip` that is not a public IPv4/IPv6 address. `invalid_fields` — a value didn''t check out; the error adds a `field_errors` object naming each field to fix.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error:
code: invalid_fields
message: Some of the information provided is invalid.
field_errors:
date_of_birth: Enter a valid date.
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/ConnectionNotFound'
'409':
$ref: '#/components/responses/UserConflict'
'502':
$ref: '#/components/responses/VerificationError'
/api/v2/kyc:
get:
tags:
- Identity Verification
summary: Get verification status
operationId: kycGetStatus
description: Polls the current verification status — the alternative to the `identity.verification.updated` webhook.
parameters:
- name: user_id
in: query
required: true
schema:
type: string
description: The connected user's id.
responses:
'200':
description: The current status.
content:
application/json:
schema:
$ref: '#/components/schemas/KycState'
example:
object: kyc
status: approved
'400':
description: '`invalid_request` — missing `user_id`.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/ConnectionNotFound'
'409':
$ref: '#/components/responses/UserConflict'
/api/v2/kyc/simulate:
post:
tags:
- Identity Verification
summary: Simulate an outcome (test mode)
operationId: kycSimulate
description: '**Test mode only.** Drives a test-mode verification to a chosen terminal outcome instantly — test verifications never complete on their own. The simulated verdict flows through the same status contract and fires the same `identity.verification.updated` webhook a real review produces, so your status handling and webhook consumer are exercised end to end. Requires a test-mode client credential; live tokens get `403 sandbox_only`.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- user_id
- outcome
properties:
user_id:
type: string
description: The connected user's id.
outcome:
type: string
enum:
- approved
- rejected
- requires_input
description: The verdict to apply. `approved` — verification succeeds. `rejected` — terminal rejection. `requires_input` — a retryable bounce asking for new document photos.
reason:
type: string
maxLength: 300
description: Optional end-user-safe explanation carried on non-approved outcomes — it appears as `reason` in statuses and webhook events, exactly like a real review's. It must not name internal providers or identifiers (rejected with `400 invalid_reason`), since it is shown to end users verbatim.
example:
user_id: usr_7g8h9i
outcome: rejected
reason: The name on the document does not match the application.
responses:
'200':
description: The verification's new state, exactly as `GET /api/v2/kyc` now reports it.
content:
application/json:
schema:
$ref: '#/components/schemas/KycState'
example:
object: kyc
simulated: true
status: rejected
reason: The name on the document does not match the application.
'400':
description: '`invalid_request` — missing `user_id` or an unknown `outcome`. `invalid_reason` — the `reason` names an internal provider or identifier (it is shown to end users verbatim).'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: '`sandbox_only` — the token is a live credential. Live verifications are decided by the identity provider and cannot be simulated.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error:
code: sandbox_only
message: Simulated verification outcomes only exist in test mode. Live verifications are decided by the identity provider.
'404':
$ref: '#/components/responses/ConnectionNotFound'
'409':
$ref: '#/components/responses/UserConflict'
'502':
$ref: '#/components/responses/VerificationError'
/api/v2/kyc/import:
post:
tags:
- Identity Verification
summary: Import a verification (Sumsub share token)
operationId: kycImport
description: 'Import a verification you already ran on your **own Sumsub account** (Reusable KYC). Generate a single-use share token for the Agentcard client id, send it here, and the user skips document capture and the face scan. A successful import drops into the exact same status contract as a fresh verification: poll `GET /api/v2/kyc` or listen for `identity.verification.updated`. Requires one-time partner pairing between your Sumsub account and Agentcard''s (per environment) — ask your Agentcard contact to enable it. Share tokens are single-use with a short TTL, so generate one fresh per import attempt.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- user_id
- share_token
properties:
user_id:
type: string
description: The connected user's id.
share_token:
type: string
description: A Sumsub share token generated by your account for the Agentcard client id (`POST /resources/accessTokens/shareToken` with `forClientId`). Single-use; expires after its `ttlInSecs`.
user_ip:
type: string
description: 'The end user''s IP address as your frontend saw it (public IPv4 or IPv6; private or reserved addresses are rejected). Strongly recommended: identity screening checks the applicant''s IP, and an IP belonging to a datacenter (such as your backend''s) can fail an otherwise valid verification. Pass the address your user connected to you from; without it, your server''s own address may be recorded as a last resort and can fail that screening.'
example:
user_id: usr_7g8h9i
share_token: _act-jwt-eyJhbGciOiJub25lIn0...
responses:
'200':
description: 'The verification''s state after the import, exactly as `GET /api/v2/kyc` now reports it. A complete applicant lands on `pending` while checks run (then `approved`). An applicant whose shared verification carried no residential address lands on `needs_information` with the exact `required_fields`: submit them via `POST /api/v2/kyc/information` right away (import first, then information), and the card issuer application is filed as soon as the address lands.'
content:
application/json:
schema:
$ref: '#/components/schemas/KycState'
example:
object: kyc
status: pending
'400':
description: '`invalid_request` — missing `user_id` or `share_token`, or a `user_ip` that is not a public IPv4/IPv6 address. `invalid_share_token` — the token is invalid, expired, or already used; generate a fresh one and retry. `sharing_not_configured` — test mode has no sharing tenant configured; use `POST /api/v2/kyc/simulate` instead.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: '`sharing_not_enabled` — your Sumsub account and Agentcard''s are not paired in this environment. Complete partner pairing first.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: '`connection_not_found` — the user has no active connection under this client.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: '`verification_in_progress` — a verification is already in flight for this user. Drive it to completion with the status flow instead of importing.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'422':
description: '`verification_not_compatible` — the shared verification does not include everything the card program requires (an identity document and a selfie check). Share applicants from a level that includes both, or let the user run the standard flow.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
responses:
UserConflict:
description: '`user_conflict` — the email on file in your organization belongs to a different account. Contact support.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
VerificationError:
description: '`verification_error` — the step failed downstream. Try again.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
ConnectionNotFound:
description: '`connection_not_found` — no connection exists for that user under your client.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
KycDocumentBadRequest:
description: '`invalid_request` — missing `user_id` or `image`, an unrecognized `document_type` / `issuing_country`, or a `user_ip` that is not a public IPv4/IPv6 address. `invalid_image` — `image` isn''t valid base64. `client_credentials_required` — the token wasn''t minted from client credentials.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Unauthorized:
description: '`unauthorized` — the platform access token is missing or expired. Exchange your client credentials for a fresh one.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
DocumentUnprocessable:
description: '`document_rejected` — the image couldn''t be processed; ask the user to retake the photo. `document_expired` — the document itself is expired; ask for a valid one. Either may include a `warnings` array with actionable feedback.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error:
code: document_rejected
message: The document image could not be processed — ask the user for a new, clear photo.
warnings:
- The verification provider still needs the back of the document — ask the user for the other side.
requestBodies:
KycDocument:
required: true
content:
application/json:
schema:
type: object
required:
- user_id
- image
properties:
user_id:
type: string
description: The connected user's id.
image:
type: string
description: The image bytes, base64-encoded.
mime_type:
type: string
default: image/jpeg
description: The image's MIME type.
document_type:
type: string
enum:
- drivers_license
- state_id
- passport
description: Optional document-type hint. Without it the document is auto-detected and defaults to a US driver's license. If a response `warnings` entry asks you to resubmit with a `document_type`, send the same image again with this field set — no need to go back to the user.
issuing_country:
type: string
description: Optional ISO 3166-1 country code (alpha-2 or alpha-3) of the country that issued the document. Defaults to US when the document doesn't reveal it — always send it for non-US documents (e.g. `DE` for a German national ID).
user_ip:
type: string
description: 'The end user''s IP address as your frontend saw it (public IPv4 or IPv6; private or reserved addresses are rejected). Strongly recommended: identity screening checks the applicant''s IP, and an IP belonging to a datacenter (such as your backend''s) can fail an otherwise valid verification. Pass the address your user connected to you from; without it, your server''s own address may be recorded as a last resort and can fail that screening.'
example:
user_id: usr_7g8h9i
image: <base64>
mime_type: image/jpeg
schemas:
KycState:
type: object
description: The single status contract every KYC response carries.
properties:
object:
type: string
enum:
- kyc
status:
type: string
enum:
- awaiting_documents
- needs_information
- requires_verification
- pending
- approved
- rejected
description: '`awaiting_documents` — upload the front and back. `needs_information` — collect the `required_fields` and submit them. `requires_verification` — show the user the `iframe_url`. `pending` — under review, no action needed. `approved` — verified, done. `rejected` — the user did not pass. Statuses are not one-way: a review can send a user back — `pending` may return to `needs_information` (a detail didn''t match the document; re-collect the listed fields and resubmit, the check re-runs automatically) or to `awaiting_documents` (the images were unusable; upload both sides again). Always branch on the current status.'
required_fields:
type: array
items:
type: string
description: Only on `needs_information` — exactly the fields to collect and post to `/kyc/information`.
iframe_url:
type: string
description: 'On every actionable status (`awaiting_documents`, `needs_information`, `requires_verification`): the hosted page that collects whatever the verification still needs, and the face scan at `requires_verification`. Embed it in an iframe with `allow="camera; microphone"`. Short-lived: always use the most recent one from a poll or webhook, never a stored copy.'
warnings:
type: array
items:
type: string
description: Optional, on document uploads — actionable feedback safe to show the user (for example, that the other side of the document is still needed).
extracted:
type: object
description: 'Optional, on document uploads — what the document reader pulled off the uploaded image(s), so you can prefill your details form instead of asking the user to re-type what the ID already says. Keys match the `/kyc/information` request fields (`first_name`, `last_name`, `date_of_birth`, `address_line1`, `address_city`, `address_region`, `address_postal_code`, `address_country`) plus `document_type`, `issuing_country`, and `document_number` (the number printed on the document — for US documents this is NOT the SSN, so never prefill it into `national_id_number` when `issuing_country` is `US`). Fields appear as they become readable: the front usually carries the name and date of birth; a US back adds the barcode address. Always let the user confirm or correct prefilled values.'
additionalProperties:
type: string
reason:
type: string
description: Optional, on `needs_information`, `awaiting_documents`, `requires_verification`, and `rejected` — a short, end-user-safe explanation of what the review asked for (for example, “Enter your full name exactly as it appears on your identity document.”). Safe to show the user verbatim.
Error:
type: object
description: Every v2 error uses the same envelope.
properties:
error:
type: object
properties:
code:
type: string
description: A stable, machine-readable string (snake_case). Branch on this.
message:
type: string
description: A human-readable explanation, safe to log.
docs:
type: string
description: A link back to the reference.
field_errors:
type: object
additionalProperties:
type: string
description: Only on `invalid_fields` — names each field to fix.
warnings:
type: array
items:
type: string
description: Only on document upload errors — actionable feedback safe to show the user.
securitySchemes:
platformToken:
type: http
scheme: bearer
description: 'A platform access token. Get one on the **Create an access token** endpoint by exchanging your `client_id` + `client_secret`, then send it as `Authorization: Bearer <token>`. Tokens live one hour.'
userAccessToken:
type: http
scheme: bearer
description: The user's connection access_token (user authentication), or an org-minted buy_token for org-owned accounts.
x-provenance:
method: harvested
publisher: Agentcard
source: https://docs.agentcard.sh/openapi.json
note: split by refine-openapis from the provider's own published document, verified by fetch (openapi/_original/*.fetch.json);
stamped by stamp-openapi-provenance.py (roadmap#793)