# Verification modes

Every flow has a **verification mode**, chosen on the Configure tab when you build it:

- **Backend** (the default): proofs are verified by Self's servers. Results arrive through [webhooks](/docs/self-enterprise/webhooks/overview/) and the [API](/docs/self-enterprise/sdk/nodejs/).
- **On-chain**: proofs are verified by a smart contract on **Celo**. Each verified user receives a **soulbound token (SBT)** in their wallet, a non-transferable, on-chain record that they passed your flow. You still get the same webhooks and API results.

The mode is part of the flow's configuration, so like every other setting it is [immutable once deployed](/docs/self-enterprise/flows/anatomy/#immutable-once-deployed). To switch modes, archive the flow and create a new one.

## Which mode should I use?

Pick **Backend** unless you specifically need an on-chain artifact. It supports every rule, has no wallet requirement for your users, and is the mode the rest of these docs assume.

Pick **On-chain** when the verification result must be readable by other smart contracts or wallets, for example token-gated apps, airdrops with sybil resistance, or membership that other on-chain protocols can check without calling your backend.

| | Backend | On-chain |
| --- | --- | --- |
| Where the proof is verified | Self's servers | A contract on Celo, deployed for your flow |
| What the user needs | The Self app | The Self app **and** a wallet |
| What the user receives | Nothing on-chain | A soulbound token (SBT) in their wallet |
| Results in webhooks and API | Yes | Yes, plus the transaction hash and contract address |
| Supported rules | All rules | All rules except **maximum age**, with [country-list limits](/docs/self-enterprise/flows/onchain-verification/#configuration-limits) |
| Networks | — | Celo Sepolia (test), Celo Mainnet (live) |
| Gas cost to you or the user | — | None, Self submits the transactions |

## What happens during a verification

The lifecycle is the same shape in both modes: create a session, the user verifies, you get a result. Here's where they differ, step by step:

**Backend**

1. Your backend calls `sessions.create(...)` and sends the user to the `verificationUrl`.
2. The user scans the QR code (or taps the deeplink) and approves in the Self app.
3. Self's servers verify the proof against the flow's rules.
4. The result lands everywhere at once: the `verification.completed` webhook, the session status, and the activity log.

**On-chain**

1. Same `sessions.create(...)` call. The only precondition is that the flow's contract is deployed for that environment (a one-time step that happens when you deploy the flow).
2. Before the QR code, the hosted page asks the user to connect a wallet and sign a gas-free message. That wallet receives the soulbound token.
3. The proof is verified by the flow's contract on Celo, which mints the token. Self submits the transaction and pays the gas.
4. Once the transaction confirms, the same webhook, session status, and activity log update, now carrying the transaction hash and contract address.

**What to expect:** on-chain adds the wallet step for the user and waits for the transaction to confirm, so results take a little longer to become terminal. Either way, treat the webhook as the authoritative result, exactly as in backend mode.

## What changes in your integration

Very little. Sessions are created the same way, the hosted page handles the wallet steps, and the terminal statuses are the same (`valid`, `invalid`, `error`, `expired`).

Three additions:

- The [`verification.completed` webhook](/docs/self-enterprise/webhooks/events/) carries `verification_mode: 'onchain'`, plus `tx_hash` and `contract_address` when a token was minted.
- The [Activity log](/docs/self-enterprise/dashboard/activity-log/) shows the mint transaction for each verification, linked to the block explorer.
- Session creation requires the flow's contract to be deployed for that environment; otherwise the API returns `409` with a `no_deployment` discriminator. Deploys finish in about a minute, so in practice you only see this immediately after creating the flow.

And one removal: on-chain verifications keep **no off-chain proof record**, the chain is the record. The session's `storage.state` reports `'skipped'` for them.

## Billing

On-chain verifications follow the same billing rules as off-chain: a confirmed mint (`valid`) and a contract rejection (`invalid`) are both charged; a verification that fails for infrastructure reasons is never charged. Test-environment verifications are never charged in either mode. See [Credits and usage](/docs/self-enterprise/billing/credits-and-usage/).

## Related

- [On-chain verification and SBTs](/docs/self-enterprise/flows/onchain-verification/): contracts, deploys, the wallet experience, and limits.
- [Anatomy of a flow](/docs/self-enterprise/flows/anatomy/): what a flow holds and why it's immutable.
- [Event catalog](/docs/self-enterprise/webhooks/events/): the webhook payload, including the on-chain fields.
