From 3c397847db8bb48bc22f59596df9a928cee2412d Mon Sep 17 00:00:00 2001 From: Carlo Mazzaferro Date: Thu, 5 Feb 2026 13:25:22 +0100 Subject: [PATCH 1/6] docs for credential refresh / issuance for PoH --- docs.json | 5 + world-id/concepts.mdx | 6 + world-id/credential-issuance/refresh.mdx | 187 +++++++++++++++++++++++ world-id/reference/credential.mdx | 128 ++++++++++++++++ 4 files changed, 326 insertions(+) create mode 100644 world-id/credential-issuance/refresh.mdx create mode 100644 world-id/reference/credential.mdx diff --git a/docs.json b/docs.json index 07d2423..a89dbec 100644 --- a/docs.json +++ b/docs.json @@ -171,10 +171,15 @@ "group": "Sign in with World ID", "pages": ["world-id/sign-in/oidc"] }, + { + "group": "Credential Issuance", + "pages": ["world-id/credential-issuance/refresh"] + }, { "group": "Technical Reference", "pages": [ "world-id/reference/idkit", + "world-id/reference/credential", "world-id/reference/api", "world-id/reference/sign-in", "world-id/reference/contracts", diff --git a/world-id/concepts.mdx b/world-id/concepts.mdx index 58ade78..0910b81 100644 --- a/world-id/concepts.mdx +++ b/world-id/concepts.mdx @@ -28,11 +28,17 @@ Some terms are used throughout the World ID documentation. Here are a few of the - **World ID**: A user's self-custodial identity, as well as the name of the protocol. - **App ID**: The ID of your app that is assigned in our [Developer Portal](https://developer.worldcoin.org/). - **Action**: A developer-facing primitive that lets you put any app operation behind a unique-human gate. An app can have one or more actions depending on your use case. +- **Issuer**: An entity authorized to issue a credential for a specific schema. Issuers sign credentials and publish their public keys in the `CredentialSchemaIssuerRegistry`. +- **Credential**: A signed attestation about a subject used to generate proofs. It includes issuer, subject, validity window, and claim commitments. - **Zero-Knowledge Proof (ZKP)**: A cryptographic method to prove that a statement is true without revealing any information about the statement itself. World ID uses ZKPs to prove that a user is verified without revealing the user's identity. - **Nullifier Hash**: A component of the World ID ZKP; a unique identifier for a combination of a user, `app_id`, and `action`. - **Signal**: A component of the World ID ZKP; data attached to the proof that cannot be tampered with. An example may be a user's choice for an election. - **Merkle Root**: A component of the World ID ZKP; The root of the [Merkle Tree](https://en.wikipedia.org/wiki/Merkle_tree) that identity commitments are inserted to. +### Further reading + +- [World ID 4 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs) + It's important to note the difference between the two types of verification, depending on the context: - **Orb/Passport Verification**: A user's identity can be verified through either an Orb, Device or Passport and their identity commitment is then recorded on the blockchain. diff --git a/world-id/credential-issuance/refresh.mdx b/world-id/credential-issuance/refresh.mdx new file mode 100644 index 0000000..060fbca --- /dev/null +++ b/world-id/credential-issuance/refresh.mdx @@ -0,0 +1,187 @@ +--- +title: "Credential Refresh" +description: "Refresh endpoint for issuing World ID PoH credentials and the Credential object format." +"og:image": "/images/docs/docs-meta.png" +"twitter:image": "/images/docs/docs-meta.png" +--- + +This endpoint issues a new proof-of-human (PoH) credential to a holder of a valid World ID. It can re-verify with a Personal Custody Package (PCP) or issue a credential-only refresh when a PCP is not available. + + + Base URL is environment-specific and served by the signup-service app-api. Contact + your World ID point of contact for environment endpoints and access. + + +## Endpoint + + + /api/v1/refresh + + +**Content-Type:** `multipart/form-data` + +## Request + +### Headers + +| Header | Type | Required | Description | +| --- | --- | --- | --- | +| `x-zkp-proof` | `string` | yes | Base64-encoded ZKP string containing `idCommitment` and `sub`. | + +### Query parameters + +| Query | Type | Required | Description | +| --- | --- | --- | --- | +| `idComm` | `string` | yes | User identity commitment. | + +### Form fields (always required) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `sub` | `string` | yes | User account ID (hex with `0x` prefix). Must match previous refreshes for this `idComm`. | +| `encrypted_user_id` | `string` | no | Temporary field while operator rewards depend on it. | + +### PCP form fields (required only when submitting a PCP) + +Include all fields below when refreshing with a Personal Custody Package. + +| Field | Type | Description | +| --- | --- | --- | +| `signup_id` | `string` | Signup ID. | +| `signup_id_salt` | `string` | Signup ID salt. | +| `orb_id` | `string` | Orb ID. | +| `orb_id_salt` | `string` | Orb ID salt. | +| `operator_id` | `string` | Operator ID. | +| `operator_id_salt` | `string` | Operator ID salt. | +| `signup_reason` | `string` | Signup reason. | +| `signup_reason_salt` | `string` | Signup reason salt. | +| `timestamp` | `string` | Timestamp. | +| `timestamp_salt` | `string` | Timestamp salt. | +| `software_version` | `string` | Software version. | +| `software_version_salt` | `string` | Software version salt. | +| `orb_country` | `string` | Orb country. | +| `orb_country_salt` | `string` | Orb country salt. | +| `iris_code_shares_0` | `string` | Iris code share 0. | +| `iris_code_shares_1` | `string` | Iris code share 1. | +| `iris_code_shares_2` | `string` | Iris code share 2. | +| `hashes.json` | `file` | PCP hashes JSON file. | +| `hashes.sign` | `file` | PCP hashes signature file. | + +### Example (credential-only refresh) + +```bash +curl -X POST "https:///api/v1/refresh?idComm=0xabc123..." \ + -H "x-zkp-proof: " \ + -F "sub=0x1a2b3c" +``` + +### Example (refresh with PCP) + +```bash +curl -X POST "https:///api/v1/refresh?idComm=0xabc123..." \ + -H "x-zkp-proof: " \ + -F "sub=0x1a2b3c" \ + -F "signup_id=signup_123" \ + -F "signup_id_salt=..." \ + -F "orb_id=orb_abc" \ + -F "orb_id_salt=..." \ + -F "operator_id=operator_123" \ + -F "operator_id_salt=..." \ + -F "signup_reason=..." \ + -F "signup_reason_salt=..." \ + -F "timestamp=1700000000" \ + -F "timestamp_salt=..." \ + -F "software_version=1.2.3" \ + -F "software_version_salt=..." \ + -F "orb_country=US" \ + -F "orb_country_salt=..." \ + -F "iris_code_shares_0=..." \ + -F "iris_code_shares_1=..." \ + -F "iris_code_shares_2=..." \ + -F "hashes.json=@hashes.json" \ + -F "hashes.sign=@hashes.sign" +``` + +## Response + +### Success response + +```json +{ + "success": true, + "credential": "", + "message": "Credential refreshed successfully" +} +``` + +### Error responses + +| Status | Error | Description | +| --- | --- | --- | +| 400 | `INVALID_SUB` | `sub` is missing or invalid. | +| 400 | `SUB_MISMATCH` | `sub` does not match the one used in previous refreshes. | +| 404 | `NO_SIGNUP_RECORD` | No enrollment record found. User must re-enroll at an Orb. | +| 429 | `RATE_LIMIT_EXCEEDED` | Refresh rate limit exceeded for the current window. | +| 503 | - | Credential refresh is disabled. | + +If PCP validation fails, the endpoint returns an error status with `PCP_VALIDATION_FAILED`. + +## Credential object format + +The `credential` response field is a base64-encoded JSON representation of the World ID `Credential` object defined in `world-id-protocol/crates/primitives/src/credential.rs`. + +### Decoding + +```javascript +const decoded = JSON.parse(Buffer.from(credential, "base64").toString("utf8")); +``` + +### Example (decoded) + +```json +{ + "id": 123456789, + "version": "V1", + "issuer_schema_id": 42, + "sub": "", + "genesis_issued_at": 1733241600, + "expires_at": 1764777600, + "claims": ["", "", ""], + "associated_data_hash": "", + "signature": "", + "issuer": { + "pk": ["", ""] + } +} +``` + +### Field definitions + +| Field | Type | Description | +| --- | --- | --- | +| `id` | `uint64` | Issuer-scoped reference identifier for the credential. | +| `version` | `string` | Credential version. Current value is `V1`. | +| `issuer_schema_id` | `uint64` | Identifier for the (issuer, schema) pair registered in `CredentialSchemaIssuerRegistry`. | +| `sub` | `FieldElement` | Blinded subject identifier derived from the World ID leaf index and an issuer-specific blinding factor. | +| `genesis_issued_at` | `uint64` | Unix timestamp (seconds) of the first issuance of this credential. | +| `expires_at` | `uint64` | Unix timestamp (seconds) for expiration. | +| `claims` | `FieldElement[]` | Up to 16 claim commitments. Unused indices are the zero field element. | +| `associated_data_hash` | `FieldElement` | Poseidon2 hash of issuer-defined associated data. The associated data itself is not included. | +| `signature` | `string` | 64-byte compressed EdDSA signature over the credential hash, hex-encoded (128 hex chars, no `0x`). | +| `issuer` | `EdDSAPublicKey` | Issuer public key that signed the credential. | + +### Field representations + +- **FieldElement** values (`sub`, `claims`, `associated_data_hash`) are hex strings with a `0x` prefix and 64 hex characters. +- **Issuer public key** (`issuer.pk`) is serialized as `[x, y]` decimal strings for BabyJubJub affine coordinates. +- **Signature** is hex-encoded compressed bytes (no `0x` prefix). + +### PoH-specific claims + +- When refreshing with a PCP, `claims[0]` and `associated_data_hash` are derived from the PCP `hashes.json` bytes. +- When refreshing without a PCP, `claims[0]` is derived from issuer-defined refresh data (a hash of `idCommitment`, `sub`, and a timestamp). + +## References + +- [World ID 4 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs) +- [PoH credential structure (Notion)](https://www.notion.so/worldcoin/WIP-World-ID-Issuers-Credential-Structures-2ca8614bdf8c80c5b1cbfdd7d6becb61?source=copy_link#2ca8614bdf8c80879d9dcd770734ae41) diff --git a/world-id/reference/credential.mdx b/world-id/reference/credential.mdx new file mode 100644 index 0000000..6d442c7 --- /dev/null +++ b/world-id/reference/credential.mdx @@ -0,0 +1,128 @@ +--- +title: "Credential Reference" +description: "World ID Credential object format, serialization, and PoH-specific usage." +"og:image": "/images/docs/docs-meta.png" +"twitter:image": "/images/docs/docs-meta.png" +--- + +A **Credential** is a signed attestation issued to a subject. It is the canonical object used by authenticators to generate World ID proofs. Relying parties (RPs) do **not** receive the credential itself; they verify proofs derived from it. + +This page documents the Credential object as defined in `world-id-protocol/crates/primitives/src/credential.rs`, plus PoH-specific conventions from the issuer credential structure spec. + + + The refresh endpoint returns a base64-encoded Credential. See + `/world-id/credential-issuance/refresh` for issuance details. + + +## Encoding + +Credentials are serialized as JSON and commonly wrapped in base64 when returned by API endpoints. + +```javascript +const decoded = JSON.parse(Buffer.from(credential, "base64").toString("utf8")); +``` + +## Credential schema + +```json +{ + "id": 123456789, + "version": "V1", + "issuer_schema_id": 42, + "sub": "0x0000000000000000000000000000000000000000000000000000000000000000", + "genesis_issued_at": 1733241600, + "expires_at": 1764777600, + "claims": [ + "0x0000000000000000000000000000000000000000000000000000000000000000" + ], + "associated_data_hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "signature": "5f2e...a3c1", + "issuer": { + "pk": ["1234567890", "9876543210"] + } +} +``` + + + The `claims` array is fixed-length (16 entries) and padded with the zero field + element. The example above is shortened for readability. + + +### Field definitions + +| Field | Type | Description | +| --- | --- | --- | +| `id` | `uint64` | Issuer-scoped reference identifier. Not exposed to RPs. | +| `version` | `string` | Credential version. Current value is `V1`. | +| `issuer_schema_id` | `uint64` | Identifier for the (issuer, schema) pair registered in `CredentialSchemaIssuerRegistry`. | +| `sub` | `FieldElement` | Blinded subject identifier derived from a World ID leaf index and an issuer-specific blinding factor. | +| `genesis_issued_at` | `uint64` | Unix timestamp (seconds) of the **first issuance** of this credential. | +| `expires_at` | `uint64` | Unix timestamp (seconds) for expiration. | +| `claims` | `FieldElement[]` | Up to 16 claim commitments. Unused indices are the zero field element. | +| `associated_data_hash` | `FieldElement` | Poseidon2 hash of issuer-defined associated data. The data itself is not included. | +| `signature` | `string` | 64-byte compressed EdDSA signature over the credential hash, hex-encoded (no `0x` prefix). | +| `issuer` | `EdDSAPublicKey` | Issuer public key that signed the credential. | + + + Claims are included for issuer-defined semantics and may not be enforced by + proofs today. Associated data is stored by authenticators and is not exposed + to RPs. + + +### Field representations + +- **FieldElement** values (`sub`, `claims`, `associated_data_hash`) are hex strings with a `0x` prefix and 64 hex characters. +- **Issuer public key** (`issuer.pk`) is serialized as `[x, y]` decimal strings for BabyJubJub affine coordinates. +- **Signature** is hex-encoded compressed bytes (128 hex chars). + +## Hashing and signing + +Credentials are hashed with Poseidon2 and signed using EdDSA over the BabyJubJub curve (V1). + +``` +claims_hash = Poseidon2_t16(claims[0..15]) +cred_hash = Poseidon2_t8( + DS, + issuer_schema_id, + sub, + genesis_issued_at, + expires_at, + claims_hash, + associated_data_hash, + id +) +signature = EdDSA_BJJ.sign(cred_hash) +``` + +`sub` is computed by hashing the World ID leaf index with a blinding factor: + +``` +sub = Poseidon2_t3(DS_SUB, leaf_index, blinding_factor) +``` + +## How credentials are used in proofs + +When an authenticator generates a proof, it includes the credential and enforces: + +- The credential signature matches the issuer key registered in `CredentialSchemaIssuerRegistry`. +- The credential `sub` matches the blinded leaf index for the holder. +- The credential is not expired and meets any minimum `genesis_issued_at` constraints. + +This makes credentials portable across authenticators while keeping subjects unlinkable between issuers. + +## PoH credential usage + +Proof-of-Human (PoH) credentials follow the issuer-defined structure. The current PoH issuer spec states: + +- The user first obtains an Orb credential (currently a PCP in v2.3 format). +- `claim[0]` is a commitment to the Orb credential, currently `H(hashes.json)`. +- The PoH credential has no associated data, so `associated_data_hash` is the zero field element. +- The PoH credential subject is blinded and differs from the Orb credential subject. +- The issuer may require a proof for the requested `sub` to prevent bricking an identity. + +These details are issuer-specific and may evolve as the protocol migrates to the World ID 4.0 credential format. + +## References + +- [World ID 4 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs) +- [PoH issuer credential structure (Notion)](https://www.notion.so/worldcoin/WIP-World-ID-Issuers-Credential-Structures-2ca8614bdf8c80c5b1cbfdd7d6becb61?source=copy_link#2ca8614bdf8c80879d9dcd770734ae41) From 44e75ebe08cd520b25689ec9ccd0734a18ac22da Mon Sep 17 00:00:00 2001 From: Carlo Mazzaferro Date: Thu, 5 Feb 2026 13:31:48 +0100 Subject: [PATCH 2/6] minor fixes --- world-id/credential-issuance/refresh.mdx | 2 +- world-id/reference/credential.mdx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/world-id/credential-issuance/refresh.mdx b/world-id/credential-issuance/refresh.mdx index 060fbca..c81b66c 100644 --- a/world-id/credential-issuance/refresh.mdx +++ b/world-id/credential-issuance/refresh.mdx @@ -184,4 +184,4 @@ const decoded = JSON.parse(Buffer.from(credential, "base64").toString("utf8")); ## References - [World ID 4 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs) -- [PoH credential structure (Notion)](https://www.notion.so/worldcoin/WIP-World-ID-Issuers-Credential-Structures-2ca8614bdf8c80c5b1cbfdd7d6becb61?source=copy_link#2ca8614bdf8c80879d9dcd770734ae41) + diff --git a/world-id/reference/credential.mdx b/world-id/reference/credential.mdx index 6d442c7..be0eb84 100644 --- a/world-id/reference/credential.mdx +++ b/world-id/reference/credential.mdx @@ -7,7 +7,7 @@ description: "World ID Credential object format, serialization, and PoH-specific A **Credential** is a signed attestation issued to a subject. It is the canonical object used by authenticators to generate World ID proofs. Relying parties (RPs) do **not** receive the credential itself; they verify proofs derived from it. -This page documents the Credential object as defined in `world-id-protocol/crates/primitives/src/credential.rs`, plus PoH-specific conventions from the issuer credential structure spec. +This page documents the Credential object as defined in [world-id-protocol](https://github.com/worldcoin/world-id-protocol/blob/main/crates/primitives/src/credential.rs#L84), plus PoH-specific conventions from the issuer credential structure spec. The refresh endpoint returns a base64-encoded Credential. See @@ -125,4 +125,4 @@ These details are issuer-specific and may evolve as the protocol migrates to the ## References - [World ID 4 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs) -- [PoH issuer credential structure (Notion)](https://www.notion.so/worldcoin/WIP-World-ID-Issuers-Credential-Structures-2ca8614bdf8c80c5b1cbfdd7d6becb61?source=copy_link#2ca8614bdf8c80879d9dcd770734ae41) + From d214306e809131fd8043c3588fc1a9c1a6d73007 Mon Sep 17 00:00:00 2001 From: Carlo Mazzaferro Date: Thu, 5 Feb 2026 13:37:23 +0100 Subject: [PATCH 3/6] minor fixes --- world-id/credential-issuance/refresh.mdx | 187 ----------------------- 1 file changed, 187 deletions(-) delete mode 100644 world-id/credential-issuance/refresh.mdx diff --git a/world-id/credential-issuance/refresh.mdx b/world-id/credential-issuance/refresh.mdx deleted file mode 100644 index c81b66c..0000000 --- a/world-id/credential-issuance/refresh.mdx +++ /dev/null @@ -1,187 +0,0 @@ ---- -title: "Credential Refresh" -description: "Refresh endpoint for issuing World ID PoH credentials and the Credential object format." -"og:image": "/images/docs/docs-meta.png" -"twitter:image": "/images/docs/docs-meta.png" ---- - -This endpoint issues a new proof-of-human (PoH) credential to a holder of a valid World ID. It can re-verify with a Personal Custody Package (PCP) or issue a credential-only refresh when a PCP is not available. - - - Base URL is environment-specific and served by the signup-service app-api. Contact - your World ID point of contact for environment endpoints and access. - - -## Endpoint - - - /api/v1/refresh - - -**Content-Type:** `multipart/form-data` - -## Request - -### Headers - -| Header | Type | Required | Description | -| --- | --- | --- | --- | -| `x-zkp-proof` | `string` | yes | Base64-encoded ZKP string containing `idCommitment` and `sub`. | - -### Query parameters - -| Query | Type | Required | Description | -| --- | --- | --- | --- | -| `idComm` | `string` | yes | User identity commitment. | - -### Form fields (always required) - -| Field | Type | Required | Description | -| --- | --- | --- | --- | -| `sub` | `string` | yes | User account ID (hex with `0x` prefix). Must match previous refreshes for this `idComm`. | -| `encrypted_user_id` | `string` | no | Temporary field while operator rewards depend on it. | - -### PCP form fields (required only when submitting a PCP) - -Include all fields below when refreshing with a Personal Custody Package. - -| Field | Type | Description | -| --- | --- | --- | -| `signup_id` | `string` | Signup ID. | -| `signup_id_salt` | `string` | Signup ID salt. | -| `orb_id` | `string` | Orb ID. | -| `orb_id_salt` | `string` | Orb ID salt. | -| `operator_id` | `string` | Operator ID. | -| `operator_id_salt` | `string` | Operator ID salt. | -| `signup_reason` | `string` | Signup reason. | -| `signup_reason_salt` | `string` | Signup reason salt. | -| `timestamp` | `string` | Timestamp. | -| `timestamp_salt` | `string` | Timestamp salt. | -| `software_version` | `string` | Software version. | -| `software_version_salt` | `string` | Software version salt. | -| `orb_country` | `string` | Orb country. | -| `orb_country_salt` | `string` | Orb country salt. | -| `iris_code_shares_0` | `string` | Iris code share 0. | -| `iris_code_shares_1` | `string` | Iris code share 1. | -| `iris_code_shares_2` | `string` | Iris code share 2. | -| `hashes.json` | `file` | PCP hashes JSON file. | -| `hashes.sign` | `file` | PCP hashes signature file. | - -### Example (credential-only refresh) - -```bash -curl -X POST "https:///api/v1/refresh?idComm=0xabc123..." \ - -H "x-zkp-proof: " \ - -F "sub=0x1a2b3c" -``` - -### Example (refresh with PCP) - -```bash -curl -X POST "https:///api/v1/refresh?idComm=0xabc123..." \ - -H "x-zkp-proof: " \ - -F "sub=0x1a2b3c" \ - -F "signup_id=signup_123" \ - -F "signup_id_salt=..." \ - -F "orb_id=orb_abc" \ - -F "orb_id_salt=..." \ - -F "operator_id=operator_123" \ - -F "operator_id_salt=..." \ - -F "signup_reason=..." \ - -F "signup_reason_salt=..." \ - -F "timestamp=1700000000" \ - -F "timestamp_salt=..." \ - -F "software_version=1.2.3" \ - -F "software_version_salt=..." \ - -F "orb_country=US" \ - -F "orb_country_salt=..." \ - -F "iris_code_shares_0=..." \ - -F "iris_code_shares_1=..." \ - -F "iris_code_shares_2=..." \ - -F "hashes.json=@hashes.json" \ - -F "hashes.sign=@hashes.sign" -``` - -## Response - -### Success response - -```json -{ - "success": true, - "credential": "", - "message": "Credential refreshed successfully" -} -``` - -### Error responses - -| Status | Error | Description | -| --- | --- | --- | -| 400 | `INVALID_SUB` | `sub` is missing or invalid. | -| 400 | `SUB_MISMATCH` | `sub` does not match the one used in previous refreshes. | -| 404 | `NO_SIGNUP_RECORD` | No enrollment record found. User must re-enroll at an Orb. | -| 429 | `RATE_LIMIT_EXCEEDED` | Refresh rate limit exceeded for the current window. | -| 503 | - | Credential refresh is disabled. | - -If PCP validation fails, the endpoint returns an error status with `PCP_VALIDATION_FAILED`. - -## Credential object format - -The `credential` response field is a base64-encoded JSON representation of the World ID `Credential` object defined in `world-id-protocol/crates/primitives/src/credential.rs`. - -### Decoding - -```javascript -const decoded = JSON.parse(Buffer.from(credential, "base64").toString("utf8")); -``` - -### Example (decoded) - -```json -{ - "id": 123456789, - "version": "V1", - "issuer_schema_id": 42, - "sub": "", - "genesis_issued_at": 1733241600, - "expires_at": 1764777600, - "claims": ["", "", ""], - "associated_data_hash": "", - "signature": "", - "issuer": { - "pk": ["", ""] - } -} -``` - -### Field definitions - -| Field | Type | Description | -| --- | --- | --- | -| `id` | `uint64` | Issuer-scoped reference identifier for the credential. | -| `version` | `string` | Credential version. Current value is `V1`. | -| `issuer_schema_id` | `uint64` | Identifier for the (issuer, schema) pair registered in `CredentialSchemaIssuerRegistry`. | -| `sub` | `FieldElement` | Blinded subject identifier derived from the World ID leaf index and an issuer-specific blinding factor. | -| `genesis_issued_at` | `uint64` | Unix timestamp (seconds) of the first issuance of this credential. | -| `expires_at` | `uint64` | Unix timestamp (seconds) for expiration. | -| `claims` | `FieldElement[]` | Up to 16 claim commitments. Unused indices are the zero field element. | -| `associated_data_hash` | `FieldElement` | Poseidon2 hash of issuer-defined associated data. The associated data itself is not included. | -| `signature` | `string` | 64-byte compressed EdDSA signature over the credential hash, hex-encoded (128 hex chars, no `0x`). | -| `issuer` | `EdDSAPublicKey` | Issuer public key that signed the credential. | - -### Field representations - -- **FieldElement** values (`sub`, `claims`, `associated_data_hash`) are hex strings with a `0x` prefix and 64 hex characters. -- **Issuer public key** (`issuer.pk`) is serialized as `[x, y]` decimal strings for BabyJubJub affine coordinates. -- **Signature** is hex-encoded compressed bytes (no `0x` prefix). - -### PoH-specific claims - -- When refreshing with a PCP, `claims[0]` and `associated_data_hash` are derived from the PCP `hashes.json` bytes. -- When refreshing without a PCP, `claims[0]` is derived from issuer-defined refresh data (a hash of `idCommitment`, `sub`, and a timestamp). - -## References - -- [World ID 4 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs) - From 937455222570c799b11eb2b94007057e898c2506 Mon Sep 17 00:00:00 2001 From: Carlo Mazzaferro Date: Thu, 5 Feb 2026 13:38:03 +0100 Subject: [PATCH 4/6] Revert "minor fixes" This reverts commit d214306e809131fd8043c3588fc1a9c1a6d73007. --- world-id/credential-issuance/refresh.mdx | 187 +++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 world-id/credential-issuance/refresh.mdx diff --git a/world-id/credential-issuance/refresh.mdx b/world-id/credential-issuance/refresh.mdx new file mode 100644 index 0000000..c81b66c --- /dev/null +++ b/world-id/credential-issuance/refresh.mdx @@ -0,0 +1,187 @@ +--- +title: "Credential Refresh" +description: "Refresh endpoint for issuing World ID PoH credentials and the Credential object format." +"og:image": "/images/docs/docs-meta.png" +"twitter:image": "/images/docs/docs-meta.png" +--- + +This endpoint issues a new proof-of-human (PoH) credential to a holder of a valid World ID. It can re-verify with a Personal Custody Package (PCP) or issue a credential-only refresh when a PCP is not available. + + + Base URL is environment-specific and served by the signup-service app-api. Contact + your World ID point of contact for environment endpoints and access. + + +## Endpoint + + + /api/v1/refresh + + +**Content-Type:** `multipart/form-data` + +## Request + +### Headers + +| Header | Type | Required | Description | +| --- | --- | --- | --- | +| `x-zkp-proof` | `string` | yes | Base64-encoded ZKP string containing `idCommitment` and `sub`. | + +### Query parameters + +| Query | Type | Required | Description | +| --- | --- | --- | --- | +| `idComm` | `string` | yes | User identity commitment. | + +### Form fields (always required) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `sub` | `string` | yes | User account ID (hex with `0x` prefix). Must match previous refreshes for this `idComm`. | +| `encrypted_user_id` | `string` | no | Temporary field while operator rewards depend on it. | + +### PCP form fields (required only when submitting a PCP) + +Include all fields below when refreshing with a Personal Custody Package. + +| Field | Type | Description | +| --- | --- | --- | +| `signup_id` | `string` | Signup ID. | +| `signup_id_salt` | `string` | Signup ID salt. | +| `orb_id` | `string` | Orb ID. | +| `orb_id_salt` | `string` | Orb ID salt. | +| `operator_id` | `string` | Operator ID. | +| `operator_id_salt` | `string` | Operator ID salt. | +| `signup_reason` | `string` | Signup reason. | +| `signup_reason_salt` | `string` | Signup reason salt. | +| `timestamp` | `string` | Timestamp. | +| `timestamp_salt` | `string` | Timestamp salt. | +| `software_version` | `string` | Software version. | +| `software_version_salt` | `string` | Software version salt. | +| `orb_country` | `string` | Orb country. | +| `orb_country_salt` | `string` | Orb country salt. | +| `iris_code_shares_0` | `string` | Iris code share 0. | +| `iris_code_shares_1` | `string` | Iris code share 1. | +| `iris_code_shares_2` | `string` | Iris code share 2. | +| `hashes.json` | `file` | PCP hashes JSON file. | +| `hashes.sign` | `file` | PCP hashes signature file. | + +### Example (credential-only refresh) + +```bash +curl -X POST "https:///api/v1/refresh?idComm=0xabc123..." \ + -H "x-zkp-proof: " \ + -F "sub=0x1a2b3c" +``` + +### Example (refresh with PCP) + +```bash +curl -X POST "https:///api/v1/refresh?idComm=0xabc123..." \ + -H "x-zkp-proof: " \ + -F "sub=0x1a2b3c" \ + -F "signup_id=signup_123" \ + -F "signup_id_salt=..." \ + -F "orb_id=orb_abc" \ + -F "orb_id_salt=..." \ + -F "operator_id=operator_123" \ + -F "operator_id_salt=..." \ + -F "signup_reason=..." \ + -F "signup_reason_salt=..." \ + -F "timestamp=1700000000" \ + -F "timestamp_salt=..." \ + -F "software_version=1.2.3" \ + -F "software_version_salt=..." \ + -F "orb_country=US" \ + -F "orb_country_salt=..." \ + -F "iris_code_shares_0=..." \ + -F "iris_code_shares_1=..." \ + -F "iris_code_shares_2=..." \ + -F "hashes.json=@hashes.json" \ + -F "hashes.sign=@hashes.sign" +``` + +## Response + +### Success response + +```json +{ + "success": true, + "credential": "", + "message": "Credential refreshed successfully" +} +``` + +### Error responses + +| Status | Error | Description | +| --- | --- | --- | +| 400 | `INVALID_SUB` | `sub` is missing or invalid. | +| 400 | `SUB_MISMATCH` | `sub` does not match the one used in previous refreshes. | +| 404 | `NO_SIGNUP_RECORD` | No enrollment record found. User must re-enroll at an Orb. | +| 429 | `RATE_LIMIT_EXCEEDED` | Refresh rate limit exceeded for the current window. | +| 503 | - | Credential refresh is disabled. | + +If PCP validation fails, the endpoint returns an error status with `PCP_VALIDATION_FAILED`. + +## Credential object format + +The `credential` response field is a base64-encoded JSON representation of the World ID `Credential` object defined in `world-id-protocol/crates/primitives/src/credential.rs`. + +### Decoding + +```javascript +const decoded = JSON.parse(Buffer.from(credential, "base64").toString("utf8")); +``` + +### Example (decoded) + +```json +{ + "id": 123456789, + "version": "V1", + "issuer_schema_id": 42, + "sub": "", + "genesis_issued_at": 1733241600, + "expires_at": 1764777600, + "claims": ["", "", ""], + "associated_data_hash": "", + "signature": "", + "issuer": { + "pk": ["", ""] + } +} +``` + +### Field definitions + +| Field | Type | Description | +| --- | --- | --- | +| `id` | `uint64` | Issuer-scoped reference identifier for the credential. | +| `version` | `string` | Credential version. Current value is `V1`. | +| `issuer_schema_id` | `uint64` | Identifier for the (issuer, schema) pair registered in `CredentialSchemaIssuerRegistry`. | +| `sub` | `FieldElement` | Blinded subject identifier derived from the World ID leaf index and an issuer-specific blinding factor. | +| `genesis_issued_at` | `uint64` | Unix timestamp (seconds) of the first issuance of this credential. | +| `expires_at` | `uint64` | Unix timestamp (seconds) for expiration. | +| `claims` | `FieldElement[]` | Up to 16 claim commitments. Unused indices are the zero field element. | +| `associated_data_hash` | `FieldElement` | Poseidon2 hash of issuer-defined associated data. The associated data itself is not included. | +| `signature` | `string` | 64-byte compressed EdDSA signature over the credential hash, hex-encoded (128 hex chars, no `0x`). | +| `issuer` | `EdDSAPublicKey` | Issuer public key that signed the credential. | + +### Field representations + +- **FieldElement** values (`sub`, `claims`, `associated_data_hash`) are hex strings with a `0x` prefix and 64 hex characters. +- **Issuer public key** (`issuer.pk`) is serialized as `[x, y]` decimal strings for BabyJubJub affine coordinates. +- **Signature** is hex-encoded compressed bytes (no `0x` prefix). + +### PoH-specific claims + +- When refreshing with a PCP, `claims[0]` and `associated_data_hash` are derived from the PCP `hashes.json` bytes. +- When refreshing without a PCP, `claims[0]` is derived from issuer-defined refresh data (a hash of `idCommitment`, `sub`, and a timestamp). + +## References + +- [World ID 4 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs) + From 19f509bdb399cd8b233c731ec4060cb71a7942fe Mon Sep 17 00:00:00 2001 From: Carlo Mazzaferro Date: Thu, 5 Feb 2026 13:39:21 +0100 Subject: [PATCH 5/6] revert removal of files --- world-id/credential-issuance/refresh.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/world-id/credential-issuance/refresh.mdx b/world-id/credential-issuance/refresh.mdx index c81b66c..e578d49 100644 --- a/world-id/credential-issuance/refresh.mdx +++ b/world-id/credential-issuance/refresh.mdx @@ -178,10 +178,12 @@ const decoded = JSON.parse(Buffer.from(credential, "base64").toString("utf8")); ### PoH-specific claims -- When refreshing with a PCP, `claims[0]` and `associated_data_hash` are derived from the PCP `hashes.json` bytes. +- When refreshing with a PCP, `claims[0]` is derived from the PCP `hashes.json` bytes. - When refreshing without a PCP, `claims[0]` is derived from issuer-defined refresh data (a hash of `idCommitment`, `sub`, and a timestamp). +- In both cases, `associated_data_hash` is instead empty, i.e. the zero field element ## References - [World ID 4 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs) +- [Credential reference](/world-id/reference/credential) From 4a1f881528388bb301d9d392210e3fa918759575 Mon Sep 17 00:00:00 2001 From: Carlo Mazzaferro Date: Thu, 5 Feb 2026 13:43:49 +0100 Subject: [PATCH 6/6] links fixes --- world-id/concepts.mdx | 5 +---- world-id/credential-issuance/refresh.mdx | 2 +- world-id/reference/credential.mdx | 4 ++-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/world-id/concepts.mdx b/world-id/concepts.mdx index 0910b81..f266a55 100644 --- a/world-id/concepts.mdx +++ b/world-id/concepts.mdx @@ -29,15 +29,12 @@ Some terms are used throughout the World ID documentation. Here are a few of the - **App ID**: The ID of your app that is assigned in our [Developer Portal](https://developer.worldcoin.org/). - **Action**: A developer-facing primitive that lets you put any app operation behind a unique-human gate. An app can have one or more actions depending on your use case. - **Issuer**: An entity authorized to issue a credential for a specific schema. Issuers sign credentials and publish their public keys in the `CredentialSchemaIssuerRegistry`. -- **Credential**: A signed attestation about a subject used to generate proofs. It includes issuer, subject, validity window, and claim commitments. +- **Credential**: A signed attestation about a subject used to generate proofs. It includes issuer, subject, validity window, and claim commitments as defined in the [World ID 4.0 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs) - **Zero-Knowledge Proof (ZKP)**: A cryptographic method to prove that a statement is true without revealing any information about the statement itself. World ID uses ZKPs to prove that a user is verified without revealing the user's identity. - **Nullifier Hash**: A component of the World ID ZKP; a unique identifier for a combination of a user, `app_id`, and `action`. - **Signal**: A component of the World ID ZKP; data attached to the proof that cannot be tampered with. An example may be a user's choice for an election. - **Merkle Root**: A component of the World ID ZKP; The root of the [Merkle Tree](https://en.wikipedia.org/wiki/Merkle_tree) that identity commitments are inserted to. -### Further reading - -- [World ID 4 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs) It's important to note the difference between the two types of verification, depending on the context: diff --git a/world-id/credential-issuance/refresh.mdx b/world-id/credential-issuance/refresh.mdx index e578d49..ad29bc9 100644 --- a/world-id/credential-issuance/refresh.mdx +++ b/world-id/credential-issuance/refresh.mdx @@ -184,6 +184,6 @@ const decoded = JSON.parse(Buffer.from(credential, "base64").toString("utf8")); ## References -- [World ID 4 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs) +- [World ID 4.0 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs) - [Credential reference](/world-id/reference/credential) diff --git a/world-id/reference/credential.mdx b/world-id/reference/credential.mdx index be0eb84..ed4dc80 100644 --- a/world-id/reference/credential.mdx +++ b/world-id/reference/credential.mdx @@ -11,7 +11,7 @@ This page documents the Credential object as defined in [world-id-protocol](http The refresh endpoint returns a base64-encoded Credential. See - `/world-id/credential-issuance/refresh` for issuance details. + [Credential Issuance](/world-id/reference/credential) for issuance details. ## Encoding @@ -124,5 +124,5 @@ These details are issuer-specific and may evolve as the protocol migrates to the ## References -- [World ID 4 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs) +- [World ID 4.0 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs)