Card Scanner API

Create a card-scan session for your users, then receive a webhook when verification completes. This reference covers the two integration points external clients need: POST /v1/sessions and the session.completed webhook payload.

Base URL: api.card-scanner.sonio-group.com Auth: X-API-Key Content-Type: application/json

Overview

Integrating the card scanner is a two-step flow:

  1. Your backend calls POST /v1/sessions to mint a short-lived scan session for a given flowId (the scan flow/branding configuration your SONIO contact provisions for you).
  2. You hand the returned continuationToken to your client (mobile app / web view), which loads the SONIO scanner UI and completes the capture.
  3. When the session reaches a terminal state, SONIO POSTs a session.completed webhook to the URL configured for your flow, carrying the verification result.

There is no polling endpoint for session status in this version of the API — the webhook is the source of truth for the outcome. Use clientReferenceId (set at session creation) to correlate the webhook back to your own records.

Authentication

Every request to POST /v1/sessions must include an X-API-Key header. Keys are issued per-integration by SONIO and may optionally be scoped to a single flowId.

Header
X-API-Key: <your-api-key>
Key scoping. If your key is scoped to a specific flow, requests for any other flowId are rejected with 403 Forbidden. Unscoped keys may create sessions for any flow you've been granted access to.
Keep it secret. Treat your API key like a password — it must only ever be sent from your backend, never embedded in a mobile app or browser bundle. Rotate a key immediately if you suspect it has leaked; contact SONIO support to revoke and reissue.

Quickstart

Create a session and inspect the response:

cURL
curl -X POST https://api.card-scanner.sonio-group.com/v1/sessions \
  -H "X-API-Key: sk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
        "flowId": "b6f1e9f0-2b7a-4b43-9b7a-4f1a2c9d3e10",
        "clientReferenceId": "order_48213",
        "locale": "en-US"
      }'
Response — 201 Created
{
  "id": "0f3c2a1b-9d4e-4a7b-8c2d-1e5f6a7b8c9d",
  "continuationToken": "6f1e9f02b7a4b439b7a4f1a2c9d3e10a1b2c3d4e5f60718293a4b5c6d7e8f90",
  "flowId": "b6f1e9f0-2b7a-4b43-9b7a-4f1a2c9d3e10",
  "status": "pending",
  "expiresAt": "2026-10-07T23:40:00.000Z",
  "startUrl": "https://card-scanner.sonio-group.com/?flow_id=b6f1e9f0-2b7a-4b43-9b7a-4f1a2c9d3e10&locale=en-US&sid=0f3c2a1b-9d4e-4a7b-8c2d-1e5f6a7b8c9d&tok=6f1e9f02b7a4b439b7a4f1a2c9d3e10a1b2c3d4e5f60718293a4b5c6d7e8f90"
}

Hand startUrl straight to a QR code or redirect to resume the scan session in the SONIO scanner UI — no need to assemble the URL yourself. Alternatively, use continuationToken directly if your client builds its own URL. Then listen for the webhook described below to get the result.

POST /v1/sessions 201

Creates a new card-scan session scoped to a flow.

Request body

FieldTypeRequiredNotes
flowIdstring (UUID)YesThe scan flow to run. Provided by SONIO when your integration is provisioned.
clientReferenceIdstringNoYour own identifier (order id, user id, etc). Echoed back in the webhook payload so you can correlate results.
localestringNoLocale hint for the scanner UI copy.

Response fields

FieldTypeNotes
idstring (UUID)Session identifier.
continuationTokenstringOpaque token your client uses to resume/open the scan UI for this session. Treat as sensitive — it grants access to continue the session.
flowIdstring (UUID)Echoes the request's flowId.
statusstringInitial status (pending immediately after creation).
expiresAtstring (ISO 8601)Session expiry — the scan must complete before this time.
startUrlstring (URL), nullableReady-to-use URL for the scanner web app — hand it directly to a QR code or a redirect. Carries flow_id/locale/sid/tok query params; locale is always resolved (your request's locale, or the flow's default — never blank). Does not carry clientReferenceId. null only if the server has no scanner web app base URL configured (should not happen in any deployed environment).

Error codes

StatusBodyMeaning
400 { "error": "flowId must be a valid UUID" } Request body failed validation (bad/missing flowId, clientReferenceId, or locale), or the flow itself is unknown/inactive on SONIO's side.
401 { "error": "Missing X-API-Key header" } / { "error": "Invalid API key" } No X-API-Key header was sent, or the key doesn't match any active key.
403 { "error": "API key is not authorized for this flow" } Your key is valid but scoped to a different flowId than the one requested.
429 { "error": "Too many requests", "message": "..." } Rate limit exceeded — see Rate limits.
500 { "error": "Failed to create session" } / { "error": "Session API key authentication unavailable" } Unexpected server-side failure. Safe to retry with backoff; contact SONIO support if persistent.

