Skip to main content
A verification system handles the most sensitive data your users will ever give you, and it is a natural target. Most of what follows is standard practice; it is listed here because every item has been missed by someone.

Credentials

  • The OAuth client id and secret live in a secret manager, not in source, config files or environment files that get committed.
  • They exist only on your backend. No mobile build, browser bundle or third-party service holds them.
  • You know how to rotate them and who to contact when you need to.
If a client secret ever appears in a repository, a log, a ticket or a chat, treat it as compromised and rotate it — even if the repository is private. Deleting the commit does not un-leak it.

The token boundary

  • Your app receives the SDK token from an endpoint you own, over your authentication. It never calls chmod to get one.
  • That endpoint returns only the SDK token. The transaction id stays on your server, so a client cannot query someone else’s transaction.
  • That endpoint is rate-limited per user. Minting tokens costs you verification volume, and an unauthenticated or unlimited endpoint is an invitation to burn it.
  • The time limit on each transaction is as short as your flow allows. A token for a flow the user starts immediately does not need 24 hours.

Configuration comes from the server

  • The transaction configuration is built on your backend from your own records. Nothing in it is copied from a value the app sent.
  • The app cannot choose the verification type, the thresholds, or the accepted document list.
A modified client that can lower the liveness threshold or widen the accepted documents is weakening its own verification. The SDK configuration is safe to build in the app because it is cosmetic; the transaction configuration is not.

Webhooks

  • Your endpoint verifies the signature against the published keys before doing anything else.
  • It rejects calls whose timestamp is older than a few minutes.
  • It treats the webhook as a notification and fetches the transaction to read the decision, rather than trusting a status in the body.
  • It is idempotent — the same notification twice does not apply twice.
  • It is HTTPS and publicly reachable, but nothing else on that host trusts requests just because they arrived on it.
See Webhooks for the verification code.

Personal data

The full result contains extracted document fields, references to document and face images, and device signals. It is personal data under every privacy regime you operate in.
  • You store the minimum you act on: decision, finding codes, transaction id, timestamp. You fetch the full result by id when you need it rather than persisting it.
  • Whatever you do persist is encrypted at rest and access-controlled to the people who need it.
  • The full result never reaches your application logs. Log the transaction id and the decision, not the payload.
  • You have a retention policy for whatever you keep, and it is enforced.
  • Your privacy notice covers identity verification, and the SDK’s in-flow privacy notices are reviewed by whoever owns that for you.

What users are told

  • Detailed error output is disabled in production builds of the SDK.
  • Finding messages are never shown verbatim. Codes are mapped to your own copy, which tells a genuine user what to do and tells a fraudster nothing. See Decisions and issues.
  • Retries are capped, and fraud-signal findings go to manual review instead of offering another attempt.

Device rules for real users

  • Compromised-device and emulator detection are set to reject. If you relaxed them while testing on emulators, the configuration your real users get does not inherit that.
  • Any rule you set to ignore was a deliberate decision, not a leftover.

Operational

  • Someone is alerted when the rate of undetermined decisions rises. It usually means a configuration change broke something.
  • Webhook delivery failures and server errors from the API are monitored.
  • A reconciliation job periodically fetches transactions that never reached a terminal status, so a lost webhook cannot leave a user stuck.