diff --git a/docs.json b/docs.json
index a132a98..a24a6af 100644
--- a/docs.json
+++ b/docs.json
@@ -63,6 +63,7 @@
"group": "IDKit",
"pages": [
"world-id/idkit/integrate",
+ "world-id/idkit/verification-flows",
"world-id/idkit/credentials",
"world-id/idkit/signatures",
"world-id/idkit/build-with-llms",
@@ -773,6 +774,10 @@
"source": "/world-id/id/idkit-v4-preview",
"destination": "/world-id/idkit/integrate"
},
+ {
+ "source": "/world-id/idkit/invite-code",
+ "destination": "/world-id/idkit/verification-flows#with-invite-code-mode"
+ },
{
"source": "/world-id/credentials/presets",
"destination": "/world-id/credentials#legacy-presets"
diff --git a/world-id/idkit/integrate.mdx b/world-id/idkit/integrate.mdx
index a4be0cd..86031d6 100644
--- a/world-id/idkit/integrate.mdx
+++ b/world-id/idkit/integrate.mdx
@@ -116,6 +116,13 @@ func handleRPSignature(w http.ResponseWriter, r *http.Request) {
+
+ The steps below cover the standard request flow. Depending on whether the user
+ already has World App and the credential you're requesting, they may be taken
+ through a different experience. See [Verification flows](/world-id/idkit/verification-flows)
+ for details.
+
+
# Step 4: Generate the connect URL and collect proof
You can test during development using the [simulator](https://simulator.worldcoin.org/) and setting `environment` to `"staging"`.
diff --git a/world-id/idkit/javascript.mdx b/world-id/idkit/javascript.mdx
index 8d82e55..2e7d823 100644
--- a/world-id/idkit/javascript.mdx
+++ b/world-id/idkit/javascript.mdx
@@ -1,5 +1,5 @@
---
-title: "Javascript"
+title: "JavaScript"
description: "Reference for `@worldcoin/idkit-core`"
"og:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png"
"twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png"
@@ -27,7 +27,8 @@ yarn add @worldcoin/idkit-core
## Entry points
- `IDKit.request(config)` for uniqueness proofs
-- `orbLegacy`, `secureDocumentLegacy`, `documentLegacy` for presets
+- `IDKit.requestWithInviteCode(config)` for invite-code mode
+- `orbLegacy`, `secureDocumentLegacy`, `documentLegacy`, `selfieCheckLegacy` for presets
Each entry point returns a builder. Finalize it with `.preset(...)`.
@@ -102,6 +103,64 @@ if (!completion.success) {
Outside World App, `connectorURI` is the URL you render as a QR code.
+## Invite-code mode
+
+Use `IDKit.requestWithInviteCode(config)` to display a short code instead of a QR. Validation, the returned `Status` shape, and the poll loop are identical to `IDKit.request`. See [Invite-code mode](/world-id/idkit/verification-flows#with-invite-code-mode) for when to use it.
+
+```ts
+import { IDKit, selfieCheckLegacy } from "@worldcoin/idkit-core";
+
+const request = await IDKit.requestWithInviteCode({
+ app_id: "app_xxxxx",
+ action: "my-action",
+ rp_context,
+ allow_legacy_proofs: true,
+}).preset(selfieCheckLegacy({ signal: "user-123" }));
+
+const code = request.code; // 6-character canonical form
+const expiresAt = request.expiresAt; // Unix seconds
+const completion = await request.pollUntilCompletion();
+```
+
+`IDKitInviteCodeRequest` exposes:
+
+- `code`
+- `expiresAt`
+- `requestId`
+- `pollOnce()`
+- `pollUntilCompletion({ pollInterval, timeout })`
+
+### Migrating from QR / connect-URL
+
+```ts
+// Before — QR / connect-URL flow
+const request = await IDKit.request({
+ app_id: "app_xxxxx",
+ action: "my-action",
+ rp_context,
+ allow_legacy_proofs: true,
+}).preset(orbLegacy({ signal: "user-123" }));
+
+const connectorURI = request.connectorURI; // render as QR
+const completion = await request.pollUntilCompletion();
+```
+
+```ts
+// After — invite-code mode
+const request = await IDKit.requestWithInviteCode({
+ app_id: "app_xxxxx",
+ action: "my-action",
+ rp_context,
+ allow_legacy_proofs: true,
+}).preset(selfieCheckLegacy({ signal: "user-123" }));
+
+const code = request.code; // display to user
+const expiresAt = request.expiresAt; // drive a countdown
+const completion = await request.pollUntilCompletion();
+```
+
+The config object is unchanged. Replace `connectorURI` (QR) with `code` and `expiresAt` in your UI. Polling, proof verification, and nullifier storage stay the same.
+
## Server-side helpers
Use subpath exports on your backend:
diff --git a/world-id/idkit/onchain-verification.mdx b/world-id/idkit/onchain-verification.mdx
index f10a369..2074fb9 100644
--- a/world-id/idkit/onchain-verification.mdx
+++ b/world-id/idkit/onchain-verification.mdx
@@ -1,5 +1,5 @@
---
-title: "On-chain verification"
+title: "On-chain Verification"
description: "Verify World ID proofs directly in Solidity for web3-native flows."
"og:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png"
"twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png"
diff --git a/world-id/idkit/react.mdx b/world-id/idkit/react.mdx
index 872f3d8..2f96234 100644
--- a/world-id/idkit/react.mdx
+++ b/world-id/idkit/react.mdx
@@ -116,6 +116,77 @@ Hook result fields:
- `result`
- `errorCode`
+## Invite-code mode
+
+For invite-code flows, use `IDKitInviteCodeRequestWidget` (controlled) or `useIDKitInviteCodeRequest` (headless). Config matches `IDKitRequestWidget` / `useIDKitRequest` — invite-code mode adds no new required fields. See [Invite-code mode](/world-id/idkit/verification-flows#with-invite-code-mode) for when to use it.
+
+```tsx
+import { IDKitInviteCodeRequestWidget, selfieCheckLegacy } from "@worldcoin/idkit";
+
+ { /* ... */ }}
+ onSuccess={() => { /* ... */ }}
+/>;
+```
+
+`useIDKitInviteCodeRequest` result fields (sibling of `useIDKitRequest`'s):
+
+- `open()`
+- `reset()`
+- `isAwaitingUserConnection`
+- `isAwaitingUserConfirmation`
+- `isSuccess`
+- `isError`
+- `code`
+- `codeExpiresAt`
+- `result`
+- `errorCode`
+
+### Migrating from QR / connect-URL
+
+```tsx
+// Before — QR / connect-URL widget
+import { IDKitRequestWidget, orbLegacy } from "@worldcoin/idkit";
+
+;
+```
+
+```tsx
+// After — invite-code widget
+import { IDKitInviteCodeRequestWidget, selfieCheckLegacy } from "@worldcoin/idkit";
+
+;
+```
+
+Swap the component and preset. Props, callbacks, and backend verification stay the same. The headless equivalent is `useIDKitInviteCodeRequest` in place of `useIDKitRequest`.
+
## Presets
React hooks/widgets take `preset` directly in config.
diff --git a/world-id/idkit/swift.mdx b/world-id/idkit/swift.mdx
index b74f87b..5c8ed56 100644
--- a/world-id/idkit/swift.mdx
+++ b/world-id/idkit/swift.mdx
@@ -75,3 +75,47 @@ case .failure(let error):
print(error)
}
```
+
+## Invite-code mode
+
+Use `presetWithInviteCode(_:)` on the builder to return an `IDKitInviteCodeRequest` instead of `IDKitRequest`. The polling surface is identical. See [Invite-code mode](/world-id/idkit/verification-flows#with-invite-code-mode) for when to use it.
+
+```swift
+let request = try IDKit.request(config: config)
+ .presetWithInviteCode(selfieCheckLegacy(signal: "user-123"))
+
+let code = request.code // 6-character canonical form
+let expiresAt = request.expiresAt // Date
+let completion = await request.pollUntilCompletion()
+```
+
+### Migrating from QR / connect-URL
+
+```swift
+// Before — QR / connect-URL flow
+let request = try IDKit.request(config: config)
+ .preset(orbLegacy(signal: "user-123"))
+
+let connectURL = request.connectorURL // render as QR
+let completion = await request.pollUntilCompletion()
+```
+
+```swift
+// After — invite-code mode
+let request = try IDKit.request(config: config)
+ .presetWithInviteCode(selfieCheckLegacy(signal: "user-123"))
+
+let code = request.code // display to user
+let expiresAt = request.expiresAt // drive a countdown
+let completion = await request.pollUntilCompletion()
+```
+
+The config object is unchanged. Replace `connectorURL` (QR) with `code` and `expiresAt` in your UI. Polling, proof verification, and nullifier storage stay the same.
+
+`IDKitInviteCodeRequest` exposes:
+
+- `code: String`
+- `expiresAt: Date`
+- `requestID: UUID`
+- `pollStatusOnce() async -> IDKitStatus`
+- `pollUntilCompletion(options:) async -> IDKitCompletionResult`
diff --git a/world-id/idkit/verification-flows.mdx b/world-id/idkit/verification-flows.mdx
new file mode 100644
index 0000000..0db3e4c
--- /dev/null
+++ b/world-id/idkit/verification-flows.mdx
@@ -0,0 +1,232 @@
+---
+title: "Verification Flows"
+description: "Understand the three verification paths a user can take when your app requests a World ID proof — hot, warm, and cold."
+"og:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png"
+"twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png"
+---
+
+When your app requests a World ID proof, the user is taken through one of three flows based on two factors: whether they have World App installed, and whether they already hold the credential you're requesting.
+
+| Flow | World App installed | Has credential | What happens |
+| --- | --- | --- | --- |
+| Hot | Yes | Yes | World App opens, displays a proof consent, and the user approves. Typically under 10 seconds. |
+| Warm | Yes | No | World App opens and walks the user through credential enrollment (e.g., PoH, Face, or NFC). Once issued, a proof consent appears and the user approves. |
+| Cold | No | No | The user must install World App first. The experience differs by platform — see below. |
+
+## Cold flow
+
+The cold flow requires the most steps and works differently on each platform because of how each platform handles **deferred deep linking** — the ability to preserve a link's context through an app store install so the app can act on it at first launch.
+
+### Android
+
+Android supports deferred deep linking through the Play Store. After the user downloads World App, the original verification context is carried forward at first launch — World App picks up where the user left off and routes them directly into credential enrollment. No re-trigger from your app is needed.
+
+```mermaid
+sequenceDiagram
+ participant App as Your App
+ participant User
+ participant Play as Play Store
+ participant WA as World App
+
+ App->>User: IDKit request (connect URL / QR)
+ User->>Play: Download World App
+ Play-->>WA: Deferred deep link preserved
+ WA->>User: Account onboarding
+ WA->>User: Credential enrollment
+ WA->>User: Proof consent
+ User->>WA: Approve
+ WA-->>App: Proof returned
+```
+
+### iOS
+
+iOS does not support deferred deep linking through the App Store. The original verification context is lost during install, so additional mechanisms are needed to resume the flow.
+
+#### Default behavior
+
+1. The user downloads World App from the App Store and completes account creation onboarding.
+2. Your app triggers an IDKit verification request.
+3. World App opens and takes the user through a hot or warm flow.
+4. The proof consent appears, the user approves, and the proof is returned to your app.
+
+Because iOS does not preserve context through the App Store install, your app should direct the user to install World App before issuing the IDKit request.
+
+```mermaid
+sequenceDiagram
+ participant App as Your App
+ participant User
+ participant Store as App Store
+ participant WA as World App
+
+ App->>User: Prompt to install World App
+ User->>Store: Download World App
+ WA->>User: Account onboarding
+ User->>App: Return to your app
+ App->>User: IDKit request (connect URL / QR)
+ WA->>User: Hot or warm flow
+ WA-->>App: Proof returned
+```
+
+#### With invite-code mode
+
+Invite-code mode displays a short 6-character code in your app that the user enters into World App. World App treats the code as an entry point to the in-app onboarding flows the user needs to complete in order to satisfy your IDKit request, then returns the proof.
+
+
+ Invite-code mode only applies to iOS as Android preserves deferred deep linking context via the Play Store.
+
+
+
+ Only the `selfieCheckLegacy` preset is supported today.
+
+
+1. Your app triggers an IDKit invite-code request.
+2. Your app opens the URL that IDKit provides. A landing page displays the invite code and a QR code for the request.
+3. The user downloads World App from the App Store.
+4. The user opens World App and completes account creation onboarding.
+5. The user enters the invite code.
+6. World App resolves the code, picks up the original verification context, and walks the user through credential enrollment.
+7. The proof consent appears, the user approves, and the proof is returned to your app.
+
+The 6-character code persists across the App Store install, so once World App is installed and onboarded the user can resume the verification flow without returning to your app first. This also covers cross-device scenarios (e.g., a desktop browser displaying the QR for the user's phone) where deep linking cannot carry context.
+
+```mermaid
+sequenceDiagram
+ participant App as Your App
+ participant User
+ participant LP as Landing Page
+ participant Store as App Store
+ participant WA as World App
+
+ App->>LP: Open IDKit-provided URL
+ LP->>User: Display invite code + QR
+ User->>Store: Download World App
+ WA->>User: Account onboarding
+ User->>WA: Enter invite code
+ WA->>WA: Resolve code & restore context
+ WA->>User: Credential enrollment
+ WA->>User: Proof consent
+ User->>WA: Approve
+ WA-->>App: Proof returned
+```
+
+##### Lifecycle
+
+- Codes expire after a short TTL (currently ten minutes).
+- Codes are one-shot — once redeemed, they cannot be reused. Re-running the request returns a fresh code with a fresh TTL.
+- After the user redeems the code, your existing poll loop receives the proof exactly as it does in QR mode.
+
+##### Integrate
+
+The setup steps that precede the request — creating an app in the Developer Portal and generating an RP signature on your backend — are identical to the [standard integration](/world-id/idkit/integrate). Only the request call and the rendered UI change.
+
+
+
+```typescript
+import { IDKit, selfieCheckLegacy } from "@worldcoin/idkit-core";
+
+// `rpSig` is fetched from your backend — see the standard integration guide.
+const request = await IDKit.requestWithInviteCode({
+ app_id: "app_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,
+ environment: "production",
+}).preset(selfieCheckLegacy({ signal: "user-123" }));
+
+const code = request.code; // 6-character canonical form
+const expiresAt = request.expiresAt; // Unix seconds
+
+const completion = await request.pollUntilCompletion();
+```
+
+```tsx
+import {
+ IDKitInviteCodeRequestWidget,
+ selfieCheckLegacy,
+ type RpContext,
+} from "@worldcoin/idkit";
+
+const rpContext: RpContext = {
+ rp_id: "rp_xxxxx",
+ nonce: rpSig.nonce,
+ created_at: rpSig.created_at,
+ expires_at: rpSig.expires_at,
+ signature: rpSig.sig,
+};
+
+ {
+ const response = await fetch("/api/verify-proof", {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({ rp_id: rpContext.rp_id, idkitResponse: result }),
+ });
+ if (!response.ok) throw new Error("Backend verification failed");
+ }}
+ onSuccess={() => {
+ // Update app state here.
+ }}
+/>;
+```
+
+```swift
+import IDKit
+
+// `rpSig` is fetched from your backend — see the standard integration guide.
+let rpContext = try RpContext(
+ rpId: "rp_xxxxx",
+ nonce: rpSig.nonce,
+ createdAt: rpSig.createdAt,
+ expiresAt: rpSig.expiresAt,
+ signature: rpSig.sig
+)
+
+let config = IDKitRequestConfig(
+ appId: "app_xxxxx",
+ action: "my-action",
+ rpContext: rpContext,
+ allowLegacyProofs: true,
+ environment: .production
+)
+
+let request = try IDKit.request(config: config)
+ .presetWithInviteCode(selfieCheckLegacy(signal: "user-123"))
+
+let code = request.code // 6-character canonical form
+let expiresAt = request.expiresAt // Date
+
+let completion = await request.pollUntilCompletion()
+```
+
+
+
+
+ Generate `rp_context` in your backend only. Never expose your RP signing key in client code.
+
+
+The config object is the same one you pass to `IDKit.request(...)` — invite-code mode introduces no new required fields.
+
+Set `allow_legacy_proofs: true` because `selfieCheckLegacy` is a v3 ("legacy") preset; the flag lets World App accept v3 proofs to satisfy the request. When invite-code mode adds support for v4 presets, set it to `false` for those flows.
+
+Polling and proof verification are unchanged from QR mode: the same `Status` values are emitted and the same `IDKitCompletionResult` is returned. Forward the result payload as-is to `POST https://developer.world.org/api/v4/verify/{rp_id}` — see [Verify the proof in your backend](/world-id/idkit/integrate#step-5-verify-the-proof-in-your-backend).
+
+##### SDK references
+
+For the full surface — entry points, hook results, and type signatures — see the per-SDK reference:
+
+- [JavaScript](/world-id/idkit/javascript#invite-code-mode)
+- [React](/world-id/idkit/react#invite-code-mode)
+- [Swift](/world-id/idkit/swift#invite-code-mode)