Rate limits

POST /v1/sessions is rate-limited per source IP, on top of the general API rate limit applied to all endpoints. Limits are configured per environment and may change without notice; a 429 response includes RateLimit-* headers indicating the window and remaining budget. Back off and retry after the window resets rather than retrying immediately.

Webhook: session.completed

When a session reaches a terminal state (succeeded, failed, document_not_accepted, cancelled, expired, or error), SONIO sends a single POST request with a JSON body to the webhook URL configured for your flow.

Request

Headers
POST <your configured webhook URL>
Content-Type: application/json
Body — success example
{
  "cardVerification": [
    {
      "provider": "SONIO",
      "status": "COMPLETED",
      "validation": "PASS",
      "cardActive": true,
      "score": "100.00",
      "reason": "",
      "reasonDetails": "",
      "externalIds": { "interviewId": "pm_1Oa2b3C4d5E6f7G8" },
      "attempts": 1,
      "processedData": {
        "creditcard": {
          "ocr": {
            "cardType": "personal",
            "cardNumber": "424242******4242",
            "expireAt": "12",
            "expireYear": "2030",
            "issuingNetwork": "Visa",
            "cardHolder": "Jane Doe"
          },
          "imagesUrl": {
            "frontCard": "https://.../front.jpg",
            "backCard": "https://.../back.jpg"
          }
        }
      }
    }
  ]
}
Body — failure example
{
  "cardVerification": [
    {
      "provider": "SONIO",
      "status": "COMPLETED",
      "validation": "FAIL",
      "cardActive": false,
      "score": "0.00",
      "reason": "document_not_accepted",
      "reasonDetails": "Submitted card image was not accepted",
      "externalIds": { "interviewId": "0f3c2a1b-9d4e-4a7b-8c2d-1e5f6a7b8c9d" },
      "attempts": 1,
      "processedData": {
        "creditcard": {
          "ocr": {
            "cardType": "",
            "cardNumber": "",
            "expireAt": "",
            "expireYear": "",
            "issuingNetwork": "",
            "cardHolder": ""
          },
          "imagesUrl": { "frontCard": "", "backCard": "" }
        }
      }
    }
  ]
}

Field reference

FieldNotes
validation"PASS" only when the session succeeded and card verification was confirmed; otherwise "FAIL".
cardActiveThe raw Stripe SetupIntent-verified boolean, independent of session status. A card Stripe verified fine on a session the user later abandoned/cancelled reports cardActive: true alongside validation: "FAIL" — use validation for the overall outcome and cardActive specifically for the card/Stripe result.
reason / reasonDetailsEmpty strings on a pass. On a fail, reason is a short code (session status or cancellation reason) and reasonDetails is a human-readable description.
externalIds.interviewIdThe Stripe payment method id when available, otherwise your session id. Use this (or your own session-id mapping) to match the webhook to the session you created — clientReferenceId is not present in this payload.
processedData.creditcard.ocr.cardNumberTruncated PAN: first six digits (BIN) + six literal asterisks + last four digits, e.g. "424242******4242" — the PCI DSS v4.0.1 Req 3.3.1-permitted truncation format. Empty string whenever either half wasn't captured (rejected scan, pre-SPWN-866 historical session, or a purged record). The full PAN is never included in the webhook payload — SONIO never persists or transmits the plaintext card number.
processedData.creditcard.imagesUrlURLs to the redacted (PAN-masked) front/back card images, if captured for your flow.

Expected response

Your endpoint should respond with any 2xx status to acknowledge receipt. Non-2xx responses or timeouts are retried up to 3 attempts total (roughly 1s then 5s apart), after which the delivery is marked failed and not retried further. Make your webhook handler idempotent — retries can resend the same payload.

Webhook security notes

No signature header is sent with this version of the webhook. Verify authenticity by:
  • Only accepting requests on the exact URL you registered for your flow (treat the URL path as a shared secret — use a long, unguessable path segment).
  • Allowlisting SONIO's outbound IP range at your firewall/load balancer, if provided by your SONIO contact.
  • Validating that the externalIds.interviewId / session id in the payload matches a session you actually created via POST /v1/sessions, and ignoring anything that doesn't.

If your integration requires cryptographic payload signing (e.g. an HMAC signature header), contact your SONIO integration contact — this is on the roadmap but not yet available on this endpoint.