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.
If you pass environment: "staging" in your IDKit request, use the Simulator to scan QR codes and complete verifications - no real World App needed.

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_xxxxx",
  action: "my-action",
  environment: "staging",
  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 (never throws - returns a discriminated union)
const completion = await request.pollUntilCompletion({
  pollInterval: 2000, // ms between polls
  timeout: 120_000, // 2 minute timeout
});

if (!completion.success) {
  console.error("Verification failed:", completion.error);
  // completion.error is an IDKitErrorCodes value, e.g. "user_rejected", "timeout"
  return;
}

// 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(completion.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 inside a successful pollUntilCompletion() response 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:
// Option A: Poll continuously (recommended)
const completion = await request.pollUntilCompletion({
  pollInterval: 2000, // default: 1000ms
  timeout: 120_000, // default: 300_000ms (5 min)
  signal: abortController.signal, // optional cancellation
});

if (completion.success) {
  console.log(completion.result); // IDKitResult - forward to your backend
} else {
  console.error(completion.error); // IDKitErrorCodes value
}

// 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);
}

Error handling

pollUntilCompletion() never throws. It returns a discriminated union:
type IDKitCompletionResult =
  | { success: true; result: IDKitResult }
  | { success: false; error: IDKitErrorCodes };
You can match against specific error codes:
import { IDKitErrorCodes } from "@worldcoin/idkit-core";

const completion = await request.pollUntilCompletion();

if (!completion.success) {
  switch (completion.error) {
    case IDKitErrorCodes.UserRejected:
      console.log("User declined in World App");
      break;
    case IDKitErrorCodes.Timeout:
      console.log("Verification timed out");
      break;
    case IDKitErrorCodes.Cancelled:
      console.log("Request was cancelled via AbortSignal");
      break;
    default:
      console.error("Verification failed:", completion.error);
  }
  return;
}

// completion.result is the IDKitResult - forward to your backend

Error codes

CodeDescription
user_rejectedUser declined the request in World App
verification_rejectedVerification rejected (legacy, replaced by user_rejected)
credential_unavailableRequested credential is not available
malformed_requestRequest was malformed
invalid_networkInvalid network
inclusion_proof_pendingInclusion proof is still being generated
inclusion_proof_failedInclusion proof generation failed
unexpected_responseUnexpected response from World App
connection_failedFailed to connect to World App
max_verifications_reachedUser has already verified for this action
failed_by_host_appVerification failed by host app
generic_errorAn unspecified error occurred
timeoutClient-side: polling timed out
cancelledClient-side: request cancelled via AbortSignal

Response format

On success, completion.result 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"
  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"
  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; // credential issuer schema ID
  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