From e360d252bdeda1efecade7b0f6763dca0e431b2a Mon Sep 17 00:00:00 2001 From: mehmetali <36207866+realmehmetali@users.noreply.github.com> Date: Sat, 18 Jul 2026 01:26:39 -0700 Subject: [PATCH] docs: require a user agent for proof verification --- openapi/developer-portal.json | 10 ++++++++++ world-id/SKILL.md | 3 ++- world-id/from-idkit-standalone.mdx | 5 ++++- world-id/idkit/integrate.mdx | 9 +++++++-- world-id/idkit/integration-prompt.mdx | 2 +- 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/openapi/developer-portal.json b/openapi/developer-portal.json index 7113d22..7f66c9b 100644 --- a/openapi/developer-portal.json +++ b/openapi/developer-portal.json @@ -695,6 +695,16 @@ "type": "string" }, "description": "RP ID (`rp_...`) is recommended. App ID (`app_...`) is also accepted for backward compatibility." + }, + { + "name": "User-Agent", + "in": "header", + "required": true, + "schema": { + "type": "string", + "example": "my-world-id-backend/1.0" + }, + "description": "Identifies the backend client. Requests without a User-Agent may be rejected by bot protection." } ], "requestBody": { diff --git a/world-id/SKILL.md b/world-id/SKILL.md index e09449c..8377130 100644 --- a/world-id/SKILL.md +++ b/world-id/SKILL.md @@ -145,7 +145,7 @@ The full code for each step is at [https://docs.world.org/world-id/idkit/integra 2. **Create or reuse the Portal resources.** Use the MCP when available. Reuse an existing app, RP, and action when they match the requested integration. For a new RP, capture `app_id`, `rp_id`, and `signing_key.private_key` from `configure_world_id`, create the action in the intended environment, and write the signing key to the prepared server-only secret store in the same step. The portal returns it exactly once. **Do not print, log, or return the private key to chat.** If the key is lost, explain that `get_world_id_signing_key` cannot recover it; rotation creates a new key and invalidates the old signer. 3. **Generate the RP signature in your backend.** *Why backend?* The signing key authenticates your app to the protocol. Leaking it lets anyone impersonate your app and forge proof requests. **CRITICAL: never sign on the client. Never expose `RP_SIGNING_KEY` as a `NEXT_PUBLIC_*` var. Never log it.** 4. **Open the IDKit widget on the client** with the signature your backend returned. The widget hands off to World ID, which produces a zero-knowledge proof. -5. **Verify the proof in your backend** by POSTing it **as-is** to `https://developer.world.org/api/v4/verify/{rp_id}`. *Why backend?* A client can return any JSON it wants. Only the World verifier — called from a trusted server — confirms the proof is real and tied to a unique credential. Verifying client-side defeats the entire point. **DO NOT mutate, re-encode, or trim the proof JSON before forwarding** — pass exactly what IDKit returned. +5. **Verify the proof in your backend** by POSTing it **as-is** to `https://developer.world.org/api/v4/verify/{rp_id}` with a descriptive `User-Agent` header (for example, `my-world-id-backend/1.0`). *Why backend?* A client can return any JSON it wants. Only the World verifier — called from a trusted server — confirms the proof is real and tied to a unique credential. Verifying client-side defeats the entire point. **DO NOT mutate, re-encode, or trim the proof JSON before forwarding** — pass exactly what IDKit returned. Requests without a `User-Agent` may be rejected by bot protection. 6. **Store the nullifier.** Every successful proof returns a `nullifier` — an RP-scoped, action-scoped, non-reversible identifier for that user. *Why store it?* Without uniqueness storage, a user can verify the same proof twice and double-claim a reward, vote, etc. Persist `(action, nullifier)` with a `UNIQUE` constraint and reject duplicates on insert. Column type: **`NUMERIC(78, 0)`** (256-bit field elements). The nullifier reveals nothing about the user — safe to store, but it's the *only* anti-replay mechanism, so it's required. ## Phase 5 — Match environments end-to-end @@ -181,6 +181,7 @@ Surface these proactively when you see the matching symptom — don't make the u |---|---|---| | Face Check appears unresponsive or never starts | The app may not be enabled for Face Check | Read `enable_face_check` through MCP when available or `/api/v1/precheck/{app_id}`. If false, stop and explain how to request access. | | World ID shows "action not found" or QR scan does nothing | Action wasn't created in the environment IDKit is pointing at | Create the missing action with `create_world_id_action` (`environment: "production"` for real devices, `"staging"` for simulator). Confirm `NEXT_PUBLIC_WLD_ENVIRONMENT` matches. | +| `/api/v4/verify/{rp_id}` returns a bot-protection `403` | The backend request has no `User-Agent` header | Add a descriptive `User-Agent`, such as `my-world-id-backend/1.0`, and retry. | | `/api/v4/verify/{rp_id}` returns `invalid_proof` or `verification_failed` | Often staging/production env mismatch, or proof was mutated before forward | Re-check Phase 5. Forward the proof JSON byte-for-byte without re-encoding fields. | | Verification fails in JS/React and the error code alone isn't enough | Need transport/payload diagnostics | Read `getDebugReport()` (or the `onError` `debugReport` arg) — it carries `transport`, `request_id`, request/response payloads, and World App `mini_app` channel info. JS SDKs only. | | `/api/v4/verify/{rp_id}` returns `not_registered` or 4xx with `rp` errors | On-chain registration is still `pending` | Poll `get_world_id_registration_status` until the intended environment is `registered`. Production must be registered before launch. | diff --git a/world-id/from-idkit-standalone.mdx b/world-id/from-idkit-standalone.mdx index c1819f3..568568c 100644 --- a/world-id/from-idkit-standalone.mdx +++ b/world-id/from-idkit-standalone.mdx @@ -159,7 +159,10 @@ export async function POST(request: Request): Promise { `https://developer.world.org/api/v4/verify/${process.env.WORLD_ID_RP_ID}`, { method: "POST", - headers: { "content-type": "application/json" }, + headers: { + "content-type": "application/json", + "user-agent": "my-world-id-backend/1.0", + }, body: JSON.stringify(idkitResponse), }, ); diff --git a/world-id/idkit/integrate.mdx b/world-id/idkit/integrate.mdx index 5480ab4..e8a827c 100644 --- a/world-id/idkit/integrate.mdx +++ b/world-id/idkit/integrate.mdx @@ -308,7 +308,9 @@ After 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. + Forward the IDKit result payload as-is. No field remapping is required. Include + a descriptive `User-Agent` header; requests without one may be rejected with a + `403` by bot protection. ```typescript title="app/api/verify-proof/route.ts" @@ -325,7 +327,10 @@ export async function POST(request: Request): Promise { `https://developer.world.org/api/v4/verify/${rp_id}`, { method: "POST", - headers: { "content-type": "application/json" }, + headers: { + "content-type": "application/json", + "user-agent": "my-world-id-backend/1.0", + }, body: JSON.stringify(idkitResponse), }, ); diff --git a/world-id/idkit/integration-prompt.mdx b/world-id/idkit/integration-prompt.mdx index dc67758..f852ebf 100644 --- a/world-id/idkit/integration-prompt.mdx +++ b/world-id/idkit/integration-prompt.mdx @@ -33,7 +33,7 @@ Integrate World ID into my project using IDKit. Here are my app details: 4. On success, send the IDKit result to my backend. The backend should forward the payload as-is to: POST https://developer.world.org/api/v4/verify/{rp_id} - No field remapping is needed. + No field remapping is needed. Include a descriptive User-Agent header (for example, my-world-id-backend/1.0); requests without one may be rejected by bot protection. ## Reference - Full docs: https://docs.world.org/llms.txt