Skip to content

Commit dc44b1a

Browse files
committed
docs: require a user agent for proof verification
1 parent 7c0a7a3 commit dc44b1a

5 files changed

Lines changed: 24 additions & 5 deletions

File tree

openapi/developer-portal.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,16 @@
695695
"type": "string"
696696
},
697697
"description": "RP ID (`rp_...`) is recommended. App ID (`app_...`) is also accepted for backward compatibility."
698+
},
699+
{
700+
"name": "User-Agent",
701+
"in": "header",
702+
"required": true,
703+
"schema": {
704+
"type": "string",
705+
"example": "my-world-id-backend/1.0"
706+
},
707+
"description": "Identifies the backend client. Requests without a User-Agent may be rejected by bot protection."
698708
}
699709
],
700710
"requestBody": {

world-id/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ The full code for each step is at [https://docs.world.org/world-id/idkit/integra
145145
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.
146146
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.**
147147
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.
148-
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.
148+
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.
149149
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 — it's safe to store, but it's the *only* anti-replay mechanism, so it's required.
150150

151151
## Phase 5 — Match environments end-to-end
@@ -181,6 +181,7 @@ Surface these proactively when you see the corresponding symptom — don't make
181181
|---|---|---|
182182
| 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. |
183183
| 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. |
184+
| `/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. |
184185
| `/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. |
185186
| 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. |
186187
| `/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. |

world-id/from-idkit-standalone.mdx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,10 @@ export async function POST(request: Request): Promise<Response> {
168168
`https://developer.world.org/api/v4/verify/${process.env.WORLD_ID_RP_ID}`,
169169
{
170170
method: "POST",
171-
headers: { "content-type": "application/json" },
171+
headers: {
172+
"content-type": "application/json",
173+
"user-agent": "my-world-id-backend/1.0",
174+
},
172175
body: JSON.stringify(idkitResponse),
173176
},
174177
);

world-id/idkit/integrate.mdx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,9 @@ After successful completion, send the returned payload to your backend and
308308
forward it directly to: `POST https://developer.world.org/api/v4/verify/{rp_id}`
309309

310310
<Note>
311-
Forward the IDKit result payload as-is. No field remapping is required.
311+
Forward the IDKit result payload as-is. No field remapping is required. Include
312+
a descriptive `User-Agent` header; requests without one may be rejected with a
313+
`403` by bot protection.
312314
</Note>
313315

314316
```typescript title="app/api/verify-proof/route.ts"
@@ -325,7 +327,10 @@ export async function POST(request: Request): Promise<Response> {
325327
`https://developer.world.org/api/v4/verify/${rp_id}`,
326328
{
327329
method: "POST",
328-
headers: { "content-type": "application/json" },
330+
headers: {
331+
"content-type": "application/json",
332+
"user-agent": "my-world-id-backend/1.0",
333+
},
329334
body: JSON.stringify(idkitResponse),
330335
},
331336
);

world-id/idkit/integration-prompt.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Integrate World ID into my project using IDKit. Here are my app details:
3333
3434
4. On success, send the IDKit result to my backend.
3535
The backend should forward the payload as-is to: POST https://developer.world.org/api/v4/verify/{rp_id}
36-
No field remapping is needed.
36+
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.
3737
3838
## Reference
3939
- Full docs: https://docs.world.org/llms.txt

0 commit comments

Comments
 (0)