*Example of the registration flow*
# Step 3: Wrap x402 calls in the agent Use this in agents that call paid x402 APIs. Without this client, agents may go straight to payment instead of using their AgentKit registration. ```typescript theme={"system"} import { createAgentkitClient } from '@worldcoin/agentkit' const agentkit = createAgentkitClient({ signer: { address: agentWallet.address, chainId: 'eip155:8453', type: 'eip191', signMessage: message => agentWallet.signMessage(message), }, }) const response = await agentkit.fetch('https://api.example.com/data') ``` Use `agentkit.fetch` anywhere the agent would otherwise call `fetch` for x402-protected APIs. It tries AgentKit first, then leaves the normal x402 payment fallback in place. If you cannot change the agent's HTTP client, add the fallback skill: ```bash theme={"system"} npx skills add worldcoin/agentkit agentkit-x402 ``` # Step 4: Wire the hooks-based server flow The example below shows the maintained Hono wrapper path. AgentKit itself is not Hono-only: Express and Next.js route handlers can use the same hooks and low-level helpers from the [SDK Reference](/agents/agent-kit/sdk-reference). ```typescript theme={"system"} import { Hono } from 'hono' import { serve } from '@hono/node-server' import { HTTPFacilitatorClient } from '@x402/core/http' import { ExactEvmScheme } from '@x402/evm/exact/server' import { paymentMiddlewareFromHTTPServer, x402HTTPResourceServer, x402ResourceServer, } from '@x402/hono' import { agentkitResourceServerExtension, createAgentBookVerifier, createAgentkitHooks, declareAgentkitExtension, InMemoryAgentKitStorage, } from '@worldcoin/agentkit' const WORLD_CHAIN = 'eip155:480' const BASE = "eip155:8453"; const WORLD_USDC = '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1' const payTo = '0xYourAddress' const facilitatorClient = new HTTPFacilitatorClient({ url: 'https://x402-worldchain.vercel.app/facilitator', }) const evmScheme = new ExactEvmScheme() // Register a money parser to accept USDC payments on WorldChain. .registerMoneyParser(async (amount, network) => { if (network !== WORLD_CHAIN) return null return { amount: String(Math.round(amount * 1e6)), asset: WORLD_USDC, extra: { name: 'USD Coin', version: '2' }, } }) const agentBook = createAgentBookVerifier() const storage = new InMemoryAgentKitStorage() const hooks = createAgentkitHooks({ agentBook, storage, mode: { type: 'free-trial', uses: 3 }, }) const resourceServer = new x402ResourceServer(facilitatorClient) .register(WORLD_CHAIN, evmScheme) .register(BASE, new ExactEvmScheme()) .registerExtension(agentkitResourceServerExtension) const routes = { 'GET /data': { // Accept payments on both World Chain and Base accepts: [ { scheme: 'exact', price: '$0.01', network: WORLD_CHAIN, payTo, }, { scheme: 'exact', price: '$0.01', network: BASE, payTo, }, ], extensions: declareAgentkitExtension({ statement: 'Verify your agent is backed by a real human', mode: { type: 'free-trial', uses: 3 }, }), }, } const httpServer = new x402HTTPResourceServer(resourceServer, routes) .onProtectedRequest(hooks.requestHook) const app = new Hono() app.use(paymentMiddlewareFromHTTPServer(httpServer)) app.get('/data', c => { return c.json({ message: 'Protected content' }) }) serve({ fetch: app.fetch, port: 4021 }) ``` This example accepts payments on both World Chain and Base. AgentBook lookup automatically resolves against the canonical World Chain deployment. # Step 5: Configure the default mode and storage This guide uses `free-trial` mode so registered human-backed agents get 3 free requests before the normal x402 payment flow resumes. `InMemoryAgentKitStorage` is fine for local testing, but production should persist both usage counters and nonces. ```typescript theme={"system"} import type { AgentKitStorage } from '@worldcoin/agentkit' class DatabaseAgentKitStorage implements AgentKitStorage { async tryIncrementUsage(endpoint: string, humanId: string, limit: number) { return db.tryIncrementUsage(endpoint, humanId, limit) } async hasUsedNonce(nonce: string) { return db.hasUsedNonce(nonce) } async recordNonce(nonce: string) { await db.recordNonce(nonce) } } const hooks = createAgentkitHooks({ agentBook, storage: new DatabaseAgentKitStorage(), mode: { type: 'free-trial', uses: 3 }, }) ``` Need `discount` mode, custom AgentBook deployments, or the low-level validation helpers? Continue to the [SDK Reference](/agents/agent-kit/sdk-reference). # SDK Reference Source: https://docs.world.org/agents/agent-kit/sdk-reference Reference for AgentKit modes, APIs, EVM signatures, and low-level helpers. Use this page when you need the full AgentKit surface area. For the shortest path to a working setup, start with [Integrate AgentKit](/agents/agent-kit/integrate). ## Access modes Usage counters are tracked per human per endpoint. Two agents backed by the same human share the same counter. | Mode | Fields | Behavior | | ------------ | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | | `free` | `{ type: "free" }` | Registered human-backed agents always bypass payment. | | `free-trial` | `{ type: "free-trial"; uses?: number }` | Registered human-backed agents bypass payment the first `N` times. Default `uses` is `1`. | | `discount` | `{ type: "discount"; percent: number; uses?: number }` | Registered human-backed agents can underpay by the configured percentage for the first `N` times. | `discount` mode requires `verifyFailureHook` to be registered on the facilitator. Without it, discounted underpayments fail settlement verification. ## Agent client APIs ### `createAgentkitClient(options)` Use this client in agents that call paid x402 APIs. `agentkit.fetch` inspects `402 Payment Required` responses and retries once with a signed `agentkit` header when the response advertises `extensions.agentkit`. ```typescript theme={"system"} import { createAgentkitClient } from '@worldcoin/agentkit' const agentkit = createAgentkitClient({ signer: { address: agentWallet.address, chainId: 'eip155:8453', type: 'eip191', signMessage: message => agentWallet.signMessage(message), }, }) ``` | Option | Type | Description | | --------- | ------------------------------------- | ------------------------------------------------------------------- | | `signer` | `AgentkitSigner` | Agent wallet identity and SIWE signing function. Required. | | `fetch` | `typeof fetch` | Optional base fetch implementation. Defaults to `globalThis.fetch`. | | `onEvent` | `(event: AgentkitFetchEvent) => void` | Optional callback for logging and debugging. | The client does not create x402 payments. It returns the original response unchanged when AgentKit is unavailable or cannot be used, so your existing x402 payment client can handle fallback. ### `AgentkitSigner` ```ts theme={"system"} type AgentkitSigner = { address: string chainId: string type: 'eip191' | 'eip1271' signMessage(message: string): Promise*AI agent pauses for World ID approval before booking a flight*
Human-in-the-loop lets an AI agent **pause mid-execution** and wait for a real, verified human to approve an action before continuing. Every approval is cryptographically bound to the action via [World ID](/world-id/overview) — no bots, no spoofing, no replay. Built on the [Workflow SDK](https://useworkflow.dev) and the [Vercel AI SDK](https://ai-sdk.dev). ## Install ```bash theme={"system"} # Server — human-in-the-loop + peer dependencies npm install @worldcoin/human-in-the-loop ai workflow # Client — React bindings + peer dependencies npm install @worldcoin/human-in-the-loop-react @worldcoin/idkit ai react ``` ## Environment variables ```bash theme={"system"} # Server — used by @worldcoin/human-in-the-loop WORLD_RP_ID=your_rp_id WORLD_SIGNING_KEY=your_signing_key # Client — used by theApproved.
return ( <> {state.status === 'error' &&{state.error.message}
} {ready && (World Chain now streams full EIP-7928 block access lists in every flashblock, enabling parallel block verification, faster validation, and a path toward gigagas-per-second throughput without increasing validator hardware requirements.
Today we announce the open-sourcing of Remainder (Reasonable Machine Learning Doubly-Efficient Prover), Tools for Humanity's in-house GKR + Hyrax proof system. Remainder enables World's users to run ML models locally over private data and prove that they executed them correctly.
The new World ID 4.0 upgrade introduces account abstraction with multi-key support, transforming a World ID from a single secret into an abstract record in a public registry. This increases protocol resilience by allowing the introduction of multiple key support, portability, recovery and improved privacy.
Developer Portal Whitelist
The default space between a header and its associated content is 16px, and the same 16px spacing applies between elements within a section. This consistent use of spacing helps create a clean visual structure and predictable rhythm across the UI.
The default space between sections is set to 32px, helping to create clear separation between distinct areas of content. The space between the search bar and the content below is 24px, while the space between a sub-headline and its associated content is 16px.
The default space between header and a section that starts with a sub headline is set to 24px.
The default space between last item inside the scrollable area if a bottom bar is presented is 32px.
| Command | Description |
|---|---|
| Pay | Request a payment inside World App |
| Wallet Auth | Authenticate with Sign-In with Ethereum |
| Send Transaction | Submit one or more World Chain transactions |
| Sign Message | Sign an EIP-191 personal message |
| Sign Typed Data | Sign an EIP-712 typed payload |
| Share Contacts | Open the World App contact picker |
| Request Permission | Request notifications or microphone access |
| Get Permissions | Read current mini app permission state |
| Send Haptic Feedback | Trigger native haptic feedback |
| Share | Open the native share sheet |
| World Chat | Open World Chat with a prefilled message |
| Attestation | Request an app attestation token |
| Close Mini App | Programmatically close the mini app |
Correctly running template should look like this
app\_6c5c5717c77abe83be8814c032c3a6f9.
'/' for Swap.
| Parameter | World Chain | OP Mainnet | Ethereum |
|---|---|---|---|
| Block time in seconds | 2 | 2 | 12 |
| Block gas limit | 30,000,000 | 30,000,000 | 30,000,000 |
| Block gas target | 10,000,000 | 5,000,000 | 15,000,000 |
| EIP-1559 elasticity multiplier | 6 | 6 | 2 |
| EIP-1559 denominator | 250 | 250 | 8 |
| Maximum base fee increase (per block) | 0.8% | 2% | 12.5% |
| Maximum base fee decrease (per block) | 0.4% | 0.4% | 12.5% |
| Name | Address |
|---|---|
| L2ToL1MessagePasser | [`0x4200000000000000000000000000000000000016`](https://worldscan.org/address/0x4200000000000000000000000000000000000016) |
| L2CrossDomainMessenger | [`0x4200000000000000000000000000000000000007`](https://worldscan.org/address/0x4200000000000000000000000000000000000007) |
| L2StandardBridge | [`0x4200000000000000000000000000000000000010`](https://worldscan.org/address/0x4200000000000000000000000000000000000010) |
| L2ERC721Bridge | [`0x4200000000000000000000000000000000000014`](https://worldscan.org/address/0x4200000000000000000000000000000000000014) |
| SequencerFeeVault | [`0x4200000000000000000000000000000000000011`](https://worldscan.org/address/0x4200000000000000000000000000000000000011) |
| OptimismMintableERC20Factory | [`0x4200000000000000000000000000000000000012`](https://worldscan.org/address/0x4200000000000000000000000000000000000012) |
| OptimismMintableERC721Factory | [`0x4200000000000000000000000000000000000017`](https://worldscan.org/address/0x4200000000000000000000000000000000000017) |
| L1Block | [`0x4200000000000000000000000000000000000015`](https://worldscan.org/address/0x4200000000000000000000000000000000000015) |
| GasPriceOracle | [`0x420000000000000000000000000000000000000F`](https://worldscan.org/address/0x420000000000000000000000000000000000000F) |
| ProxyAdmin | [`0x4200000000000000000000000000000000000018`](https://worldscan.org/address/0x4200000000000000000000000000000000000018) |
| BaseFeeVault | [`0x4200000000000000000000000000000000000019`](https://worldscan.org/address/0x4200000000000000000000000000000000000019) |
| L1FeeVault | [`0x420000000000000000000000000000000000001A`](https://worldscan.org/address/0x420000000000000000000000000000000000001A) |
| GovernanceToken | [`0x4200000000000000000000000000000000000042`](https://worldscan.org/address/0x4200000000000000000000000000000000000042) |
| SchemaRegistry | [`0x4200000000000000000000000000000000000020`](https://worldscan.org/address/0x4200000000000000000000000000000000000020) |
| EAS | [`0x4200000000000000000000000000000000000021`](https://worldscan.org/address/0x4200000000000000000000000000000000000021) |
| Name | Address |
|---|---|
| AnchorStateRegistryProxy | [`0x90BB48fe3310499Db36437dCAE642F721e32d094`](https://etherscan.io/address/0x90BB48fe3310499Db36437dCAE642F721e32d094) |
| Batch Submitter | [`0xdBBE3D8c2d2b22A2611c5A94A9a12C2fCD49Eb29`](https://etherscan.io/address/0xdBBE3D8c2d2b22A2611c5A94A9a12C2fCD49Eb29) |
| Challenger | [`0xA4fB12D15Eb85dc9284a7df0AdBC8B696EdbbF1d`](https://etherscan.io/address/0xA4fB12D15Eb85dc9284a7df0AdBC8B696EdbbF1d) |
| DelayedWETHProxy | [`0x19f4eF9dDE39a50a2EF948B7d55AA19F60d99498`](https://etherscan.io/address/0x19f4eF9dDE39a50a2EF948B7d55AA19F60d99498) |
| DisputeGameFactoryProxy | [`0x069c4c579671f8c120b1327a73217D01Ea2EC5ea`](https://etherscan.io/address/0x069c4c579671f8c120b1327a73217D01Ea2EC5ea) |
| L1CrossDomainMessengerProxy | [`0xf931a81D18B1766d15695ffc7c1920a62b7e710a`](https://etherscan.io/address/0xf931a81D18B1766d15695ffc7c1920a62b7e710a) |
| L1ERC721BridgeProxy | [`0x1Df436AfDb2fBB40F1fE8bEd4Fc89A0D0990a8E9`](https://etherscan.io/address/0x1Df436AfDb2fBB40F1fE8bEd4Fc89A0D0990a8E9) |
| L1StandardBridgeProxy | [`0x470458C91978D2d929704489Ad730DC3E3001113`](https://etherscan.io/address/0x470458C91978D2d929704489Ad730DC3E3001113) |
| L2OutputOracleProxy (legacy) | [`0x19A6d1E9034596196295CF148509796978343c5D`](https://etherscan.io/address/0x19A6d1E9034596196295CF148509796978343c5D) |
| MIPS | [`0xaCc005DCd857B401e4732E6F7837135A22825cfA`](https://etherscan.io/address/0xaCc005DCd857B401e4732E6F7837135A22825cfA) |
| OptimismMintableERC20FactoryProxy | [`0x82Cb528466cF22412d89bdBE9bCF04856790dD0e`](https://etherscan.io/address/0x82Cb528466cF22412d89bdBE9bCF04856790dD0e) |
| OptimismPortalProxy | [`0xd5ec14a83B7d95BE1E2Ac12523e2dEE12Cbeea6C`](https://etherscan.io/address/0xd5ec14a83B7d95BE1E2Ac12523e2dEE12Cbeea6C) |
| PermissionedDisputeGame | [`0xe1dFFCBE4e22B813F26d2106D943C102e7cAb87e`](https://etherscan.io/address/0xe1dFFCBE4e22B813F26d2106D943C102e7cAb87e) |
| PreimageOracle | [`0x1E1d73536A081Ef2F355d29794547a9770Aeb1E0`](https://etherscan.io/address/0x1E1d73536A081Ef2F355d29794547a9770Aeb1E0) |
| ProtocolVersionsProxy | [`0x8eeF04eef96fef1050702453f75F0Fc4f8F70973`](https://etherscan.io/address/0x8eeF04eef96fef1050702453f75F0Fc4f8F70973) |
| ProxyAdmin | [`0xd7405BE7f3e63b094Af6C7C23D5eE33Fd82F872D`](https://etherscan.io/address/0xd7405BE7f3e63b094Af6C7C23D5eE33Fd82F872D) |
| SafeProxyFactory | [`0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2`](https://etherscan.io/address/0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2) |
| SuperchainConfigProxy | [`0x95703e0982140D16f8ebA6d158FccEde42f04a4C`](https://etherscan.io/address/0x95703e0982140D16f8ebA6d158FccEde42f04a4C) |
| SystemConfigProxy | [`0x6ab0777fD0e609CE58F939a7F70Fe41F5Aa6300A`](https://etherscan.io/address/0x6ab0777fD0e609CE58F939a7F70Fe41F5Aa6300A) |
| OpUSDCBridgeAdapter | [`0x153A69e4bb6fEDBbAaF463CB982416316c84B2dB`](https://etherscan.io/address/0x153A69e4bb6fEDBbAaF463CB982416316c84B2dB) |
| Name | Address |
|---|---|
| L2ToL1MessagePasser | [`0x4200000000000000000000000000000000000016`](https://worldchain-sepolia.explorer.alchemy.com/address/0x4200000000000000000000000000000000000016) |
| L2CrossDomainMessenger | [`0x4200000000000000000000000000000000000007`](https://worldchain-sepolia.explorer.alchemy.com/address/0x4200000000000000000000000000000000000007) |
| L2StandardBridge | [`0x4200000000000000000000000000000000000010`](https://worldchain-sepolia.explorer.alchemy.com/address/0x4200000000000000000000000000000000000010) |
| L2ERC721Bridge | [`0x4200000000000000000000000000000000000014`](https://worldchain-sepolia.explorer.alchemy.com/address/0x4200000000000000000000000000000000000014) |
| SequencerFeeVault | [`0x4200000000000000000000000000000000000011`](https://worldchain-sepolia.explorer.alchemy.com/address/0x4200000000000000000000000000000000000011) |
| OptimismMintableERC20Factory | [`0x4200000000000000000000000000000000000012`](https://worldchain-sepolia.explorer.alchemy.com/address/0x4200000000000000000000000000000000000012) |
| OptimismMintableERC721Factory | [`0x4200000000000000000000000000000000000017`](https://worldchain-sepolia.explorer.alchemy.com/address/0x4200000000000000000000000000000000000017) |
| L1Block | [`0x4200000000000000000000000000000000000015`](https://worldchain-sepolia.explorer.alchemy.com/address/0x4200000000000000000000000000000000000015) |
| GasPriceOracle | [`0x420000000000000000000000000000000000000F`](https://worldchain-sepolia.explorer.alchemy.com/address/0x420000000000000000000000000000000000000F) |
| ProxyAdmin | [`0x4200000000000000000000000000000000000018`](https://worldchain-sepolia.explorer.alchemy.com/address/0x4200000000000000000000000000000000000018) |
| BaseFeeVault | [`0x4200000000000000000000000000000000000019`](https://worldchain-sepolia.explorer.alchemy.com/address/0x4200000000000000000000000000000000000019) |
| L1FeeVault | [`0x420000000000000000000000000000000000001A`](https://worldchain-sepolia.explorer.alchemy.com/address/0x420000000000000000000000000000000000001A) |
| GovernanceToken | [`0x4200000000000000000000000000000000000042`](https://worldchain-sepolia.explorer.alchemy.com/address/0x4200000000000000000000000000000000000042) |
| SchemaRegistry | [`0x4200000000000000000000000000000000000020`](https://worldchain-sepolia.explorer.alchemy.com/address/0x4200000000000000000000000000000000000020) |
| EAS | [`0x4200000000000000000000000000000000000021`](https://worldchain-sepolia.explorer.alchemy.com/address/0x4200000000000000000000000000000000000021) |
| Name | Address |
|---|---|
| AnchorStateRegistryProxy | [`0xcC59030b952CE2c888f7Fc15f99E34c72cC6ca21`](https://sepolia.etherscan.io/address/0xcC59030b952CE2c888f7Fc15f99E34c72cC6ca21) |
| Batch Submitter | [`0x0f3ff4731D7a10B89ED79AD1Fd97844d7F66B96d`](https://sepolia.etherscan.io/address/0x0f3ff4731D7a10B89ED79AD1Fd97844d7F66B96d) |
| Challenger | [`0x945185C01fb641bA3E63a9bdF66575e35a407837`](https://sepolia.etherscan.io/address/0x945185C01fb641bA3E63a9bdF66575e35a407837) |
| DelayedWETHProxy | [`0x5706FB7D51a5c75cafaED0506d5a8e8ade1D83f7`](https://sepolia.etherscan.io/address/0x5706FB7D51a5c75cafaED0506d5a8e8ade1D83f7) |
| DisputeGameFactoryProxy | [`0x8Ec1111f67Dad6b6A93B3F42DfBC92D81c98449A`](https://sepolia.etherscan.io/address/0x8Ec1111f67Dad6b6A93B3F42DfBC92D81c98449A) |
| L1CrossDomainMessengerProxy | [`0x7768c821200554d8F359A8902905Ba9eDe5659a9`](https://sepolia.etherscan.io/address/0x7768c821200554d8F359A8902905Ba9eDe5659a9) |
| L1ERC721BridgeProxy | [`0x3580505c56f8560E3777E92Fb27f70fD20c5B493`](https://sepolia.etherscan.io/address/0x3580505c56f8560E3777E92Fb27f70fD20c5B493) |
| L1StandardBridgeProxy | [`0xd7DF54b3989855eb66497301a4aAEc33Dbb3F8DE`](https://sepolia.etherscan.io/address/0xd7DF54b3989855eb66497301a4aAEc33Dbb3F8DE) |
| L2OutputOracleProxy (legacy) | [`0xc8886f8BAb6Eaeb215aDB5f1c686BF699248300e`](https://sepolia.etherscan.io/address/0xc8886f8BAb6Eaeb215aDB5f1c686BF699248300e) |
| MIPS | [`0xaCc005DCd857B401e4732E6F7837135A22825cfA`](https://sepolia.etherscan.io/address/0xaCc005DCd857B401e4732E6F7837135A22825cfA) |
| OptimismMintableERC20FactoryProxy | [`0x2D272eF54Ee8EF5c2Ff3523559186580b158cd57`](https://sepolia.etherscan.io/address/0x2D272eF54Ee8EF5c2Ff3523559186580b158cd57) |
| OptimismPortalProxy | [`0xFf6EBa109271fe6d4237EeeD4bAb1dD9A77dD1A4`](https://sepolia.etherscan.io/address/0xFf6EBa109271fe6d4237EeeD4bAb1dD9A77dD1A4) |
| PermissionedDisputeGame | [`0xe1dFFCBE4e22B813F26d2106D943C102e7cAb87e`](https://sepolia.etherscan.io/address/0xe1dFFCBE4e22B813F26d2106D943C102e7cAb87e) |
| PreimageOracle | [`0x1E1d73536A081Ef2F355d29794547a9770Aeb1E0`](https://sepolia.etherscan.io/address/0x1E1d73536A081Ef2F355d29794547a9770Aeb1E0) |
| ProtocolVersionsProxy | [`0x01DBC9aBe8e59f021d47Cf79143DE830820CbA29`](https://sepolia.etherscan.io/address/0x01DBC9aBe8e59f021d47Cf79143DE830820CbA29) |
| ProxyAdmin | [`0x3a987FE1cb587B0A1808cf9bB7Cbe0E341838319`](https://sepolia.etherscan.io/address/0x3a987FE1cb587B0A1808cf9bB7Cbe0E341838319) |
| SafeProxyFactory | [`0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2`](https://sepolia.etherscan.io/address/0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2) |
| SuperchainConfigProxy | [`0xC2Be75506d5724086DEB7245bd260Cc9753911Be`](https://sepolia.etherscan.io/address/0xC2Be75506d5724086DEB7245bd260Cc9753911Be) |
| SystemConfigProxy | [`0x166F9406e79A656f12F05247fb8F5DfA6155bCBF`](https://sepolia.etherscan.io/address/0x166F9406e79A656f12F05247fb8F5DfA6155bCBF) |
| Property | Value |
|---|---|
| Framework | OP Stack |
| Chain ID |
ID480 (0x1e0)
|
| EIP-3770 Short Name |
wc
|
| Settlement layer | Ethereum |
| Data availability | Ethereum |
| Gas Limit | 80M |
| Gas Target | 40M |
| Block Time | 2s |
| Important Links | URLs |
|---|---|
| Bridge | worldchain-mainnet.bridge.alchemy.com |
| Block Explorer | worldscan.org |
| Status Page | worldchain-mainnet-status.alchemy.com |
| RPC | worldchain-mainnet.g.alchemy.com/public |
| Developer Telegram | @worlddevelopersupport |
| Hardfork | Timestamp |
|---|---|
| Fjord | 1721826000 (Wed 24 Jul 2024 13:00:00 UTC) |
| Granite | 1727780400 (Tue 1 Oct 2024 11:00:00 UTC) |
| Holocene | 1738238400 (Thu 30 Jan 2025 12:00:00 UTC) |
| Property | Value |
|---|---|
| Framework | OP Stack |
| Chain ID |
ID4801 (0x12C1)
|
| EIP-3770 Short Name |
wcsep
|
| Settlement layer | Ethereum Sepolia |
| Data availability | Ethereum Sepolia |
| Gas Limit | 30M |
| Gas Target | 15M |
| Block Time | 2s |
| Important Links | URLs |
|---|---|
| Bridge | worldchain-sepolia.bridge.alchemy.com |
| Block Explorer | worldchain-sepolia.explorer.alchemy.com |
| Status Page | worldchain-sepolia-status.alchemy.com |
| Faucet | alchemy.com/faucets/world-chain-sepolia |
| RPC | worldchain-sepolia.g.alchemy.com/public |
| Hardfork | Timestamp |
|---|---|
| Fjord | 1721739600 (Tue 23 Jul 2024 13:00:00 UTC) |
| Granite | 1726570800 (Tue 17 Sep 2024 11:00:00 UTC) |
| Holocene | 1737633600 (Thu 23 Jan 2025 12:00:00 UTC) |
| Pectra Blob Fix | 1742486400 (Thu 20 Mar 2025 16:00:00 UTC) |
| Contract | World Chain Mainnet Address |
|---|---|
| WLD | [`0x2cfc85d8e48f8eab294be644d9e25c3030863003`](https://worldscan.org/address/0x2cFc85d8E48F8EAB294be644d9E25C3030863003) |
| WBTC | [`0x03c7054bcb39f7b2e5b2c7acb37583e32d70cfa3`](https://worldscan.org/address/0x03c7054bcb39f7b2e5b2c7acb37583e32d70cfa3) |
| SDAI | [`0x859dbe24b90c9f2f7742083d3cf59ca41f55be5d`](https://worldscan.org/address/0x859dbe24b90c9f2f7742083d3cf59ca41f55be5d) |
| WETH | [`0x4200000000000000000000000000000000000006`](https://worldscan.org/address/0x4200000000000000000000000000000000000006) |
| USDC | [`0x79A02482A880bCE3F13e09Da970dC34db4CD24d1`](https://worldscan.org/address/0x79A02482A880bCE3F13e09Da970dC34db4CD24d1) |
| L1 OpUSDCBridgeAdapter (Ethereum mainnet) | [`0x153A69e4bb6fEDBbAaF463CB982416316c84B2dB`](https://etherscan.io/address/0x153A69e4bb6fEDBbAaF463CB982416316c84B2dB) |
| Contract | World Chain Mainnet Address |
|---|---|
| WorldIDAddressBook | [`0x57b930D551e677CC36e2fA036Ae2fe8FdaE0330D`](https://worldscan.org/address/0x57b930D551e677CC36e2fA036Ae2fe8FdaE0330D) |
| WorldIDRouter | [`0x17B354dD2595411ff79041f930e491A4Df39A278`](https://worldscan.org/address/0x17B354dD2595411ff79041f930e491A4Df39A278) |
| Contract | World Chain Mainnet Address |
|---|---|
| HelperBatch Contract | [`0x8d98006269238CAEd033b2d94661B29312AD09b7`](https://worldscan.org/address/0x8d98006269238CAEd033b2d94661B29312AD09b7) |
| SafeL2Singleton | [`0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552`](https://worldscan.org/address/0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552) |
| SafeProxyFactory | [`0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2`](https://worldscan.org/address/0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2) |
| Contract | World Chain Mainnet Address |
|---|---|
| HelperBatch Contract | [`0x866087c23a7eE1fD5498ef84D59aF742f3d4b322`](https://worldscan.org/address/0x866087c23a7eE1fD5498ef84D59aF742f3d4b322) |
| SafeL2Singleton | [`0x29fcB43b46531BcA003ddC8FCB67FFE91900C762`](https://worldscan.org/address/0x29fcB43b46531BcA003ddC8FCB67FFE91900C762) |
| SafeProxyFactory | [`0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67`](https://worldscan.org/address/0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67) |
| Contract | World Chain Mainnet Address |
|---|---|
| TimeBasedAllowance Module | [`0xa9bcF56d9FCc0178414EF27a3d893C9469e437B7`](https://worldscan.org/address/0xa9bcF56d9FCc0178414EF27a3d893C9469e437B7) |
| 4337 Module | [`0x75cf11467937ce3F2f357CE24ffc3DBF8fD5c226`](https://worldscan.org/address/0x75cf11467937ce3F2f357CE24ffc3DBF8fD5c226) |
| AddModules Helper | [`0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67`](https://worldscan.org/address/0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67) |
| Contract | World Chain Mainnet Address |
|---|---|
| ApprovalSwap | [`0xf4305dd6256dc2b0d07caaf2953688defbc86e9d`](https://worldscan.org/address/0xf4305dd6256dc2b0d07caaf2953688defbc86e9d) |
| v3CoreFactoryAddress | [`0x7a5028BDa40e7B173C278C5342087826455ea25a`](https://worldscan.org/address/0x7a5028BDa40e7B173C278C5342087826455ea25a) |
| AddModules Helper | [`0x091AD9e2e6e5eD44c1c66dB50e49A601F9f36cF6`](https://worldscan.org/address/0x091AD9e2e6e5eD44c1c66dB50e49A601F9f36cF6) |
| Multicall2Address | [`0x0a22c04215c97E3F532F4eF30e0aD9458792dAB9`](https://worldscan.org/address/0x0a22c04215c97E3F532F4eF30e0aD9458792dAB9) |
| ProxyAdminAddress | [`0x8B52DaCB7B5d9A959CDcD5419061c0eDD1296c29`](https://worldscan.org/address/0x8B52DaCB7B5d9A959CDcD5419061c0eDD1296c29) |
| TickLensAddress | [`0xE61df0CaC9d85876aCE5E3037005D80943570623`](https://worldscan.org/address/0xE61df0CaC9d85876aCE5E3037005D80943570623) |
| NftDescriptorLibraryAddressV1\_3\_0 | [`0x38c68A1D60C47973EcE5bc1725B65D8Bec438192`](https://worldscan.org/address/0x38c68A1D60C47973EcE5bc1725B65D8Bec438192) |
| NonfungibleTokenPositionDescriptorAddressV1\_3\_0 | [`0x70410a302c4a5c52C659b780941c947Abd437FeB`](https://worldscan.org/address/0x70410a302c4a5c52C659b780941c947Abd437FeB) |
| DescriptorProxyAddress | [`0xe6FcB4952b2d3Fab6DA4BC165831f5575e093feC`](https://worldscan.org/address/0xe6FcB4952b2d3Fab6DA4BC165831f5575e093feC) |
| NonfungibleTokenPositionManagerAddress | [`0xec12a9F9a09f50550686363766Cc153D03c27b5e`](https://worldscan.org/address/0xec12a9F9a09f50550686363766Cc153D03c27b5e) |
| V3MigratorAddress | [`0x9EBDdCBa71C9027E1eB45135672a30bcFEec9de3`](https://worldscan.org/address/0x9EBDdCBa71C9027E1eB45135672a30bcFEec9de3) |
| V3StakerAddress | [`0x053956ab1618EcaCc135Ebc18Fd7564979aD4295`](https://worldscan.org/address/0x053956ab1618EcaCc135Ebc18Fd7564979aD4295) |
| QuoterV2Address | [`0x10158D43e6cc414deE1Bd1eB0EfC6a5cBCfF244c`](https://worldscan.org/address/0x10158D43e6cc414deE1Bd1eB0EfC6a5cBCfF244c) |
| Contract | World Chain Mainnet Address |
|---|---|
| WLDGrant | [`0x7DD5B6B5A574EFd452AC0cfE3e1B69384a03C6f9`](https://worldscan.org/address/0x7DD5B6B5A574EFd452AC0cfE3e1B69384a03C6f9?tab=contract) |
| RecurringGrantDrop | [`0x2c1Ca1FBbD5f28e5492Cc6bF8C4e8c57354eb162`](https://worldscan.org/address/0x2c1Ca1FBbD5f28e5492Cc6bF8C4e8c57354eb162?tab=contract) |
| WLDGrantReservations | [`0x3a00fe3254c94c4689cb5163c91ee501d942e710`](https://worldscan.org/address/0x3a00fE3254c94C4689CB5163c91Ee501D942E710?tab=contract) |
| RecurringGrantDropReservations | [`0xc2D270651cEF0AA3734c9A7fEaCd3b3B39e36e18`](https://worldscan.org/address/0xc2D270651cEF0AA3734c9A7fEaCd3b3B39e36e18?tab=contract) |
| Grants4FirstBatch | [`0xae3f204c75e46c27f66c843bc9f3bbd04a6374c5`](https://worldscan.org/address/0xaE3f204c75E46C27f66C843bC9F3Bbd04a6374c5?tab=contract) |
| WLDVault | [`0x14a028cC500108307947dca4a1Aa35029FB66CE0`](https://worldscan.org/address/0x14a028cC500108307947dca4a1Aa35029FB66CE0?tab=contract) |
| Contract | World Chain Mainnet Address |
|---|---|
| MiniKitTransfer | [`0x9CC547e0Ca60dC249Eea2d91Ba12F00C4ca12787`](https://worldscan.org/address/0x9CC547e0Ca60dC249Eea2d91Ba12F00C4ca12787?tab=contract) |
| Contract | World Chain Mainnet Address |
|---|---|
| Entrypoint v0.7 | [`0x0000000071727De22E5E9d8BAf0edAc6f37da032`](https://worldscan.org/address/0x0000000071727De22E5E9d8BAf0edAc6f37da032#code) |
| Contract | World Chain Mainnet Address |
|---|---|
| WLD/USD | [`0x8Bb2943AB030E3eE05a58d9832525B4f60A97FA0`](https://worldscan.org/address/0x8Bb2943AB030E3eE05a58d9832525B4f60A97FA0) |
| ETH/USD | [`0xe1d72a719171DceAB9499757EB9d5AEb9e8D64A6`](https://worldscan.org/address/0xe1d72a719171DceAB9499757EB9d5AEb9e8D64A6) |
| BTC/USD | [`0xdD91675235C37a47597c053807d61Da27Ae1AE6C`](https://worldscan.org/address/0xdD91675235C37a47597c053807d61Da27Ae1AE6C) |
| USDC/USD | [`0xF4301686AfF4eE36d70c718a9e62309b53862BE8`](https://worldscan.org/address/0xF4301686AfF4eE36d70c718a9e62309b53862BE8) |
| ezETH/USD | [`0x78ce3664d1582e14270092ea9817f111ac53524b`](https://worldscan.org/address/0x78ce3664d1582e14270092ea9817f111ac53524b#code) |
| Attribute | Description |
|---|---|
genesis\_issued\_at
|
The Unix timestamp of the user's first PoH credential. Remains constant across credential renewals. |
expires\_at
|
Unix timestamp when the credential expires. This is currently three years after the most recent issuance. |
associated\_data\_hash
|
The PoH credential has no associated data, so this field is always
FieldElement::ZERO.
|
| Attribute | Description |
|---|---|
genesis\_issued\_at
|
The timestamp of when the unique document was first verified. |
expires\_at
|
The expiration of the document with a maximum of 10 years. |
associated\_data\_commitment
|
A commitment to the user's Associated Data (see NFC Issuer implementation notes ). |
| Preset | What it requests |
|---|---|
orbLegacy |
Orb verification. |
secureDocumentLegacy |
At least a Secure Document verification. Returns the user's highest legacy credential: Secure Document or Orb. |
documentLegacy |
At least a Document verification. Returns the user's highest legacy credential: Document, Secure Document, or Orb. |
deviceLegacy |
Deprecated. Keep deviceLegacy only for existing Device integrations. For new integrations, use Selfie Check (Beta). |
| Parameter | Where it is used | Description |
|---|---|---|
signal |
Presets | Binds app context into the proof, such as a user ID or wallet address. Your backend should enforce the same value. |
allow\_legacy\_proofs |
Request config and widgets | Required for request flows. Set to true while accepting World ID 3.0 fallback proofs; set to false for World ID 4.0-only requests. |
require\_user\_presence |
Request config and widgets | Optional liveness step. Defaults to false. |
Design components and templates for World ID integration
| Element | Recommendation |
|---|---|
| QR Code | Minimum 160 x 160px |
| Padding | Min. 24px all sides |
| Logo size | 32-48px width |
| Font (title) | Minimum 16px |
| Font (description) | Minimum 14px |
Download the human badge asset.
| Content | Placement | Format |
|---|---|---|
| Social profiles | Below or beside username / bio | Icon + label |
| Gaming profiles | Under character name or stats | Icon + label |
| Leaderboards | Right of username | Icon + label |
| Messaging/chat | Right of username | Icon + label |
| Account dashboards | Near user ID, next to login method, etc. | Icon + label |
| Code | Meaning | Typical action |
|---|---|---|
user\_rejected |
User cancelled in World ID. | Treat as user cancellation, allow retry. |
verification\_rejected |
Legacy rejection code (older bridge/app behavior). | Handle same as user\_rejected. |
credential\_unavailable |
Requested credential type is not available for that user. | Offer fallback credential policy or explain requirement. |
world\_id\_4\_not\_available |
World ID 4.0 credential is not available for that user. | Use a compatible fallback request or explain the World ID 4.0 requirement. |
world\_id\_3\_not\_available |
World ID 3.0 credential is not available for that user. | Use a compatible fallback request or explain the World ID 3.0 requirement. |
malformed\_request |
Payload or configuration is invalid. | Check app\_id, rp\_context, and request shape. |
invalid\_network |
Environment mismatch between app config and World App context. | Align staging/production settings. |
inclusion\_proof\_pending |
Credential inclusion data is not ready yet. | Retry later. |
inclusion\_proof\_failed |
Inclusion proof retrieval failed. | Retry; if repeated, treat as operational incident. |
unexpected\_response |
Malformed or unsupported bridge/app response. | Log diagnostics and retry once. |
connection\_failed |
Could not establish/maintain bridge communication. | Check connectivity and bridge reachability. |
max\_verifications\_reached |
Action already verified the maximum allowed number of times. | Treat as terminal business-rule outcome. |
failed\_by\_host\_app |
World App returned a proof, but the host app's handleVerify callback threw or rejected it. |
Inspect the callback and backend verification response. Do not use this code alone to decide whether the issue is in the integration or credential availability. |
invalid\_rp\_signature |
RP signature could not be verified. | Check the RP signing key, nonce, timestamps, action, and signed message. |
nullifier\_replayed |
Nullifier was already used for this action. | Treat as an already-verified outcome; do not retry the same action as a new verification. |
duplicate\_nonce |
RP reused a signature nonce. | Generate a fresh nonce and signed RP context for each request. |
unknown\_rp |
RP is not known to the registry. | Check the registered RP ID and app configuration. |
inactive\_rp |
RP is registered but inactive. | Reactivate or reconfigure the RP before retrying. |
timestamp\_too\_old |
RP request timestamp is too old. | Generate a new signed RP context with a current timestamp. |
timestamp\_too\_far\_in\_future |
RP request timestamp is too far in the future. | Fix server clock skew and generate a new signed RP context. |
invalid\_timestamp |
RP request timestamp is invalid. | Check timestamp format and regenerate the signed RP context. |
rp\_signature\_expired |
RP signature has expired. | Request a fresh RP signature before starting verification. |
user\_presence\_failed |
Required user-presence check was not completed. | Let the user retry the request. |
identity\_attributes\_not\_matched |
User identity attributes did not match the requested constraints. | Show an eligibility fallback or adjust the requested attribute constraints. |
generic\_error |
Catch-all unknown failure. | Log details and retry with backoff. |
invalid\_rp\_id\_format |
RP ID is malformed. | Use the registered rp\_... ID from your app configuration. |
timeout |
Client-side polling timeout. | Extend timeout or let the user retry. |
cancelled |
Client-side cancellation (abort/task cancel/user close). | Treat as neutral cancellation path. |
&a=
const expiresAt = request.expiresAt; // Unix seconds
const completion = await request.pollUntilCompletion();
```
`IDKitInviteCodeRequest` exposes:
* `connectorURI`
* `expiresAt`
* `requestId`
* `pollOnce()`
* `pollUntilCompletion({ pollInterval, timeout })`
* `getDebugReport()`
### Migrating from QR / connect-URL
```ts theme={"system"}
// Before — QR / connect-URL flow
const request = await IDKit.request({
app_id: "app_xxxxx",
action: "my-action",
rp_context,
allow_legacy_proofs: true,
}).preset(orbLegacy({ signal: "user-123" }));
const connectorURI = request.connectorURI; // render as QR
const completion = await request.pollUntilCompletion();
```
```ts theme={"system"}
// After — invite-code mode
const request = await IDKit.requestWithInviteCode({
app_id: "app_xxxxx",
action: "my-action",
rp_context,
allow_legacy_proofs: true,
}).preset(selfieCheckLegacy({ signal: "user-123" }));
const connectorURI = request.connectorURI; // display to user (URL with code embedded)
const expiresAt = request.expiresAt; // drive a countdown
const completion = await request.pollUntilCompletion();
```
The config object is unchanged. The `connectorURI` now includes `&c=&a=` params; use it alongside `expiresAt` in your UI. Polling, proof verification, and nullifier storage stay the same.
## Server-side helpers
Use subpath exports on your backend:
```ts theme={"system"}
import { signRequest } from "@worldcoin/idkit-core/signing";
import { hashSignal } from "@worldcoin/idkit-core/hashing";
const { sig, nonce, createdAt, expiresAt } = signRequest({
signingKeyHex: process.env.RP_SIGNING_KEY!,
action: "my-action",
ttl: 300, // optional, default 300s
});
const signalHash = hashSignal("user-123");
```
`signRequest` should only run in trusted server environments. See [RP Signatures](/world-id/idkit/signatures) for the full algorithm and test vectors.
## Related pages
* [Getting started](/world-id/idkit/integrate)
* [RP Signatures](/world-id/idkit/signatures)
* [React](/world-id/idkit/react)
* [Error Codes](/world-id/idkit/error-codes)
* [POST /v4/verify reference](/api-reference/developer-portal/verify)
# Kotlin
Source: https://docs.world.org/world-id/idkit/kotlin
Reference for the IDKit Kotlin API: request builders, presets, and polling.
The Kotlin SDK is currently under active development. Expect minor API evolution while the SDK hardens.
## Install
The package is published as `com.worldcoin:idkit` to GitHub Packages.
```kotlin theme={"system"}
dependencyResolutionManagement {
repositories {
mavenCentral()
maven {
url = uri("https://maven.pkg.github.com/worldcoin/idkit")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN") // requires read:packages
}
}
}
}
dependencies {
implementation("com.worldcoin:idkit:")
}
```
## Request flow
```kotlin theme={"system"}
import com.worldcoin.idkit.IDKit
import com.worldcoin.idkit.IDKitRequestConfig
import com.worldcoin.idkit.IDKitPollOptions
import com.worldcoin.idkit.IDKitCompletionResult
import com.worldcoin.idkit.orbLegacy
import uniffi.idkit_core.Environment
import uniffi.idkit_core.RpContext
val rpContext = RpContext(
rpId = "rp_xxxxx",
nonce = backend.nonce,
createdAt = backend.createdAt.toULong(),
expiresAt = backend.expiresAt.toULong(),
signature = backend.sig,
)
val config = IDKitRequestConfig(
appId = "app_xxxxx",
action = "my-action",
rpContext = rpContext,
allowLegacyProofs = true,
returnTo = "myapp://verify-done", // Optional: mobile deep-link callback URL
environment = Environment.PRODUCTION,
)
val request = IDKit.request(config).preset(orbLegacy(signal = "user-123"))
val connectorURI = request.connectorURI
val completion = request.pollUntilCompletion(
options = IDKitPollOptions(pollIntervalMs = 2_000u, timeoutMs = 120_000u)
)
if (completion is IDKitCompletionResult.Success) {
val result = completion.result
}
```
## Presets
```kotlin theme={"system"}
import com.worldcoin.idkit.IDKit
import com.worldcoin.idkit.orbLegacy
val request = IDKit.request(config).preset(orbLegacy(signal = "user-123"))
```
## Polling patterns
* `pollStatusOnce()` for manual loops
* `pollUntilCompletion(options)` for blocking until terminal state
* `statusFlow(pollInterval)` extension for coroutine-driven updates
```kotlin theme={"system"}
import com.worldcoin.idkit.IDKitStatus
import com.worldcoin.idkit.statusFlow
import kotlinx.coroutines.flow.collectLatest
import kotlin.time.Duration.Companion.seconds
request.statusFlow(2.seconds).collectLatest { status ->
when (status) {
IDKitStatus.WaitingForConnection -> Unit
IDKitStatus.AwaitingConfirmation -> Unit
is IDKitStatus.Confirmed -> println(status.result)
is IDKitStatus.Failed -> println(status.error)
}
}
```
# Mini Apps
Source: https://docs.world.org/world-id/idkit/mini-apps
IDKit is the verification layer for Mini Apps. MiniKit handles native Mini App commands such as wallet auth, payments, transactions, sharing, and permissions. World ID verification, including Selfie Check, is implemented with `@worldcoin/idkit`.
Inside World App, IDKit uses the native World App transport — no QR code is shown. The same widget also works outside World App, where it falls back to the normal connect URL and QR experience. Your backend responsibilities do not change: sign each request server-side, verify the proof server-side, and store nullifiers for replay protection.
If you are migrating from MiniKit 1.x, replace `MiniKit.verify(...)` or
`MiniKit.commandsAsync.verify(...)` with IDKit. MiniKit 2.x does not proxy
verification requests.
## How IDKit fits a Mini App
The integration is the standard IDKit flow — follow the [Integrate IDKit](/world-id/idkit/integrate) guide for installation, the server-side RP signature endpoint, backend proof verification, and nullifier storage. Nothing in those steps changes for Mini Apps.
The Mini-App-specific details:
* **Separate app IDs.** If your Mini App and World ID integration use separate Developer Portal apps, MiniKit initialization uses the Mini App `app_id`, while IDKit uses the World ID `app_id`, `rp_id`, action, and signing key.
* **No transport configuration needed.** IDKit detects World App and uses the native transport automatically.
* **Standalone websites work too.** If your Mini App also runs as a regular website, the same widget handles browser users. For iOS install-continuation flows outside World App, see [invite-code mode](/world-id/idkit/verification-flows#with-invite-code-mode).
## Choose a verification level
| Goal | IDKit preset |
| ------------------------------------------------------ | ------------------- |
| Strong sybil resistance or one-human-one-action checks | `proofOfHuman` |
| Lower-friction liveness or bot deterrence | `selfieCheckLegacy` |
| Passport-backed checks | `passport` |
Check out this [page](/world-id/idkit/credentials) to learn about the different World ID credentials and which preset to use for each.
## Example
This is the same widget from [Step 4 of the integration guide](/world-id/idkit/integrate#step-4-generate-the-connect-url-and-collect-proof), using the `proofOfHuman` preset:
```tsx title="components/WorldIdGate.tsx" theme={"system"}
{
// Unlock the protected Mini App experience here.
}}
onError={(errorCode, debugReport) => {
// Inside World App, debugReport?.transport is "mini_app".
console.error("IDKit error", errorCode, debugReport);
}}
/>
```
Use a stable `signal` (user ID, wallet address, claim ID) to bind the proof to app-specific context, and [store the nullifier](/world-id/idkit/integrate#step-6-store-the-nullifier) in your backend to enforce uniqueness.
## Related pages
* [Integrate IDKit](/world-id/idkit/integrate)
* [Configure Credentials](/world-id/idkit/credentials)
* [React reference](/world-id/idkit/react)
* [MiniKit commands](/mini-apps/quick-start/commands)
# On-chain Verification
Source: https://docs.world.org/world-id/idkit/onchain-verification
Verify World ID proofs directly in Solidity for web3-native flows.
On-chain verification is best for web3 apps that need proof checks enforced by
smart contracts (for example gating mints, voting, or claims without backend
trust assumptions). If you do not need contract-level enforcement, use
[POST /v4/verify](/api-reference/developer-portal/verify) instead.
## 1. Verifying Legacy proofs (World ID 3.0)
If you are integrating World ID on-chain, we strongly recommend deploying your contract behind an upgradable proxy (e.g. UUPS or Transparent Proxy). This allows you to upgrade your verification logic as new World ID versions are released. See the [World ID 4.0 Migration guide](/world-id/4-0-migration) for details.
For v3 proofs, verify against `WorldIDRouter.verifyProof(...)`. Use the Router address for the chain you're deploying to:
| Chain | `WorldIDRouter` Mainnet | `WorldIDRouter` Testnet |
| ----------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| World Chain | [0x17B354dD...A278](https://worldscan.org/address/0x17B354dD2595411ff79041f930e491A4Df39A278) | [0x57f92815...f611](https://sepolia.worldscan.org/address/0x57f928158C3EE7CDad1e4D8642503c4D0201f611) |
| Ethereum | [id.worldcoin.eth](https://etherscan.io/address/0x163b09b4fe21177c455d850bd815b6d583732432#code) | [0x469449f2...2157](https://sepolia.etherscan.io/address/0x469449f251692e0779667583026b5a1e99512157#code) |
| Base | [0xBCC7e591...4163](https://basescan.org/address/0xBCC7e5910178AFFEEeBA573ba6903E9869594163#code) | [0x42FF98C4...C02](https://sepolia.basescan.org/address/0x42FF98C4E85212a5D31358ACbFe76a621b50fC02#code) |
| Optimism | [optimism.id.worldcoin.eth](https://optimistic.etherscan.io/address/0x57f928158C3EE7CDad1e4D8642503c4D0201f611#code) | [0x11cA3127...4334](https://sepolia-optimism.etherscan.io/address/0x11cA3127182f7583EfC416a8771BD4d11Fae4334#code) |
| Polygon | [polygon.id.worldcoin.eth](https://polygonscan.com/address/0x515f06B36E6D3b707eAecBdeD18d8B384944c87f#code) | — |
```solidity theme={"system"}
interface IWorldID {
function verifyProof(
uint256 root,
uint256 groupId,
uint256 signalHash,
uint256 nullifierHash,
uint256 externalNullifierHash,
uint256[8] calldata proof
) external view;
}
contract VerifyLegacyV3 {
IWorldID public immutable worldIdRouter;
uint256 public constant GROUP_ID = 1; // Orb
mapping(uint256 => bool) public nullifierHashes;
error InvalidNullifier();
constructor(IWorldID _worldIdRouter) {
worldIdRouter = _worldIdRouter;
}
function verifyLegacyAndExecute(
uint256 root,
uint256 signalHash,
uint256 nullifierHash,
uint256 externalNullifierHash,
uint256[8] calldata proof
) external {
if (nullifierHashes[nullifierHash]) revert InvalidNullifier();
worldIdRouter.verifyProof(
root,
GROUP_ID,
signalHash,
nullifierHash,
externalNullifierHash,
proof
);
nullifierHashes[nullifierHash] = true;
// Execute protected business logic here.
}
}
```
For legacy proofs, ensure `groupId = 1` (Orb-only on-chain path).
If your v3 proof arrives as ABI-encoded bytes, decode it to `uint256[8]` before
calling `verifyProof`:
```ts viem theme={"system"}
import { decodeAbiParameters } from "viem";
const unpackedProof = decodeAbiParameters([{ type: "uint256[8]" }], proof)[0];
```
```ts ethers.js theme={"system"}
import { defaultAbiCoder as abi } from "@ethersproject/abi";
const unpackedProof = abi.decode(["uint256[8]"], proof)[0];
```
## 2. Verifying Uniqueness proofs in `WorldIDVerifier.sol` (World ID 4.0)
`WorldIDVerifier` is deployed on World Chain Mainnet as an upgradeable proxy.
Use the proxy address for your environment:
| Environment | Chain | `WorldIDVerifier` proxy |
| ----------- | ----------- | ---------------------------------------------------------------------------------------------------------------------- |
| Production | World Chain | [0x00000000009E00F9FE82CfeeBB4556686da094d7](https://worldscan.org/address/0x00000000009E00F9FE82CfeeBB4556686da094d7) |
| Staging | World Chain | [0x703a6316c975DEabF30b637c155edD53e24657DB](https://worldscan.org/address/0x703a6316c975DEabF30b637c155edD53e24657DB) |
For v4 uniqueness proofs, call `verify(...)` on the `WorldIDVerifier` proxy and
store used nullifiers to enforce one-human-one-action semantics in your
contract.
```solidity theme={"system"}
interface IWorldIDVerifier {
function verify(
uint256 nullifier,
uint256 action,
uint64 rpId,
uint256 nonce,
uint256 signalHash,
uint64 expiresAtMin,
uint64 issuerSchemaId,
uint256 credentialGenesisIssuedAtMin,
uint256[5] calldata zeroKnowledgeProof
) external view;
}
contract VerifyUniquenessV4 {
IWorldIDVerifier public immutable verifier;
mapping(uint256 => bool) public nullifierUsed;
error InvalidNullifier();
constructor(IWorldIDVerifier _verifier) {
verifier = _verifier;
}
function verifyAndExecute(
uint256 nullifier,
uint256 action,
uint64 rpId,
uint256 nonce,
uint256 signalHash,
uint64 expiresAtMin,
uint64 issuerSchemaId,
uint256 credentialGenesisIssuedAtMin,
uint256[5] calldata proof
) external {
if (nullifierUsed[nullifier]) revert InvalidNullifier();
verifier.verify(
nullifier,
action,
rpId,
nonce,
signalHash,
expiresAtMin,
issuerSchemaId,
credentialGenesisIssuedAtMin,
proof
);
// Mark nullifier after successful verification (sybil resistance).
nullifierUsed[nullifier] = true;
// Execute protected business logic here.
}
}
```
Minimal mapping from IDKit result:
* `nullifier` = `responses[i].nullifier`
* `action` = `keccak256(action)` as `uint256`
* `rpId` = numeric form of your `rp_context.rp_id` (the `rp_`-prefixed string from your RP context, not the result)
* `nonce` = top-level `nonce`
* `signalHash` = `responses[i].signal_hash`
* `expiresAtMin` = `responses[i].expires_at_min`
* `issuerSchemaId` = `responses[i].issuer_schema_id`
* `credentialGenesisIssuedAtMin` = the request's `genesis_issued_at_min` constraint (`0` if unconstrained) — not returned in `responses[i]`
* `proof` = `responses[i].proof` (`uint256[5]`)
# React
Source: https://docs.world.org/world-id/idkit/react
Reference for @worldcoin/idkit React widgets and hooks, including request flows and presets.
Use `@worldcoin/idkit` when you want React-native ergonomics on top of `@worldcoin/idkit-core`.
## Install
```bash npm theme={"system"}
npm i @worldcoin/idkit
```
```bash pnpm theme={"system"}
pnpm add @worldcoin/idkit
```
```bash yarn theme={"system"}
yarn add @worldcoin/idkit
```
## Choose your API
* **Widgets**: quickest integration, built-in modal and state handling
* **Hooks**: headless control for custom UI and flow orchestration
## Controlled widgets
### Request widget
```tsx theme={"system"}
import { IDKitRequestWidget, orbLegacy, type RpContext } from "@worldcoin/idkit";
const rpContext: RpContext = {
rp_id: "rp_xxxxx",
nonce: "0x...",
created_at: 1735689600,
expires_at: 1735689900,
signature: "0x...",
};
{
const response = await fetch("/api/verify-proof", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ rp_id: rpContext.rp_id, idkitResponse: result }),
});
if (!response.ok) {
throw new Error("Backend verification failed");
}
}}
onSuccess={() => {
// Called after `handleVerify` resolves (or immediately if omitted).
// Update your app state/UI here.
}}
onError={(errorCode, debugReport) => {
console.error("IDKit error", errorCode, debugReport);
}}
/>;
```
### Widget callbacks
* `onSuccess` is required for `IDKitRequestWidget` and `IDKitSessionWidget`.
* `handleVerify` is optional and only available on widgets. Use it to verify the proof in your backend before success is emitted.
* If `handleVerify` throws/rejects, the widget enters an error state, emits `onError("failed_by_host_app")`, and does not call `onSuccess`. This means World App already returned a proof, but your callback or backend did not accept it.
* `onError` is optional.
## Headless hooks
### `useIDKitRequest`
```tsx theme={"system"}
import { useIDKitRequest, orbLegacy } from "@worldcoin/idkit";
const flow = useIDKitRequest({
app_id: "app_xxxxx",
action: "my-action",
rp_context,
allow_legacy_proofs: true,
preset: orbLegacy({ signal: "user-123" }),
polling: {
interval: 2_000,
timeout: 120_000,
},
});
;
```
Hook result fields:
* `open()`
* `reset()`
* `isAwaitingUserConnection`
* `isAwaitingUserConfirmation`
* `isSuccess`
* `isError`
* `connectorURI`
* `result`
* `errorCode`
* `getDebugReport()`
## Invite-code mode
For invite-code flows, use `IDKitInviteCodeRequestWidget` (controlled) or `useIDKitInviteCodeRequest` (headless). Config matches `IDKitRequestWidget` / `useIDKitRequest` — invite-code mode adds no new required fields. See [Invite-code mode](/world-id/idkit/verification-flows#with-invite-code-mode) for when to use it.
```tsx theme={"system"}
import { IDKitInviteCodeRequestWidget, selfieCheckLegacy } from "@worldcoin/idkit";
{ /* ... */ }}
onSuccess={() => { /* ... */ }}
/>;
```
`useIDKitInviteCodeRequest` result fields (sibling of `useIDKitRequest`'s):
* `open()`
* `reset()`
* `isAwaitingUserConnection`
* `isAwaitingUserConfirmation`
* `isSuccess`
* `isError`
* `connectorURI`
* `codeExpiresAt`
* `result`
* `errorCode`
* `getDebugReport()`
### Migrating from QR / connect-URL
```tsx theme={"system"}
// Before — QR / connect-URL widget
import { IDKitRequestWidget, orbLegacy } from "@worldcoin/idkit";
;
```
```tsx theme={"system"}
// After — invite-code widget
import { IDKitInviteCodeRequestWidget, selfieCheckLegacy } from "@worldcoin/idkit";
;
```
Swap the component and preset. Props, callbacks, and backend verification stay the same. The headless equivalent is `useIDKitInviteCodeRequest` in place of `useIDKitRequest`.
## Session flows
Session verification uses `IDKitSessionWidget` (controlled) or `useIDKitSession` (headless); `onSuccess` returns an `IDKitResultSession`. Both share the request flows' widget callbacks and hook-result fields — including `onError(errorCode, debugReport?)` and `getDebugReport()`.
## Presets
React hooks/widgets take `preset` directly in config.
## Localization and UX notes
* Widgets support `language="en" | "es" | "th"`
* Widgets default to `autoClose={true}` after success
## Backend utilities
For RP signature generation in React/Next.js apps, use the pure JS subpath:
```ts theme={"system"}
import { signRequest } from "@worldcoin/idkit/signing";
```
See [RP Signatures](/world-id/idkit/signatures) for the full algorithm and test vectors.
# RP Signatures
Source: https://docs.world.org/world-id/idkit/signatures
Spec for generating RP signatures, with pseudocode, SDK examples, and test vectors.
Relying Party (RP) signatures prove a proof request comes from your app, preventing impersonation attacks.
Your backend signs every request with the `signing_key` from the [Developer Portal](https://developer.world.org),
and World ID verifies the signature before generating a proof. World ID enforces RP signatures for [World ID 4.0 requests](/world-id/4-0-migration).
Never expose your signing key to client-side code. If the key leaks, rotate it immediately in the Developer Portal.
## Algorithm
```text title="Implement it yourself" theme={"system"}
// IMPORTANT: Use Keccak-256, NOT SHA3-256. They have different padding.
// Most Ethereum libraries (ethers, viem, web3) use Keccak-256.
function hash_to_field(input_bytes) -> bytes32:
h = keccak256(input_bytes) // 32 bytes
n = big_endian_uint256(h) >> 8 // shift right 8 bits
return uint256_to_32bytes_be(n) // always starts with 0x00
function compute_rp_signature_message(nonce_bytes32, created_at_u64, expires_at_u64, action?) -> bytes:
size = 81 if action else 49
msg = new bytes(size)
msg[0] = 0x01 // version byte
msg[1..32] = nonce_bytes32 // 32-byte field element
msg[33..40] = u64_to_be(created_at) // big-endian uint64
msg[41..48] = u64_to_be(expires_at) // big-endian uint64
if action is not null:
msg[49..80] = hash_to_field(utf8_encode(action))
return msg
function sign_request(signing_key_hex, action?, ttl_seconds = 300):
// Accept signing keys with or without 0x prefix
key = parse_hex_32_bytes(signing_key_hex)
// 1. Generate nonce
random = crypto_random_bytes(32)
nonce_bytes = hash_to_field(random)
// 2. Timestamps
created_at = unix_time_seconds()
expires_at = created_at + ttl_seconds
// 3. Build message
msg = compute_rp_signature_message(nonce_bytes, created_at, expires_at, action)
// 4. EIP-191 prefix and hash
// The prefix uses the DECIMAL byte length of the message (e.g. "49" or "81")
prefix = "\x19Ethereum Signed Message:\n" + decimal_string(length(msg))
digest = keccak256(prefix + msg)
// 5. Sign with recoverable ECDSA (secp256k1)
(r, s, recovery_id) = ecdsa_secp256k1_sign(digest, key)
// 6. Encode: r(32) || s(32) || v(1), where v = recovery_id + 27
sig65 = r + s + byte(recovery_id + 27)
return {
sig: "0x" + hex(sig65),
nonce: "0x" + hex(nonce_bytes),
created_at: created_at,
expires_at: expires_at,
}
```
```typescript title="JavaScript / TypeScript" theme={"system"}
// Also available from @worldcoin/idkit/signing and @worldcoin/idkit-core/signing
import { signRequest } from "@worldcoin/idkit-server";
const sig = signRequest({
signingKeyHex: process.env.RP_SIGNING_KEY!,
action: "my-action",
ttl: 300, // optional, default 300s
});
// sig = { sig, nonce, createdAt, expiresAt }
```
```go title="Go" theme={"system"}
import "github.com/worldcoin/idkit/go/idkit"
// One-shot signing with options
sig, err := idkit.SignRequest(
os.Getenv("RP_SIGNING_KEY"),
idkit.WithAction("my-action"),
idkit.WithTTL(300), // optional, default 300s
)
// Or create a reusable signer for high-throughput backends
signer, err := idkit.NewSigner(os.Getenv("RP_SIGNING_KEY"))
sig, err = signer.SignRequest(idkit.WithAction("my-action"))
```
## Test vectors
Verify your implementation against these. All vectors use deterministic inputs.
### `hash_to_field`
```text title="empty string" theme={"system"}
input: "" (empty)
output: 0x00c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4
```
```text title='"test_signal"' theme={"system"}
input: "test_signal"
output: 0x00c1636e0a961a3045054c4d61374422c31a95846b8442f0927ad2ff1d6112ed
```
```text title="raw bytes" theme={"system"}
input: [0x01, 0x02, 0x03]
output: 0x00f1885eda54b7a053318cd41e2093220dab15d65381b1157a3633a83bfd5c92
```
```text title='"hello"' theme={"system"}
input: "hello" (0x68656c6c6f)
output: 0x001c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36dea
```
### `compute_rp_signature_message`
```text title="without action (49 bytes)" theme={"system"}
compute_rp_signature_message(
nonce = 0x008ae1aa597fa146ebd3aa2ceddf360668dea5e526567e92b0321816a4e895bd,
created_at = 1700000000,
expires_at = 1700000300,
)
output:
01008ae1aa597fa146ebd3aa2ceddf360668dea5e526567e92b0321816a4e895bd000000006553f100000000006553f22c
```
```text title='with action "test-action" (81 bytes)' theme={"system"}
compute_rp_signature_message(
nonce = 0x008ae1aa597fa146ebd3aa2ceddf360668dea5e526567e92b0321816a4e895bd,
created_at = 1700000000,
expires_at = 1700000300,
action = "test-action",
)
output:
01008ae1aa597fa146ebd3aa2ceddf360668dea5e526567e92b0321816a4e895bd000000006553f100000000006553f22c00aa0ce59768ae5b1c52f07a9387f14f09f277422c0d2f8a268c7bad0c60a46a
```
### `sign_request`
```text title="without action (session proof)" theme={"system"}
sign_request(
signing_key = 0xabababababababababababababababababababababababababababababababab,
random = [0x00, 0x01, ..., 0x1f], // deterministic for testing
created_at = 1700000000, // fixed clock for testing
ttl = 300,
)
nonce: 0x008ae1aa597fa146ebd3aa2ceddf360668dea5e526567e92b0321816a4e895bd
msg length: 49 bytes
sig: 0x14f693175773aed912852a601e9c0fd30f2afe2738d31388316232ce6f64ae9e4edbfb19d81c4229ba9c9fca78ede4b28956b7ba4415f08d957cbc1b3bdaa4021b
```
```text title='with action "test-action" (uniqueness proof)' theme={"system"}
sign_request(
signing_key = 0xabababababababababababababababababababababababababababababababab,
action = "test-action",
random = [0x00, 0x01, ..., 0x1f], // deterministic for testing
created_at = 1700000000, // fixed clock for testing
ttl = 300,
)
nonce: 0x008ae1aa597fa146ebd3aa2ceddf360668dea5e526567e92b0321816a4e895bd
msg length: 81 bytes
sig: 0x05594adb6c1495768a38d523d7d6ee6356b2c31231919198794ed022ade7d08f73753f83bd167067d99c9b969d28e9222315837c66af25867b041273a6d5056f1b
```
## Related pages
* [Integration guide](/world-id/idkit/integrate)
* [JavaScript SDK reference](/world-id/idkit/javascript)
* [Go SDK reference](/world-id/idkit/go)
# Swift
Source: https://docs.world.org/world-id/idkit/swift
Reference for the IDKit Swift API: request builders, presets, and polling.
Use `IDKit` for native iOS/macOS integrations backed by the same Rust core as other SDKs.
## Requirements
* iOS 15+ / macOS 12+
* Xcode 16+
## Install
Use Swift Package Manager with the published `idkit-swift` repository:
```swift theme={"system"}
.package(url: "https://github.com/worldcoin/idkit-swift.git", from: "")
```
## Request flow
```swift theme={"system"}
import IDKit
let rpContext = try RpContext(
rpId: "rp_xxxxx",
nonce: backend.nonce,
createdAt: backend.createdAt,
expiresAt: backend.expiresAt,
signature: backend.sig
)
let config = IDKitRequestConfig(
appId: "app_xxxxx",
action: "my-action",
rpContext: rpContext,
allowLegacyProofs: true,
returnTo: "myapp://verify-done", // Optional: mobile deep-link callback URL
environment: .production
)
let request = try IDKit.request(config: config).preset(orbLegacy(signal: "user-123"))
let connectURL = request.connectorURL
let requestID = request.requestID
```
## Presets
```swift theme={"system"}
import IDKit
let request = try IDKit.request(config: config).preset(
orbLegacy(signal: "user-123")
)
```
## Polling API
* `pollStatusOnce() async -> IDKitStatus`
* `pollUntilCompletion(options:) async -> IDKitCompletionResult`
* `IDKitPollOptions(pollIntervalMs:timeoutMs:)`
```swift theme={"system"}
let completion = await request.pollUntilCompletion(
options: IDKitPollOptions(pollIntervalMs: 2_000, timeoutMs: 120_000)
)
switch completion {
case .success(let result):
print(result)
case .failure(let error):
print(error)
}
```
## Invite-code mode
Use `presetWithInviteCode(_:)` on the builder to return an `IDKitInviteCodeRequest` instead of `IDKitRequest`. The polling surface is identical. See [Invite-code mode](/world-id/idkit/verification-flows#with-invite-code-mode) for when to use it.
```swift theme={"system"}
let request = try IDKit.request(config: config)
.presetWithInviteCode(selfieCheckLegacy(signal: "user-123"))
let connectorURL = request.connectorURL // URL with &c=&a=
let expiresAt = request.expiresAt // Date
let completion = await request.pollUntilCompletion()
```
### Migrating from QR / connect-URL
```swift theme={"system"}
// Before — QR / connect-URL flow
let request = try IDKit.request(config: config)
.preset(orbLegacy(signal: "user-123"))
let connectURL = request.connectorURL // render as QR
let completion = await request.pollUntilCompletion()
```
```swift theme={"system"}
// After — invite-code mode
let request = try IDKit.request(config: config)
.presetWithInviteCode(selfieCheckLegacy(signal: "user-123"))
let connectorURL = request.connectorURL // display to user (URL with code embedded)
let expiresAt = request.expiresAt // drive a countdown
let completion = await request.pollUntilCompletion()
```
The config object is unchanged. The `connectorURL` now includes `&c=&a=` params; use it alongside `expiresAt` in your UI. Polling, proof verification, and nullifier storage stay the same.
`IDKitInviteCodeRequest` exposes:
* `connectorURL: URL`
* `expiresAt: Date`
* `requestID: String`
* `pollStatusOnce() async -> IDKitStatus`
* `pollUntilCompletion(options:) async -> IDKitCompletionResult`
# Verification Flows
Source: https://docs.world.org/world-id/idkit/verification-flows
The flows your integration can land users in when you request a World ID proof.
When your app requests a World ID proof, the user lands in one of three flows, determined by whether they have World ID installed and — if not — whether they already have a World ID.
The integration flow is the same for all three paths, but the user experience differs.
| Flow | World ID installed | Existing account | What happens |
| --------- | ------------------ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Hot | Yes | — | World ID opens, shows a proof consent, and the user approves — typically under 10 seconds. If the user lacks the requested [credential](/world-id/idkit/credentials), World ID walks them through enrollment first; your integration sees no difference. |
| Cold | No | No | The user installs World ID and completes account onboarding first. The experience differs by platform — see below. |
| Semi-cold | No | Yes | The user installs World ID and logs in to their existing account. **Android:** the flow resumes automatically. **iOS:** the user must return to your app and rescan, unless you use invite-code mode — see below. |
## Cold and semi-cold flows
Both flows require an install and behave differently per platform because of how each handles **deferred deep linking** — preserving a link's context through an app store install so the app can act on it at first launch.
### Android
Android supports deferred deep linking through the Play Store. After downloading World ID, the user creates an account (cold) or logs in to an existing one (semi-cold), and World ID resumes the verification flow automatically.
```mermaid theme={"system"}
sequenceDiagram
participant App as Your App
participant User
participant Play as Play Store
participant WA as World ID
App->>User: IDKit request (connect URL / QR)
User->>Play: Download World ID
Play-->>WA: Deferred deep link preserved
WA->>User: Account onboarding OR Login
opt Credential not yet held
WA->>User: Credential enrollment
end
WA->>User: Proof consent
User->>WA: Approve
WA-->>App: Proof returned
```
### iOS
iOS does not support deferred deep linking through the App Store. Install loses the original verification context — for both cold and semi-cold users — so resuming the flow requires additional mechanisms.
#### Default behavior
1. The user downloads World ID from the App Store and creates an account or logs in to an existing one.
2. The user returns to your app, which re-triggers the IDKit verification request (e.g., by rescanning the QR code).
3. World ID opens and takes the user through the standard in-app flow.
4. The proof consent appears, the user approves, and the proof returns to your app.
```mermaid theme={"system"}
sequenceDiagram
participant App as Your App
participant User
participant Store as App Store
participant WA as World ID
App->>User: Prompt to install World ID
User->>Store: Download World ID
WA->>User: Account onboarding OR Login
User->>App: Return to your app
App->>User: IDKit request (rescan QR / connect URL)
WA->>User: Standard in-app flow
WA-->>App: Proof returned
```
#### With invite-code mode
Invite-code mode shows a 6-character code in your app that the user enters into World ID. World ID treats the code as an entry point to the in-app onboarding flows the user must complete to satisfy your IDKit request, then returns the proof.
Invite-code mode exists because iOS lacks deferred deep linking — Android preserves the context via the Play Store.
1. Your app triggers an IDKit invite-code request.
2. Your app opens the URL that IDKit provides. One of three paths follows:
* **User has World ID (mobile):** World ID launches directly via deep link.
* **User has World ID (desktop):** The user scans the QR code with World ID.
* **User needs to install World ID:** The user installs World ID, completes onboarding or logs in, then enters the invite code to resume the request.
3. World ID restores the verification context, walks the user through credential enrollment if needed, and presents a proof consent.
4. The user approves and the proof returns to your app.
The 6-character code persists across the App Store install, so once World ID is installed and onboarded the user can resume the flow without first returning to your app. This also covers cross-device scenarios (e.g., a desktop browser showing the QR for the user's phone) where deep linking cannot carry context.
**Demo**
**Flow diagram**
```mermaid theme={"system"}
sequenceDiagram
participant App as Your App
participant User
participant LP as Landing Page
participant Store as App Store
participant WA as World ID
App->>LP: Open IDKit-provided URL
LP->>User: Display invite code + QR
User->>Store: Download World ID
WA->>User: Account onboarding OR Login
User->>WA: Enter invite code
WA->>WA: Resolve code & restore context
WA->>User: Credential enrollment
WA->>User: Proof consent
User->>WA: Approve
WA-->>App: Proof returned
```
**Lifecycle**
* Codes expire after a short TTL (currently fifteen minutes).
* Codes are one-shot — once redeemed, they cannot be reused. Re-running the request returns a fresh code with a fresh TTL.
* After the user redeems the code, your existing poll loop receives the proof exactly as in QR mode.
**Integrate**
Setup is identical to the [standard integration](/world-id/idkit/integrate) — only the request call changes. Only the `selfieCheckLegacy` preset is supported for Selfie Check (Beta) today. It returns World ID 3.0 proofs; Selfie Check with World ID 4.0 is not available yet. For code samples and migration guides, see the per-SDK sections: [JavaScript](/world-id/idkit/javascript#invite-code-mode), [React](/world-id/idkit/react#invite-code-mode), [Swift](/world-id/idkit/swift#invite-code-mode).
# Overview
Source: https://docs.world.org/world-id/overview
## World ID
World ID is a privacy-preserving protocol that lets people prove they are real and unique online without sharing personal information.
It gives developers a high-assurance trust layer to stop bots, duplicate accounts, and abuse — while keeping onboarding fast and user data off your servers.
Through credentials like Proof of Human, Document, and Selfie Check, World ID extends that trust layer with additional proofs without exposing underlying user data.
```tsx theme={"system"}
import { IDKitRequestWidget } from "@worldcoin/idkit";
const rpContext = await getRpContext("verify-account");
```
## Credentials
Proof of Human
Highest-assurance uniqueness signal from Orb verification. Best for one-person-one-action flows and strong Sybil resistance.
Document
Proves possession of a unique government document through NFC checks. Useful for proof of age and document-backed access flows.
Selfie Check Beta
Low-friction liveness and uniqueness signal from a selfie flow. Best for sign-up and bot defense where speed matters most.
## Start building
## Privacy by architecture
# Authenticator Reference
Source: https://docs.world.org/world-id/reference/authenticator
## Authenticator Requests & Responses
This section describes how authenticators receive and respond to requests from RPs. This also reflects what is implemented in the .
The authenticator receives a request from an RP in the following format.
### Request Schema
The schema for the request is defined in the `world-id-primitives` crate as [`ProofRequest`](https://docs.rs/world-id-primitives/latest/world_id_primitives/request/struct.ProofRequest.html).
### Request Examples
```json theme={"system"}
{
"id": "req_18c0f7f03e7d",
"version": 1,
"created_at": "1771612953",
"expires_at": "1771613013",
"rp_id": "rp_0000000000000000000000000000000000001",
"action": "0x0000000000000000000000000000000000000000000000000000000000000001",
"nonce": "0x11d223ce7b91ac212f42cf50f0a3439ae3fcdba4ea32acb7f194d1051ed324c2",
"signature": "304502205cce35752b1642327bebf9f203960dc83f92fa919a4567981ce0c157060ca04e022100f328e42ff2609ddc1fbc7da17896ded2687acb3a4eeb8847a6f3a87ce50ed016",
"requests": [
{
"identifier": "passport",
"issuer_schema_id": 9303,
"signal": "abcd-efgh-ijkl"
}
]
}
```
```json theme={"system"}
{
"id": "req_18c0f7f03e7d",
"version": 1,
"created_at": "1771612953",
"expires_at": "1771613013",
"rp_id": "rp_0000000000000000000000000000000000001",
"action": "0x0000000000000000000000000000000000000000000000000000000000000001",
"nonce": "0x11d223ce7b91ac212f42cf50f0a3439ae3fcdba4ea32acb7f194d1051ed324c2",
"signature": "304502205cce35752b1642327bebf9f203960dc83f92fa919a4567981ce0c157060ca04e022100f328e42ff2609ddc1fbc7da17896ded2687acb3a4eeb8847a6f3a87ce50ed016",
"requests": [
{
"identifier": "passport",
"issuer_schema_id": 9303,
"signal": "abcd-efgh-ijkl"
},
{
"identifier": "poh",
"issuer_schema_id": 1,
"signal": "abcd-efgh-ijkl"
}
],
"constraints": {
"all": ["passport", "poh"]
}
}
```
```json theme={"system"}
{
"id": "req_18c0f7f03e7d",
"version": 1,
"created_at": "1771612953",
"expires_at": "1771613013",
"rp_id": "rp_0000000000000000000000000000000000001",
"action": "0x0000000000000000000000000000000000000000000000000000000000000001",
"nonce": "0x11d223ce7b91ac212f42cf50f0a3439ae3fcdba4ea32acb7f194d1051ed324c2",
"signature": "304502205cce35752b1642327bebf9f203960dc83f92fa919a4567981ce0c157060ca04e022100f328e42ff2609ddc1fbc7da17896ded2687acb3a4eeb8847a6f3a87ce50ed016",
"requests": [
{
"identifier": "passport",
"issuer_schema_id": 9303,
"signal": "abcd-efgh-ijkl"
},
{
"identifier": "my-number-card",
"issuer_schema_id": 9310,
"signal": "mnop-qrst-uvwx"
},
{
"identifier": "orb",
"issuer_schema_id": 1,
"signal": "abcd-efgh-ijkl"
}
],
"constraints": {
"all": [
"orb",
{
"any": ["passport", "my-number-card"]
}
]
}
}
```
### Constraint Evaluation
When using the `any` constraint, the order of credential types in the array determines **priority order**. The authenticator will attempt to provide the first available credential type in the list. If that credential is not available, it will fall back to the next type in the array, and so on.
Priority ordering only applies to `any` constraints. For `all` constraints,
all specified credential types must be provided regardless of order.
**Example**:
```json theme={"system"}
{
"constraints": { "any": ["poh", "passport"] }
}
```
In this case:
* If the user has an PoH credential, the authenticator will provide it.
* If the user does not have an PoH credential but has a passport credential, the authenticator will provide the passport instead.
* The PoH credential type has priority over passport.
This priority mechanism allows RPs to request their preferred credential type while still accepting fallback options if the preferred type is unavailable.
### Response Schema
The schema for the response is defined in the `world-id-primitives` crate as [`ProofResponse`](https://docs.rs/world-id-primitives/latest/world_id_primitives/request/struct.ProofResponse.html).
### Response Examples
```json theme={"system"}
{
"id": "req_18c0f7f03e7d",
"version": 1,
"responses": [
{
"identifier": "orb",
"issuer_schema_id": 1,
"proof": "0x0000000000000000000000000000000000000000000000000000000000000000000000000",
"nullifier": "nil_00000000000000000000000000000000000000000000000001"
}
]
}
```
```json theme={"system"}
{
"id": "req_18c0f7f03e7d",
"version": 1,
"error": "constraints_not_satisfied",
"responses": []
}
```
```json theme={"system"}
{
"id": "req_18c0f7f03e7d",
"version": 1,
"responses": [
{
"identifier": "orb",
"issuer_schema_id": 1,
"proof": "0x0000000000000000000000000000000000000000000000000000000000000000000000000",
"session_nullifier": "00000000000000000000000000000000000000000000000001"
}
]
}
```
# Contracts 3.0
Source: https://docs.world.org/world-id/reference/contracts
World ID 3.0 smart contracts overview: supported chains, architecture, World ID Router verifyProof, and sybil resistance.
This page documents World ID 3.0 (legacy) smart contracts. For the latest on-chain verification guide including World ID 4.0, see the [On-chain Verification page](/world-id/idkit/onchain-verification) and the [World ID 4.0 Migration guide](/world-id/4-0-migration).
All of our smart contracts are available on GitHub:
* [World ID Smart Contracts](https://github.com/worldcoin/world-id-contracts)
* [State Bridge Smart Contracts](https://github.com/worldcoin/world-id-state-bridge)
If you're interested in using World ID and verifying proofs on-chain, see our
[On-Chain Verification guide](/world-id/idkit/onchain-verification).
## Supported Chains
Chain
Testnet
Role
Identity Availability
World Chain
World Chain
Bridged
\~5 Minutes after Ethereum
Ethereum
Sepolia
Canonical
\~60 minutes after verification
Optimism
Optimism Sepolia
Bridged
\~5 Minutes after Ethereum
Polygon
Polygon
Bridged
\~40 Minutes after Ethereum
Base
Base
Bridged
\~5 Minutes after Ethereum
Find our smart contract addresses in the
[On-chain Verification guide](/world-id/idkit/onchain-verification).
## Architecture
This section offers a high-level overview of the various smart contracts that make up World ID. This structure (including state bridging) is replicated on testnets -- currently Sepolia, Optimism Sepolia, and Base Sepolia.
### Identity Managers: `WorldIdIdentityManager`
Identity Managers are only deployed on Ethereum. The Identity Manager contracts are responsible for managing the Semaphore instance. Worldcoin's signup sequencers call the Identity Manager contracts to add or remove identities from the merkle tree.
### State Bridges: `OpStateBridge`/`PolygonStateBridge`
One State Bridge contract is deployed on Ethereum for each bridged chain. It publishes the root of the merkle tree to its configured chain's World ID contract, allowing proofs to be verified on that chain.
### Bridged World ID: `OpWorldId`/`PolygonWorldId`
One World ID contract is deployed on each bridged chain, with an associated State Bridge contract on Ethereum. It is responsible for receiving merkle roots from its State Bridge contract, and verifying World ID proofs against those roots.
You can deploy your own State Bridge contract on Ethereum and Bridged World ID
contract to any chain to bridge World ID to that chain permissionlessly.
### World ID Router: `WorldIdRouter`
**This is the contract you should interact with.**
The World ID Router will route your call to the correct Identity Manager contract (Ethereum) or World ID contract (L2 Chains) based on the `groupId` argument. This contract is proxied, so you will not need to update your code if the underlying contracts are upgraded.
Only Orb credentials are supported on-chain, so the `groupId` must be `1`.
## Usage
The `verifyProof` method of the **World ID Router** is used to verify proofs on-chain.
### Arguments
Parameter
Type
Description
root
uint256
The World ID root to verify against.
groupId
uint256
Determines which Credential Type to verify against. As only Orb
credentials are supported on-chain, this must be 1.
signalHash
uint256
The keccak256 hash of the signal to verify.
nullifierHash
uint256
The root of the merkle tree to verify against. This is obtained from the
IDKit widget as a hex string nullifier\_hash, and must be
converted to a uint256 before passing it to the
verifyProof method.
externalNullifierHash
uint256
The keccak256 hash of the externalNullifier to
verify. The externalNullifier is computed from the
app\_id and action.
proof
uint256\[8]
The zero-knowledge proof to verify. This is obtained from the IDKit
widget as a hex string proof, and must be converted to a
uint256\[8] before passing it to the
verifyProof method.
#### Example: groupId
```solidity title="Orb-Only groupId" theme={"system"}
uint256 internal immutable groupId = 1;
```
#### Example: signalHash
```solidity title="signalHash" theme={"system"}
abi.encodePacked(signal).hashToField();
```
#### Example: externalNullifierHash
```solidity title="externalNullifierHash" theme={"system"}
externalNullifier = abi.encodePacked(abi.encodePacked(appId).hashToField(), action)
externalNullifierHash = externalNullifier.hashToField();
```
Read more about the External Nullifier in Protocol Internals.
#### Example: Unpacking Proof
```ts title="viem" theme={"system"}
import { decodeAbiParameters } from 'viem'
const unpackedProof = decodeAbiParameters([{ type: 'uint256[8]' }], proof)[0]
```
```ts title="ethers.js" theme={"system"}
import { defaultAbiCoder as abi } from '@ethers/utils'
const unpackedProof = abi.decode(['uint256[8]'], proof)[0]
```
### Sybil resistance
While the World ID protocol makes it very easy to make your contracts sybil resistant, this takes a little more than just calling the `verifyProof` function. To make your contract sybil-resistant, you'll need to do the following:
* Store the `nullifier` of each user that has successfully verified a proof.
* When a user attempts to verify a proof, check that the `nullifier` is not already in the list of used `nullifier`s.
Here's an example function doing the above. You can also use the [World ID starter kits](/world-id/idkit/onchain-verification) to get started with sybil resistance.
```solidity theme={"system"}
/// @param root The root (returned by the IDKit widget).
/// @param groupId The group ID
/// @param signal An arbitrary input from the user, usually the user's wallet address
/// @param nullifier The nullifier for this proof, preventing double signaling (returned by the IDKit widget).
/// @param proof The zero-knowledge proof that demonstrates the claimer is registered with World ID (returned by the IDKit widget).
function verifyAndExecute(
address signal,
uint256 root,
uint256 nullifier,
uint256[8] calldata proof
) public {
// First make sure this person hasn't done this before
if (nullifiers[nullifier]) revert InvalidNullifier();
// Verify the provided proof is valid and the user is verified by World ID
worldId.verifyProof(
root,
groupId,
abi.encodePacked(signal).hashToField(),
nullifier,
externalNullifierHash,
proof
);
// Record the user has done this, so they can't do it again (sybil-resistance)
nullifiers[nullifier] = true;
// Finally, execute your logic here, for example issue a token, NFT, etc...
}
```
# NFC Issuer
Source: https://docs.world.org/world-id/reference/nfc-issuer
NFC issuer endpoints, including migration for World ID v3 NFC credentials.
The NFC issuer exposes endpoints for issuing and migrating NFC credentials. For product-level credential semantics, Sybil-resistance guarantees, and validity details, see [NFC Credential](/world-id/credentials/9303).
The base URL is environment-specific. Contact your World ID point of contact for
environment endpoints and access.
## Migration
This endpoint is intended for World ID v3 holders to obtain v4 NFC
credentials. It is not a general re-issuance endpoint for v4 holders.
/v2/migrate
**Content-Type:** `application/json`
### Request
#### Headers
Header
Required
Description
x-zkp-proof
string
yes
Base64-encoded JSON containing the ZKP proof and identity commitment.
attestation-gateway-token
string
yes
Attestation gateway token for device integrity verification.
#### Body fields
Field
Required
Description
identityCommitment
string
yes
The holder's identity commitment (decimal or hex with 0x
prefix). Must match the ZKP header.
sub
string
yes
World ID 4.0 blinded subject identifier (hex with 0x
prefix, 256-bit). Must match previous migrations for this identity.
credential
object
yes
Flow-specific credential data (see below). The client decrypts PCP data
locally before submission.
#### Credential fields
The `credential` object contains data extracted and decrypted from the user's Personal Custody Package (PCP) by the client.
Field
Description
credential.sod
string
Base64-encoded SOD (Security Object Document) in DER format from
the identity document.
credential.verification\_metadata
string
Base64-encoded verification metadata from the original document
check.
```json theme={"system"}
{
"identityCommitment": "0x000000000000000000000000000000000000000000000000000000000000000c",
"sub": "0x000000000000000000000000000000000000000000000000000000000000002a",
"credential": {
"sod": "",
"verification_metadata": ""
}
}
```
Field
Description
credential.sd\_jwt
string
SD-JWT (Selective Disclosure JWT) from the MNC verification flow.
```json theme={"system"}
{
"identityCommitment": "0x000000000000000000000000000000000000000000000000000000000000000c",
"sub": "0x000000000000000000000000000000000000000000000000000000000000002a",
"credential": {
"sd_jwt": ""
}
}
```
### Response
#### Success response
```json theme={"system"}
{
"result": {
"credential": ""
}
}
```
#### Error responses
| Status | Error | Description |
| ------ | ------------------ | --------------------------------------------------------------------------- |
| 400 | `invalid_data` | Request payload is malformed or missing required fields. |
| 400 | `sub_mismatch` | `sub` does not match the one used in previous migrations for this identity. |
| 400 | `document_expired` | The identity document has expired and cannot be used for migration. |
| 401 | `unauthorized` | Authentication failed. |
| 404 | `not_found` | No matching enrollment record found for this credential. |
## Planned Endpoints
### Enrollment Coming soon
Details coming soon.
### Re-issuance Coming soon
Details coming soon.
## Implementation Notes
This is advanced documentation about the internal workings of the NFC
Credential and is not relevant for RP integration.
### Associated Data
The associated data of this credential contains different data groups found in the original document. More information coming soon.
#### Associated Data Commitment
To ensure the associated data has guaranteed integrity and can be re-used in the future for credential re-issuance, the commitment is computed as follows:
1. For ICAO-9303 documents, the message digest of the `EF.SOD` signature is used. The message digest is obtained from `SignedData.SignerInfos[0].SignedAttrs` where the signed attribute for the digest is identified by the Object Identifier `1.2.840.113549.1.9.4`. This digest is then hashed with the `blake3` hashing function from the raw bytes. Finally, the `blake3` hash is converted to a field element with modulo reduction.
2. Information on MNC documents coming soon.
# PoH Issuer
Source: https://docs.world.org/world-id/reference/poh-issuer
PoH issuer endpoints, including credential refresh for v3 to v4 migration.
The PoH issuer provides endpoints that issue proof-of-human (PoH) credentials. The main endpoint is credential refresh, which re-issues a PoH credential using a Personal Custody Package (PCP) or a credential-only refresh flow.
Base URL is environment-specific and served by the signup-service app-api. Contact
your World ID point of contact for environment endpoints and access.
The refresh endpoint is intended for World ID v3 holders to obtain v4 PoH
credentials. It is not a general re-issuance endpoint for v4 holders.
## Credential refresh
/api/v1/refresh
**Content-Type:** `multipart/form-data`
## Request
### Headers
| Header | Type | Required | Description |
| ------------- | -------- | -------- | -------------------------------------------------------------- |
| `x-zkp-proof` | `string` | yes | Base64-encoded ZKP string containing `idCommitment` and `sub`. |
### Query parameters
| Query | Type | Required | Description |
| -------- | -------- | -------- | ------------------------- |
| `idComm` | `string` | yes | User identity commitment. |
### Form fields (always required)
| Field | Type | Required | Description |
| ------------------- | -------- | -------- | ---------------------------------------------------------------------------------------- |
| `sub` | `string` | yes | User account ID (hex with `0x` prefix). Must match previous refreshes for this `idComm`. |
| `encrypted_user_id` | `string` | no | Temporary field while operator rewards depend on it. |
### PCP form fields (required only when submitting a PCP)
Include all fields below when refreshing with a Personal Custody Package.
| Field | Type | Description |
| ----------------------- | -------- | -------------------------- |
| `signup_id` | `string` | Signup ID. |
| `signup_id_salt` | `string` | Signup ID salt. |
| `orb_id` | `string` | Orb ID. |
| `orb_id_salt` | `string` | Orb ID salt. |
| `operator_id` | `string` | Operator ID. |
| `operator_id_salt` | `string` | Operator ID salt. |
| `signup_reason` | `string` | Signup reason. |
| `signup_reason_salt` | `string` | Signup reason salt. |
| `timestamp` | `string` | Timestamp. |
| `timestamp_salt` | `string` | Timestamp salt. |
| `software_version` | `string` | Software version. |
| `software_version_salt` | `string` | Software version salt. |
| `orb_country` | `string` | Orb country. |
| `orb_country_salt` | `string` | Orb country salt. |
| `iris_code_shares_0` | `string` | Iris code share 0. |
| `iris_code_shares_1` | `string` | Iris code share 1. |
| `iris_code_shares_2` | `string` | Iris code share 2. |
| `hashes.json` | `file` | PCP hashes JSON file. |
| `hashes.sign` | `file` | PCP hashes signature file. |
### Example (credential-only refresh)
```bash theme={"system"}
curl -X POST "https:///api/v1/refresh?idComm=0xabc123..." \
-H "x-zkp-proof: " \
-F "sub=0x1a2b3c"
```
### Example (refresh with PCP)
```bash theme={"system"}
curl -X POST "https:///api/v1/refresh?idComm=0xabc123..." \
-H "x-zkp-proof: " \
-F "sub=0x1a2b3c" \
-F "signup_id=signup_123" \
-F "signup_id_salt=..." \
-F "orb_id=orb_abc" \
-F "orb_id_salt=..." \
-F "operator_id=operator_123" \
-F "operator_id_salt=..." \
-F "signup_reason=..." \
-F "signup_reason_salt=..." \
-F "timestamp=1700000000" \
-F "timestamp_salt=..." \
-F "software_version=1.2.3" \
-F "software_version_salt=..." \
-F "orb_country=US" \
-F "orb_country_salt=..." \
-F "iris_code_shares_0=..." \
-F "iris_code_shares_1=..." \
-F "iris_code_shares_2=..." \
-F "hashes.json=@hashes.json" \
-F "hashes.sign=@hashes.sign"
```
## Response
### Success response
```json theme={"system"}
{
"success": true,
"credential": "",
"message": "Credential refreshed successfully"
}
```
### Error responses
| Status | Error | Description |
| ------ | --------------------- | ---------------------------------------------------------- |
| 400 | `INVALID_SUB` | `sub` is missing or invalid. |
| 400 | `SUB_MISMATCH` | `sub` does not match the one used in previous refreshes. |
| 404 | `NO_SIGNUP_RECORD` | No enrollment record found. User must re-enroll at an Orb. |
| 429 | `RATE_LIMIT_EXCEEDED` | Refresh rate limit exceeded for the current window. |
| 503 | - | Credential refresh is disabled. |
If PCP validation fails, the endpoint returns an error status with `PCP_VALIDATION_FAILED`.
## Credential object format
The `credential` response field is a base64-encoded JSON representation of the World ID `Credential` object defined in `world-id-protocol/crates/primitives/src/credential.rs`.
### Example (decoded)
```json theme={"system"}
{
"id": 123456789,
"version": "V1",
"issuer_schema_id": 42,
"sub": "",
"genesis_issued_at": 1733241600,
"expires_at": 1764777600,
"claims": ["", "", ""],
"associated_data_hash": "",
"signature": "",
"issuer": {
"pk": ["", ""]
}
}
```
### Field definitions
| Field | Type | Description |
| ---------------------- | ---------------- | ------------------------------------------------------------------------------------------------------- |
| `id` | `uint64` | Issuer-scoped reference identifier for the credential. |
| `version` | `string` | Credential version. Current value is `V1`. |
| `issuer_schema_id` | `uint64` | Identifier for the (issuer, schema) pair registered in `CredentialSchemaIssuerRegistry`. |
| `sub` | `FieldElement` | Blinded subject identifier derived from the World ID leaf index and an issuer-specific blinding factor. |
| `genesis_issued_at` | `uint64` | Unix timestamp (seconds) of the first issuance of this credential. |
| `expires_at` | `uint64` | Unix timestamp (seconds) for expiration. |
| `claims` | `FieldElement[]` | Up to 16 claim commitments. Unused indices are the zero field element. |
| `associated_data_hash` | `FieldElement` | Poseidon2 hash of issuer-defined associated data. The associated data itself is not included. |
| `signature` | `string` | 64-byte compressed EdDSA signature over the credential hash, hex-encoded (128 hex chars, no `0x`). |
| `issuer` | `EdDSAPublicKey` | Issuer public key that signed the credential. |
### Field representations
* **FieldElement** values (`sub`, `claims`, `associated_data_hash`) are hex strings with a `0x` prefix and 64 hex characters.
* **Issuer public key** (`issuer.pk`) is serialized as `[x, y]` decimal strings for BabyJubJub affine coordinates.
* **Signature** is hex-encoded compressed bytes (no `0x` prefix).
### PoH credential semantics
* The user first obtains an Orb credential (currently a PCP in v2.3 format).
* `claim[0]` is a commitment to the Orb credential, currently `H(hashes.json)`.
* When refreshing without a PCP, `claims[0]` is derived from issuer-defined refresh data (a hash of `idCommitment`, `sub`, and a timestamp).
* The PoH credential has no associated data, so `associated_data_hash` is the zero field element.
* The PoH credential subject is blinded and differs from the Orb credential subject.
* The issuer may require a proof for the requested `sub` to prevent bricking an identity.
These details are issuer-specific and may evolve as the protocol migrates to the World ID 4.0 credential format.
## References
* [World ID 4.0 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs)
# How to get access
Source: https://docs.world.org/world-id/sandbox/sandbox-access
Install the sandbox World ID app and point your integration at the Sandbox environment.
Getting set up in Sandbox has two parts: install the **sandbox World ID app** on your
test device, and point your **integration** at the Sandbox environment. Both are
covered below.
New here? Start with [What is Sandbox?](/world-id/sandbox/what-is-sandbox) for context.
## 1. Install the sandbox World ID app
The sandbox apps are not publicly listed in the App Store or on Google Play. You
install them through the testing links below.
### iOS — TestFlight
The sandbox build is distributed through TestFlight, and gated behind an enrollment
request.
#### Request tester access
1. Install [TestFlight](https://apps.apple.com/app/testflight/id899247664) from the
App Store if you don't have it.
2. In the [Developer Portal](https://developer.world.org), select **World ID Sandbox**
from the sidebar, choose the **iOS** tab, and submit the Apple Account email you
want enrolled. Enrollment is tied to a team, so open the sandbox panel from within
a team.
3. Wait for approval.
#### Install the build
1. Once your request is approved, you'll get a TestFlight invitation email.
2. Accept the invitation.
3. Install it.
#### Troubleshooting FAQ
* Confirm the email you submitted is the Apple Account you'll use to sign in to
TestFlight, not any other email.
* If your request was rejected or your access was revoked, submitting the same email
again won't create a new request. Contact Sandbox Support at
[sandbox.access@toolsforhumanity.org](mailto:sandbox.access@toolsforhumanity.org).
### Android — Google Play
The sandbox build is distributed through a **private testing track on Google Play**.
#### Request tester access
1. In the [Developer Portal](https://developer.world.org), select **World ID Sandbox**
from the sidebar.
2. Enter the email address for the Google Account you use with the Google Play Store,
then request tester access.
3. Wait until access is granted before opening the Google Play testing link.
#### Install the build
1. On your Android device, open the Google Play Store and confirm that it is signed in
with the exact Google Account for which you requested tester access.
2. If this is the account's first Google Play Store sign-in, complete the initial setup
and accept all Google Play Terms of Service before continuing.
3. In the browser that will open the testing link, sign in to the same Google Account.
The browser and Google Play Store must use the same account; otherwise, Google Play
may report that the build is unavailable.
4. Return to **World ID Sandbox** in the
[Developer Portal](https://developer.world.org). On your Android device, scan the QR
code or open the Google Play testing link.
5. Install the **World ID (Sandbox)** build from Google Play.
#### Troubleshooting FAQ
* Confirm that the email used to request tester access matches the Google Account used
by both the browser and Google Play Store.
* If the account has just signed in to Google Play for the first time, Google may need
time to recognize the new account. Wait at least **15 minutes** before opening the testing link again.
## 2. Point your integration at Sandbox
1. **Update IDKit** to the latest version.
2. **Set `environment: sandbox`** in your IDKit configuration.
3. To verify a proof, send it to the production verify endpoint (`https://developer.world.org/api/v4/verify/${rp_id}`).
Nothing else is required — the handoff will open the sandbox World ID app and return a
proof to your session over the bridge.
## Things to know
* **Sandbox apps aren't publicly listed in the app stores.** Testers install via
TestFlight (iOS) or a private Google Play testing link (Android). Because access is
gated to testers, install links and store acquisition can behave differently from a
public production listing.
* **Proofs are non-production.** Identities and proofs issued in Sandbox are for
integration validation only.
* **Accounts are resettable.** You can delete and recreate accounts freely while
testing.
## Next step
Once you're installed and pointed at Sandbox, you're ready to run the Selfie Check
integration flows. See [Testing Selfie Check in Sandbox](/world-id/sandbox/testing-selfie-check)
for coverage, critical user journeys, and known limitations.
# Testing Selfie Check (Beta) in Sandbox
Source: https://docs.world.org/world-id/sandbox/testing-selfie-check
Coverage, critical user journeys, and known limitations for testing your Selfie Check (Beta) integration in Sandbox.
Sandbox lets you run the full [Selfie Check (Beta)](/world-id/credentials/11) relying-party
journey end-to-end — from your surface, through IDKit, into the sandbox World ID app,
and back — without touching production identities or real proofs.
Selfie Check (Beta) must be enabled for your app before you can test it. To
enable the feature flag, request access through your World point of contact.
New to Sandbox? Start with [What is Sandbox?](/world-id/sandbox/what-is-sandbox) and
[How to get access](/world-id/sandbox/sandbox-access) before working through this
guide.
In scope: the full relying-party journey — request handoff, consent, capture,
enrollment and matching, proof generation, and delivery of the proof. Out of scope:
production identity data and real-world uniqueness at scale. Sandbox accounts and
proofs are for integration testing only, not load testing, security certification,
or production sign-off.
## Coverage
Testing is organized by entry surface and user state, using the same [Hot, Cold, and
Semi-cold states](/world-id/idkit/verification-flows) as the rest of World ID:
* **Hot** — the user already has World ID installed. If they're already Selfie Check
enrolled, they go straight to face match; if not, World ID walks them through
enrollment first, then match. (Selfie Check has no distinct Warm flow — enrollment
happens inline within Hot, same as [Verification Flows](/world-id/idkit/verification-flows)
describes.)
* **Cold** — a new user with no World ID app: the full funnel, including install,
account creation, date of birth, invite code (iOS), enrollment, and Selfie Check.
* **Semi-cold** — an existing user without World ID on this device: reinstall and
account recovery, then Selfie Check.
| Entry surface | State | What it exercises |
| ------------- | --------- | --------------------------------------------------------------------------------------------------- |
| Native app | Hot | Same-device: validated user presents a face credential; deep link into World ID and back. |
| Native app | Cold | Install → create account → add date of birth → redeem invite code → enroll → complete Selfie Check. |
| Native app | Semi-cold | Existing user reinstalls and recovers their account on a fresh device, then completes Selfie Check. |
| Web app | Hot | Cross-device: start on web, complete on phone via QR scan, proof returns to the web session. |
| Web app | Cold | Same cold funnel, cross-device via QR; proof returns to the originating web session. |
| Web app | Semi-cold | Same semi-cold recovery, cross-device via QR. |
## Known limitations
* **Sandbox apps aren't publicly listed in the app stores.** Testers install through
TestFlight on iOS or a private Google Play testing link on Android. Because those
testing programs gate access, app-store acquisition can differ from a public
production listing — see
[How to get access](/world-id/sandbox/sandbox-access#things-to-know).
* **iOS Semi-cold is currently limited.** The reinstall/login journey reliably works on
Android today. On iOS, if the user taps "Sign in" instead of "Sign up" mid-flow,
there's no path to add the invite code — they have to restart from a fresh QR or deep
link. Expect iOS Semi-cold to behave differently from Android until this is closed.
* **Invite-code handling in the Cold flow differs by platform.** Confirm how invite
codes are presented and redeemed on the platform you're targeting — see
[invite-code mode](/world-id/idkit/verification-flows#with-invite-code-mode).
## Next step
Questions, or found a journey these scenarios don't cover? Reach out to your World
point of contact.
# What is Sandbox?
Source: https://docs.world.org/world-id/sandbox/what-is-sandbox
An isolated, production-like environment for testing your World ID integration end-to-end.
Sandbox is a dedicated, production-like environment for building and testing your
World ID integration. It runs alongside Production as its own environment, with its
own backend and its own builds of the World ID app for iOS and Android, built from
the same codebase and release process as Production — so Sandbox is just as stable
and reliable.
It lets you run a complete integration — from your app or website, through IDKit, into
the World ID app, and back — without touching production identities or issuing real
proofs.
## Why use Sandbox
Sandbox is built for integration testing. It gives you:
* **A full end-to-end round trip.** Sandbox is a valid IDKit destination, so you can
drive a real request from your surface into the World ID app and receive a proof
back over the bridge — the same path your production integration will use.
* **Resettable accounts.** Delete an account and sign up again as often as you need.
* **Simulated verification.** Exercise verification flows without real hardware or
real-world credentials.
* **Controllable gating.** You decide whether the system enforces fraud, risk, and
attestation checks. Turn enforcement off to move quickly through integration testing,
or turn it on to validate how your integration behaves under production-like gating.
## What you can test
The full relying-party journey from your surface into the World ID app and back:
request handoff, consent, capture, enrollment and matching, proof generation, and
delivery of the proof to your session.
Both same-device (deep link) and cross-device (QR) flows are supported, on both iOS
and Android sandbox builds.
## What Sandbox is not
* **Not production.** Accounts and proofs issued in Sandbox are for integration
validation only — not load testing, security certification, or production sign-off.
* **Not a source of real uniqueness.** Sandbox does not represent real-world identity
or uniqueness at scale.
* **Not publicly listed in the app stores.** The iOS sandbox app is available through
TestFlight, while Android is distributed through a private Google Play testing
track. See [How to get access](/world-id/sandbox/sandbox-access) for how to install
them.
## Next step
Ready to try it? Head to [How to get access](/world-id/sandbox/sandbox-access) to
install the sandbox app and point your integration at Sandbox.
# Understanding when to use World ID
Source: https://docs.world.org/world-id/yc
World ID is a privacy-preserving trust layer for companies that need to know whether a real, unique, present, or eligible human is behind an action.
## Example scenarios
Enforce one-person, one-action on rewards, referrals, voting, reviews,
promotions, and social participation.
**Integrate Proof of Human**
Require human presence when someone approves a payment, recovers an account,
deploys software, or authorizes a high-risk transaction.
**Add Selfie Check**
Confirm someone meets a policy without asking your product to store their
full identity documents.
**Explore Identity Check**
Distinguish people from agents, and make human approval an explicit boundary
for consequential actions.
**Integrate AgentKit**
Reduce impersonation, fake accounts, reputation manipulation, and fraudulent
content while preserving privacy.
**Integrate Proof of Human**
## Where it fits in YC startups (based on Fall 2026 RFS guidelines)
| If you're building... | The simplest place to use World ID |
| ------------------------------------------ | -------------------------------------------------------------------------------------------------- |
| Proving you're human | Require one human per account to reduce Sybil attacks. |
| An AI consumer product | Protect a free trial, referral, reward, or limited promotion from duplicate claims. |
| Multiplayer AI | Distinguish verified people from AI agents in a shared environment. |
| An operating system for the physical world | Make sure tasks are assigned to real, unique workers. |
| A real-world data product | Limit data contribution and rewards to one unique contributor. |
| AI-native compliance | Add a privacy-preserving proof that a user is a real human before a regulated or high-risk action. |
| Crypto or stablecoins | Run fair allocations, grants, governance, airdrops, or incentives. |
| Cloud tools for small software | Reduce fake workspaces, spam, and abuse before privileged access. |
| Education | Protect scholarships, subsidized access, rewards, and student benefits. |
| Products for the aging population | Protect patients, family members, and caregivers from impersonation. |
| Self-maintaining APIs | Add human accountability before critical code changes, merges, or deployments. |
| Defense or physical infrastructure | Add a human trust signal to safety-critical operations. |
## Ready to build?
[Start integrating World ID](/world-id/idkit/integrate) to bring trust to your product.