Identity API
Reference
Hosted verification sessions
Send a customer a link. They match their ID against the national register and pass a camera liveness check on a Sendexa-hosted page, and you get the result.
How it works
- Your server creates a session and gets back a
verificationUrl. - You send that link to the customer by SMS, email, WhatsApp or in your app. Sendexa doesn't send it for you.
- The customer sees a consent screen with your business name, logo and privacy-policy link (set the link under Hosted Sessions → Settings). Nothing is collected until they agree.
- If you asked for an ID check, the customer enters their Ghana Card or Voter ID details and we match them against the register. They get up to 3 attempts.
- They photograph their card (front, and back for a Ghana Card). On a laptop they can scan a QR code to continue on their phone.
- The camera coaches them into the oval and takes a selfie. During each liveness gesture the screen briefly flashes colours the server picked at random; the server checks the camera frames show the face reflecting exactly those colours, which a replayed video can't do.
- You receive a
VERIFICATION_COMPLETEDwebhook, or poll the session. If you set aredirectUrl, the customer is sent back to it with?sessionId=…&status=….
Billing
The ID step is billed as a normal KYC check each time the register answers (at most 3 times per session). The session fee is billed when every step has passed, including sessions held for review (whatever you decide). Failed and expired sessions don't pay it.
POST/v1/verify/sessions
Create a session. Works with any valid API key.
| Field | Type | Description |
|---|---|---|
contactrequired | string | The customer's phone number or email. Stored for your records; we don't message it. |
contactTyperequired | string | PHONE or EMAIL |
idDocument | string | GHANA_CARD or VOTER_ID to add the ID step. Leave it out for a liveness-only session. |
purpose | string | Up to 200 characters, shown to the customer, e.g. "opening your account". |
challenges | string[] | Which gestures to ask for: BLINK, SMILE, LOOK_LEFT, LOOK_RIGHT, LOOK_UP, LOOK_DOWN. Defaults to all six. |
ttlMinutes | number | How long the link stays valid, 5 to 60. Default 20. |
redirectUrl | string | https link the customer returns to when they finish. We append sessionId and status (COMPLETED or FAILED). Treat it as a UX hint and confirm the result by webhook or GET. |
Bash
curl -X POST 'https://api.sendexa.co/v1/verify/sessions' \-H 'Authorization: Basic <YOUR_TOKEN>' \-H 'Content-Type: application/json' \-d '{"contact": "+233241234567","contactType": "PHONE","idDocument": "GHANA_CARD","purpose": "opening your account","challenges": ["BLINK", "LOOK_LEFT", "SMILE"],"redirectUrl": "https://acme.com/onboarding/verified"}'
Returns 201:
JSON
{"success": true,"data": {"sessionId": "q8V3nX0bTzK2mR7wYp1cHg","verificationUrl": "https://dash.sendexa.co/v/q8V3nX0bTzK2mR7wYp1cHg","challenges": ["BLINK", "LOOK_LEFT", "SMILE"],"idDocument": "GHANA_CARD","redirectUrl": "https://acme.com/onboarding/verified","expiresAt": "2026-10-12T10:20:00.000Z"}}
GET/v1/verify/sessions/:sessionId
Fetch a session's current state.
JSON
{"success": true,"data": {"sessionId": "q8V3nX0bTzK2mR7wYp1cHg","contact": "+233241234567","contactType": "PHONE","purpose": "opening your account","status": "COMPLETED","challenges": ["BLINK", "LOOK_LEFT", "SMILE"],"completedChallenges": ["BLINK", "LOOK_LEFT", "SMILE"],"idDocument": "GHANA_CARD","idVerified": true,"identityCheckId": "IDN-5f0c2a9e-7c1b-4f7e-9d2a-0b6c1e8f3a21","failReason": null,"consentAt": "2026-10-12T10:01:02.000Z","riskFlags": [],"reviewedByUserId": null,"reviewedAt": null,"reviewNote": null,"redirectUrl": "https://acme.com/onboarding/verified","images": [{ "id": "cm2x9a1b20003", "kind": "ID_FRONT", "createdAt": "2026-10-12T10:02:40.000Z" },{ "id": "cm2x9a1b20004", "kind": "ID_BACK", "createdAt": "2026-10-12T10:03:05.000Z" },{ "id": "cm2x9a1b20005", "kind": "SELFIE", "createdAt": "2026-10-12T10:03:51.000Z" }],"cost": 0.15,"currency": "USD","expiresAt": "2026-10-12T10:20:00.000Z","completedAt": "2026-10-12T10:04:31.000Z","createdAt": "2026-10-12T10:00:00.000Z"}}
| Field | Type | Description |
|---|---|---|
status | string | See the table below. |
idVerified | boolean | null | null until the customer submits ID details; then whether they matched. |
identityCheckId | string | null | The KYC check run for the ID step. Fetch it from GET /v1/identity/checks/:checkId (needs identity.read) to see the matchScore. |
failReason | string | null | Why a FAILED session failed. See below. |
consentAt | string | null | When the customer accepted the consent screen. |
images | object[] | Photos collected (ID_FRONT, ID_BACK, SELFIE). They're stored encrypted and can be viewed by your team in the dashboard, where each view is logged. They're deleted after your retention period (default 90 days). |
cost | number | The session fee charged; 0 unless the session completed. ID-step charges are separate KYC checks. |
GET/v1/verify/sessions
List your sessions, newest first. Query parameters:
limit (default 50, max 100) and offset. Returns { sessions, total, limit, offset }.POST/v1/verify/sessions/:sessionId/review
Approve or reject a session in
NEEDS_REVIEW from your own back office. Your team can do the same in the dashboard. The first decision wins; a second gets 409. The webhook fires after the decision.| Field | Type | Description |
|---|---|---|
decisionrequired | string | APPROVE or REJECT |
note | string | Why. Required when rejecting; up to 1000 characters. Returned as reviewNote. |
Bash
curl -X POST 'https://api.sendexa.co/v1/verify/sessions/q8V3nX0bTzK2mR7wYp1cHg/review' \-H 'Authorization: Basic <YOUR_TOKEN>' \-H 'Content-Type: application/json' \-d '{ "decision": "REJECT", "note": "Selfie does not match the card photo" }'
Statuses
| Value | Meaning |
|---|---|
PENDING | Created; the customer hasn't started. |
IN_PROGRESS | The customer has started (submitted ID details or completed a gesture). |
NEEDS_REVIEW | Every step passed, but a risk flag needs your decision. Approve or reject it in the dashboard or with the review endpoint below. |
COMPLETED | Every step passed (or you approved it in review). The customer is verified. |
FAILED | A step failed. failReason says which. |
EXPIRED | The link timed out before the customer finished. |
Risk flags
In riskFlags. Any flag holds the session at NEEDS_REVIEW instead of completing it.
| Value | Meaning |
|---|---|
LOW_FLASH_RESPONSE | The customer's face barely reflected the screen-flash colours. Often bright surroundings; sometimes a replayed or injected video. Check the selfie and card photos. |
ID_PARTIAL_MATCH | The ID matched, but the register's name-match score was under 100%. Check the names against the card photo. |
Fail reasons
| Value | Meaning |
|---|---|
ID_MISMATCH | The ID details didn't match the register (or the register couldn't confirm them). |
REGISTRY_UNAVAILABLE | The register couldn't be reached on any of the customer's 3 attempts. |
BILLING_FAILED | Your wallet balance was too low to run the ID check or pay the session fee. |
STATIC_FEED | The camera picture didn't change between frames: a photo or a frozen feed. |
REJECTED_IN_REVIEW | Your team (or your server) rejected it in review. reviewNote says why. |
any other text | A liveness failure, in plain English, e.g. "No blink detected". |
A session only reaches COMPLETED if the ID step passed (when there is one), every gesture passed, and either nothing was flagged or you approved it. Treat anything else as not verified.