IDKit is our solution for integrating World ID. You can use the React SDK for a pre-built widget or the JS and mobile SDKs for a custom integration.To familiarize yourself with the core concepts of World ID, check out this page.
Create your app in the Developer Portal. If you’re migrating from an old app you have to go through RP registration by clicking the Enable World ID 4.0 banner. Keep these values:
Step 4: Generate the connect URL and collect proof
You can test during development using the simulator and setting environment to "staging".
import { IDKit, orbLegacy } from "@worldcoin/idkit-core";const rpSig = await fetch("/api/rp-signature", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "my-action" }),}).then((r) => r.json());const request = await IDKit.request({ // App ID: `app_id` from the Developer Portal app_id: "app_xxxxx", // Action: Context that scopes what the user is proving uniqueness for // e.g., "verify-account-2026" or "claim-airdrop-2026". action: "my-action", rp_context: { rp_id: "rp_xxxxx", // Your app's `rp_id` from the Developer Portal nonce: rpSig.nonce, created_at: rpSig.created_at, expires_at: rpSig.expires_at, signature: rpSig.sig, }, allow_legacy_proofs: true, environment: "production", // Only set this to staging for testing with the simulator return_to: "myapp://verify-done", // Optional: mobile deep-link callback URL // Signal (optional): Bind specific context into the requested proof. // Examples: user ID, wallet address. Your backend should enforce the same value.}).preset(orbLegacy({ signal: "local-election-1" }));const connectUrl = request.connectorURI;const response = await request.pollUntilCompletion();
After successful completion, send the returned payload to your backend and
forward it directly to: POST https://developer.world.org/api/v4/verify/{rp_id}
Forward the IDKit result payload as-is. No field remapping is required.
app/api/verify-proof/route.ts
import { NextResponse } from "next/server";import type { IDKitResult } from "@worldcoin/idkit";export async function POST(request: Request): Promise<Response> { const { rp_id, idkitResponse } = (await request.json()) as { rp_id: string; idkitResponse: IDKitResult; }; const response = await fetch( `https://developer.world.org/api/v4/verify/${rp_id}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(idkitResponse), }, ); if (!response.ok) { return NextResponse.json({ error: "Verification failed" }, { status: 400 }); } // Proof is valid — now store the nullifier (see Step 6) return NextResponse.json({ success: true });}
Every World ID proof contains a nullifier — a value derived from the user’s World ID, your app, and the action. The same person verifying the same action always produces the same nullifier, but different apps or actions produce different ones — making nullifiers unlinkable across apps.The Developer Portal confirms the proof is cryptographically valid, but your backend must check that the nullifier hasn’t been used before. Without this, the same person could verify multiple times for the same action.Nullifiers are returned as 0x-prefixed hex strings representing 256-bit integers. We recommend converting and storing them as numbers to avoid parsing and casing issues that can lead to security vulnerabilities. For example, PostgreSQL doesn’t natively support 256-bit integers, instead you can convert to the nullifier to a decimal and store it as NUMERIC(78, 0).
CREATE TABLE nullifiers ( nullifier NUMERIC(78, 0) NOT NULL, action TEXT NOT NULL, verified_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (nullifier, action));