Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"johndoe",
"keccak",
"keypair",
"nullifiers",
"kotlinx",
"Lazo",
"listas",
Expand Down Expand Up @@ -149,13 +150,15 @@
"sybil",
"SYNCMODE",
"tamperable",
"TIMESTAMPTZ",
"testnet",
"testnets",
"thirdweb",
"touchpoints",
"tunnelmole",
"tute",
"uniffi",
"unlinkable",
"unpackedProof",
"urlencode",
"userop",
Expand Down
1 change: 1 addition & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"group": "IDKit",
"pages": [
"world-id/idkit/integrate",
"world-id/idkit/signatures",
"world-id/idkit/build-with-llms",
"world-id/idkit/onchain-verification",
"world-id/idkit/design-guidelines",
Expand Down
55 changes: 51 additions & 4 deletions world-id/idkit/go.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,18 @@ go get github.com/worldcoin/idkit/go/idkit@latest

## Generate RP signature

Use `idkit.SignRequestWithTTL` instead to configure the signature's expiration time (default is 5 minutes).
### One-shot signing

Use `SignRequest` with functional options for the simplest integration:

```go
import "github.com/worldcoin/idkit/go/idkit"

sig, err := idkit.SignRequest(os.Getenv("RP_SIGNING_KEY"))
// For uniqueness proofs: include the action
sig, err := idkit.SignRequest(
os.Getenv("RP_SIGNING_KEY"),
idkit.WithAction("my-action"),
)
if err != nil {
// handle error
}
Expand All @@ -35,11 +41,52 @@ rpContext := map[string]any{
}
```

### Reusable signer

For high-throughput backends, create a `Signer` once and reuse it. This parses the key upfront and avoids repeated allocations.

```go
signer, err := idkit.NewSigner(os.Getenv("RP_SIGNING_KEY"))
if err != nil {
log.Fatal(err)
}

// Use in your request handler
sig, err := signer.SignRequest(
idkit.WithAction("my-action"),
idkit.WithTTL(600), // optional, default 300s
)
```

## API

- `SignRequest(signingKeyHex)`
- `SignRequestWithTTL(signingKeyHex, ttl)`
### Functions

| Function | Description |
|----------|-------------|
| `SignRequest(signingKeyHex, opts...)` | One-shot signing with options |
| `SignRequestWithTTL(signingKeyHex, ttl)` | Convenience wrapper with custom TTL |
| `NewSigner(signingKeyHex)` | Creates a reusable `Signer` from a hex key |

### Options

| Option | Description |
|--------|-------------|
| `WithAction(action)` | Hashes and appends the action to the signed payload (required for uniqueness proofs) |
| `WithTTL(ttl)` | Overrides the default 300-second TTL |

### `RpSignature`

```go
type RpSignature struct {
Sig string `json:"sig"` // 0x-prefixed, 65-byte hex
Nonce string `json:"nonce"` // 0x-prefixed, 32-byte field element
CreatedAt uint64 `json:"created_at"` // Unix seconds
ExpiresAt uint64 `json:"expires_at"` // Unix seconds
}
```

## Related pages

- [RP Signatures](/world-id/idkit/signatures) — algorithm details, pseudocode, and test vectors
- [Integrate IDKit](/world-id/idkit/integrate)
53 changes: 44 additions & 9 deletions world-id/idkit/integrate.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,15 @@ Signatures verify that proof requests genuinely come from your app, preventing a
<CodeGroup title="Generate RP signature">
```typescript title="JavaScript"
import { NextResponse } from "next/server";
import { signRequest } from "@worldcoin/idkit/signing";
import { signRequest } from "@worldcoin/idkit-core/signing";

export async function POST(request: Request): Promise<Response> {
const { action } = await request.json();
const signingKey = process.env.RP_SIGNING_KEY!;

const { sig, nonce, createdAt, expiresAt } = signRequest(action, signingKey);
const { sig, nonce, createdAt, expiresAt } = signRequest({
signingKeyHex: process.env.RP_SIGNING_KEY!,
action,
});

return NextResponse.json({
sig,
Expand Down Expand Up @@ -81,7 +83,15 @@ func handleRPSignature(w http.ResponseWriter, r *http.Request) {
return
}

sig, err := idkit.SignRequest(os.Getenv("RP_SIGNING_KEY"))
var body struct {
Action string `json:"action"`
}
_ = json.NewDecoder(r.Body).Decode(&body)

sig, err := idkit.SignRequest(
os.Getenv("RP_SIGNING_KEY"),
idkit.WithAction(body.Action),
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
Expand All @@ -105,7 +115,7 @@ func handleRPSignature(w http.ResponseWriter, r *http.Request) {


# 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"`.
You can test during development using the [simulator](https://simulator.worldcoin.org/) and setting `environment` to `"staging"`.

<CodeGroup title="Create request and collect proof">
```typescript title="JavaScript"
Expand All @@ -122,7 +132,7 @@ const request = await IDKit.request({
app_id: "app_xxxxx",
// Action: Context that scopes what the user is proving uniqueness for
// e.g., "verify-account-2026" or "claim-airdrop-2026".
action: "my-action",
action: "my-action",
rp_context: {
rp_id: "rp_xxxxx", // Your app's `rp_id` from the Developer Portal
nonce: rpSig.nonce,
Expand All @@ -132,7 +142,8 @@ const request = await IDKit.request({
},
allow_legacy_proofs: true,
environment: "production", // Only set this to staging for testing with the simulator
// Signal (optional): Bind specific context into the requested proof.
return_to: "myapp://verify-done", // Optional: mobile deep-link callback URL
// Signal (optional): Bind specific context into the requested proof.
// Examples: user ID, wallet address. Your backend should enforce the same value.
}).preset(orbLegacy({ signal: "local-election-1" }));

Expand Down Expand Up @@ -300,11 +311,33 @@ export async function POST(request: Request): Promise<Response> {
},
);

const payload = await response.json();
return NextResponse.json(payload, { status: response.status });
if (!response.ok) {
return NextResponse.json({ error: "Verification failed" }, { status: 400 });
}

// Proof is valid — now store the nullifier (see Step 6)
return NextResponse.json({ success: true });
}
```

# Step 6: Store the nullifier

Every World ID proof contains a nullifier — a value derived from the user's World ID, your app, and the action. The same person verifying the same action always produces the same nullifier, but different apps or actions produce different ones — making nullifiers unlinkable across apps.

The Developer Portal confirms the proof is **cryptographically valid**, but your backend must check that the nullifier hasn't been used before. Without this, the same person could verify multiple times for the same action.

Nullifiers are returned as 0x-prefixed hex strings representing 256-bit integers. We recommend converting and storing them as numbers to avoid parsing and casing issues that can lead to security vulnerabilities. For example, PostgreSQL doesn't natively support 256-bit integers, instead you can convert to the nullifier to a decimal and store it as `NUMERIC(78, 0)`.

<CodeGroup>
```sql title="PostgreSQL schema"
CREATE TABLE nullifiers (
nullifier NUMERIC(78, 0) NOT NULL,
action TEXT NOT NULL,
verified_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (nullifier, action)
);
```
</CodeGroup>

## Architecture overview

Expand All @@ -328,9 +361,11 @@ sequenceDiagram
Client->>Backend: 6. Forward proof payload
Backend->>Portal: POST /v4/verify/{rp_id}
Portal-->>Backend: Verification result
Backend->>Backend: 7. Check & store nullifier
Backend-->>Client: Success / failure
```

## Next pages

- [RP Signatures](/world-id/idkit/signatures) — algorithm details, pseudocode, and test vectors
- [POST /v4/verify reference](/api-reference/developer-portal/verify)
17 changes: 9 additions & 8 deletions world-id/idkit/javascript.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,10 @@ const builder = IDKit.request({
expires_at: 1735689900,
signature: "0x...",
},
action_description: "Verify user",
bridge_url: undefined,
allow_legacy_proofs: true,
override_connect_base_url: undefined,
environment: "production", // Only set this to staging for testing with the simulator
return_to: "myapp://verify-done", // Optional: mobile deep-link callback URL
bridge_url: undefined, // Optional: custom bridge URL
});
```

Expand Down Expand Up @@ -111,19 +110,21 @@ Use subpath exports on your backend:
import { signRequest } from "@worldcoin/idkit-core/signing";
import { hashSignal } from "@worldcoin/idkit-core/hashing";

const { sig, nonce, createdAt, expiresAt } = signRequest(
"my-action",
process.env.RP_SIGNING_KEY!,
);
const { sig, nonce, createdAt, expiresAt } = signRequest({
signingKeyHex: process.env.RP_SIGNING_KEY!,
action: "my-action",
ttl: 300, // optional, default 300s
});

const signalHash = hashSignal("user-123");
```

`signRequest` should only run in trusted server environments.
`signRequest` should only run in trusted server environments. See [RP Signatures](/world-id/idkit/signatures) for the full algorithm and test vectors.

## Related pages

- [Getting started](/world-id/idkit/integrate)
- [RP Signatures](/world-id/idkit/signatures)
- [React](/world-id/idkit/react)
- [Error Codes](/world-id/idkit/error-codes)
- [POST /v4/verify reference](/api-reference/developer-portal/verify)
4 changes: 1 addition & 3 deletions world-id/idkit/kotlin.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,8 @@ val config = IDKitRequestConfig(
appId = "app_xxxxx",
action = "my-action",
rpContext = rpContext,
actionDescription = "Verify user",
bridgeUrl = null,
allowLegacyProofs = true,
overrideConnectBaseUrl = null,
returnTo = "myapp://verify-done", // Optional: mobile deep-link callback URL
environment = Environment.PRODUCTION,
)

Expand Down
2 changes: 2 additions & 0 deletions world-id/idkit/react.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,5 @@ For RP signature generation in React/Next.js apps, use the pure JS subpath:
```ts
import { signRequest } from "@worldcoin/idkit/signing";
```

See [RP Signatures](/world-id/idkit/signatures) for the full algorithm and test vectors.
Loading
Loading