afinX KYC is a hosted widget that captures Ghana Card / passport / driver's license, runs a liveness selfie check, and returns a clean pass / review / fail decision plus per-attribute match scores. Build your decisioning UI today against our sandbox — same shape as production.
https://kyc.afinx.co/kyc_<token>
Three calls. The widget walks the user through ID type → ID capture → selfie liveness, then returns a decision. Cross-check it against a previously linked Bridge item if you want belt-and-braces fraud defense.
Server-side, with your sandbox or live bearer token. Optionally pass connected_item_id to cross-check the verified identity against a linked bank or wallet's account holder name.
curl https://api.afinx.co/v1/kyc/sessions \
-H "Authorization: Bearer afinx_test_sk_…" \
-H "Content-Type: application/json" \
-d '{
"user_id": "user_123",
"webhook_url": "https://yourapp.com/webhooks/kyc",
"connected_item_id": "item_…"
}'Drop the user into the hosted widget with their kycToken. They walk through ID type → ID capture → selfie. The widget reports back via postMessage. In sandbox, the user picks their outcome from a Stripe-test-card-style menu so you can drive every decision branch.
window.open(
`https://kyc.afinx.co/${kycToken}`,
'afinx-kyc',
'width=520,height=720',
);
window.addEventListener('message', (e) => {
if (e.origin !== 'https://kyc.afinx.co') return;
if (e.data?.type === 'KYC_SUCCESS') {
exchangeToken(e.data.publicToken);
}
});Take the publicToken from the postMessage event and exchange it server-side for the persisted KycSubmission — decision, per-attribute match scores, watchlist screening result, evidence trail.
curl https://api.afinx.co/v1/kyc/exchange-token \
-H "Authorization: Bearer afinx_test_sk_…" \
-d '{ "publicToken": "public_kyc_…" }'
# Returns:
# {
# "submissionId": "sub_…",
# "decision": "pass" | "review" | "fail",
# "decisionReason": "all_attributes_matched",
# "matchScores": { "name": { "match": "high", "score": 0.94 }, … },
# "watchlist": { "sanctions": false, "peps": false, … }
# }Real ID types, real decision logic, real edge cases. Built for the African market — Ghana Card, regional driver's licenses, machine- readable passports.
Three ID types in sandbox today. NIA-format Ghana Card validation, machine-readable passport zone, DVLA driver's license. BVN/NIN coming soon.
Selfie capture with liveness detection. Match score against the photo on the submitted ID. In sandbox, pick the outcome — happy, low-quality, or liveness fail.
Worst-of-three decision: any fail outcome → fail; any review outcome → review; otherwise pass. Per-attribute scores so you can build your own thresholds on top.
Sanctions, PEP, and adverse-media checks bundled into every submission. Hits surface in the response payload — wire your own escalation logic from there.
Pass connected_item_id to compare the verified identity against the holder name on a previously linked Bridge item. account_holder_match returns high / no_match — kills synthetic ID attempts cold.
Built-in event simulator on every submission detail page. Trigger session.completed, review_completed, sanctions_match, id_expired_warning — test your async handlers without waiting for real events.
Stripe gives you test card numbers that map to outcomes. We give you outcome buttons in the widget. Pick the path you want to test — nothing's left to chance.
| Outcome | Decision | Reason |
|---|---|---|
| happy | pass | all_attributes_matched |
| review_low_confidence | review | low_confidence |
| review_sanctions | review | sanctions_hit |
| fail_expired | fail | id_expired |
| fail_no_match | fail | id_did_not_match_selfie |
| fail_liveness | fail | liveness_failure |
One widget, two integration shapes. Popup keeps the user on your page and reports back via postMessage. Redirect navigates to the widget and back to your redirectUri — Stripe-Checkout style.
async function verifyIdentity(kycToken) {
const popup = window.open(
`https://kyc.afinx.co/${kycToken}`,
'afinx-kyc',
'width=520,height=720',
);
return new Promise((resolve, reject) => {
window.addEventListener('message', (e) => {
if (e.origin !== 'https://kyc.afinx.co') return;
if (e.data?.type === 'KYC_SUCCESS') {
popup?.close();
resolve(e.data.publicToken);
}
if (e.data?.type === 'KYC_EXIT') reject(new Error('cancelled'));
if (e.data?.type === 'KYC_ERROR') reject(new Error(e.data.error));
});
});
}// 1. open
window.location.href =
`https://kyc.afinx.co/${kycToken}?mode=redirect`;
// 2. handle the callback at your redirect_uri
// e.g. /kyc/callback?afinx_status=success
// &public_token=public_kyc_…
const params = new URLSearchParams(location.search);
switch (params.get('afinx_status')) {
case 'success':
exchangeToken(params.get('public_token'));
break;
case 'exit':
showCancelled();
break;
case 'error':
showError(params.get('error_message'));
break;
}Per-submission audit trail. Hashed and masked ID storage — we never keep plaintext numbers. Watchlist screening on every check. Webhook signatures (production). Submission disconnect revokes everything. Documentation written for your auditor, not just your engineers.