Skip to main content
IDKit v4 is currently in preview. APIs may change before the stable release. Use @worldcoin/idkit-core@^4.0 for early access.

Install

npm install @worldcoin/idkit-core

Prerequisites

Before writing code you need:
  1. An app ID (app_...) from the Developer Portal.
  2. An RP ID (rp_...) and signing key — obtained when you register your Relying Party in the Developer Portal.
  3. A backend endpoint that generates RP signatures (see Backend setup).

Quick start (browser)

import { IDKit, orbLegacy } from "@worldcoin/idkit-core";

// 1. Initialize (call once on page load)
await IDKit.init();

// 2. Fetch an RP signature from your backend
const rpSig = await fetch("/api/rp-signature", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ action: "my-action" }),
}).then((r) => r.json());

// 3. Create a verification request
const request = await IDKit.request({
  app_id: "app_staging_xxxxx",
  action: "my-action",
  rp_context: {
    rp_id: "rp_xxxxx",
    nonce: rpSig.nonce,
    created_at: rpSig.created_at,
    expires_at: rpSig.expires_at,
    signature: rpSig.sig,
  },
  allow_legacy_proofs: true,
}).preset(orbLegacy({ signal: "user-123" }));

// 4. Display QR code for users to scan with World App
console.log("Scan this:", request.connectorURI);

// 5. Wait for the proof
const result = await request.pollForUpdates({
  pollInterval: 2000, // ms between polls
  timeout: 120_000, // 2 minute timeout
});

// 6. Send result directly to the Developer Portal v4 verify endpoint
const verification = await fetch("/api/verify-proof", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(result),
});
Use IDKit.init() in the browser and IDKit.initServer() in Node.js. Both are safe to call multiple times — initialization only happens once.

Backend setup

Your backend needs two endpoints: one to generate RP signatures, and one to verify proofs.

Generate RP signatures

The signRequest function must run server-side — it requires your RP signing key.
import { IDKit, signRequest } from "@worldcoin/idkit-core";

// Initialize for Node.js (call once at startup)
await IDKit.initServer();

const SIGNING_KEY = process.env.RP_SIGNING_KEY; // 32-byte hex private key

app.post("/api/rp-signature", (req, res) => {
  const { action } = req.body;
  const sig = signRequest(action, SIGNING_KEY);

  res.json({
    sig: sig.sig,
    nonce: sig.nonce,
    created_at: Number(sig.createdAt),
    expires_at: Number(sig.expiresAt),
  });
});
Never expose your RP signing key to client-side code. signRequest() will throw if called outside a server environment.

Verify proofs

The result object returned by pollForUpdates() is shaped exactly as the Developer Portal’s /v4/verify endpoint expects — just forward it directly:
app.post("/api/verify-proof", async (req, res) => {
  const proof = req.body;

  const response = await fetch(
    `https://developer.world.org/api/v4/verify/${RP_ID}`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(proof),
    },
  );

  const result = await response.json();
  res.status(response.ok ? 200 : 400).json(result);
});

Presets

Presets are the simplest way to create a request. Each preset configures the credential type and handles both v4 and v3 proof formats.
PresetCredentialFunction
Orb (Legacy)orborbLegacy({ signal? })
Secure Document (Legacy)secure_documentsecureDocumentLegacy({ signal? })
Document (Legacy)documentdocumentLegacy({ signal? })
import { IDKit, orbLegacy, secureDocumentLegacy } from "@worldcoin/idkit-core";

// Orb verification with a signal
const request = await IDKit.request({ ...config }).preset(
  orbLegacy({ signal: "user-123" }),
);

// Secure document verification
const request2 = await IDKit.request({ ...config }).preset(
  secureDocumentLegacy(),
);

Request configuration

Polling for results

Once you have a request, display connectorURI as a QR code and poll for the proof:
// request.connectorURI — show this as a QR code
// request.requestId    — unique ID for this verification

// Option A: Poll continuously (recommended)
const result = await request.pollForUpdates({
  pollInterval: 2000, // default: 1000ms
  timeout: 120_000, // default: 300_000ms (5 min)
  signal: abortController.signal, // optional cancellation
});

// Option B: Manual polling
const status = await request.pollOnce();
// status.type: "waiting_for_connection" | "awaiting_confirmation" | "confirmed" | "failed"
if (status.type === "confirmed") {
  console.log(status.result);
}

Response format

The result from pollForUpdates() can be forwarded directly to the Developer Portal /v4/verify endpoint — no transformation needed. The response shape depends on which World ID protocol version the user’s World App uses.

World ID v3 vs v4

World ID v3 is the current live protocol. World ID v4 is the new protocol shipping with this SDK. During the preview period, you will receive v3 proofs since World ID v4 has not launched yet. Once v4 rolls out, users with updated World Apps will produce v4 proofs instead. Set allow_legacy_proofs: true so your app works with both — the Developer Portal /v4/verify endpoint handles either format transparently.

V3 response (current — World ID 3.0)

interface IDKitResultV3 {
  protocol_version: "3.0";
  nonce: string;
  action?: string;
  responses: ResponseItemV3[];
}

interface ResponseItemV3 {
  identifier: string; // e.g. "orb", "face"
  proof: string; // ABI-encoded proof (hex)
  merkle_root: string; // Merkle root (hex)
  nullifier: string; // nullifier hash (hex)
  signal_hash?: string; // included if signal was provided
}

V4 response (upcoming — World ID 4.0)

interface IDKitResultV4 {
  protocol_version: "4.0";
  nonce: string;
  action: string;
  responses: ResponseItemV4[];
}

interface ResponseItemV4 {
  identifier: string; // e.g. "orb", "face", "document"
  proof: string[]; // compressed Groth16 proof + Merkle root
  nullifier: string; // RP-scoped nullifier (hex)
  signal_hash?: string; // included if signal was provided
  issuer_schema_id: number; // 1=orb, 2=face, 3=secure_document, 4=document, 5=device
  expires_at_min: number; // credential expiration (unix seconds)
}
You can check protocol_version on the result to determine which format you received.
The signal_hash field is returned as a convenience — it’s the hash of the signal you provided in the preset. You can also compute it yourself with hashSignal():
import { hashSignal } from "@worldcoin/idkit-core";
const hash = hashSignal("user-123"); // 0x-prefixed hex string