🛡️ Identity verification widget

Verify your users
in seconds, not days

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.

Hosted — we capture, you decideStripe-test-card sandbox outcomesWebhook simulator built-in
kyc URL patternsession tokens expire in 30 min
https://kyc.afinx.co/kyc_<token>

How it works

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.

1

Create a KYC session

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_…"
      }'
2

Open the widget

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);
  }
});
3

Exchange for the submission

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, … }
# }
Available in sandbox today

What the widget handles

Real ID types, real decision logic, real edge cases. Built for the African market — Ghana Card, regional driver's licenses, machine- readable passports.

Ghana Card, passport, license

Three ID types in sandbox today. NIA-format Ghana Card validation, machine-readable passport zone, DVLA driver's license. BVN/NIN coming soon.

Selfie liveness check

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.

Pass / review / fail decisions

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.

Watchlist screening

Sanctions, PEP, and adverse-media checks bundled into every submission. Hits surface in the response payload — wire your own escalation logic from there.

Cross-check linked accounts

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.

Webhook simulator

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.

Drive every branch from sandbox

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.

OutcomeDecisionReason
happypassall_attributes_matched
review_low_confidencereviewlow_confidence
review_sanctionsreviewsanctions_hit
fail_expiredfailid_expired
fail_no_matchfailid_did_not_match_selfie
fail_livenessfailliveness_failure

Wire it up in an evening

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.

popup modepostMessage
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));
    });
  });
}
redirect mode?afinx_status=…
// 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;
}

Built for compliance from day one

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.