Skip to content
Open
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
5 changes: 5 additions & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,15 @@
"group": "Sign in with World ID",
"pages": ["world-id/sign-in/oidc"]
},
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's create a new section altogether for the PoH Issuer. @andy-t-wang sg?

Image

"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",
Expand Down
3 changes: 3 additions & 0 deletions world-id/concepts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,14 @@ 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 as defined in the [World ID 4.0 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's point instead to the source of truth for what is a credential, https://docs.rs/world-id-primitives/latest/world_id_primitives/credential/struct.Credential.html

- **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.


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.
Expand Down
189 changes: 189 additions & 0 deletions world-id/credential-issuance/refresh.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
---
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear that this endpoint is only intended for v3 users to get their v4 credential


<Note>
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.
</Note>

## Endpoint

<ParamField path="method" type="POST">
/api/v1/refresh
</ParamField>

**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://<app-api-host>/api/v1/refresh?idComm=0xabc123..." \
-H "x-zkp-proof: <base64-zkp>" \
-F "sub=0x1a2b3c"
```

### Example (refresh with PCP)

```bash
curl -X POST "https://<app-api-host>/api/v1/refresh?idComm=0xabc123..." \
-H "x-zkp-proof: <base64-zkp>" \
-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": "<base64-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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd drop this section, it's generally self-explanatory with the Credential struct and it may easily become outdated


```javascript
const decoded = JSON.parse(Buffer.from(credential, "base64").toString("utf8"));
```

### Example (decoded)

```json
{
"id": 123456789,
"version": "V1",
"issuer_schema_id": 42,
"sub": "<field-element-hex>",
"genesis_issued_at": 1733241600,
"expires_at": 1764777600,
"claims": ["<field-element-hex>", "<field-element-hex>", "<field-element-hex>"],
"associated_data_hash": "<field-element-hex>",
"signature": "<signature-hex>",
"issuer": {
"pk": ["<x-decimal>", "<y-decimal>"]
}
}
```

### 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is super important to document, but let's keep it only in a single place


- 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.0 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs)
- [Credential reference](/world-id/reference/credential)

128 changes: 128 additions & 0 deletions world-id/reference/credential.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we take the general information and move it to the protocol repo? perhaps it's worth having a generic Credential page here, but it should be fully separate from the PoH-specific definitions

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](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.

<Note>
The refresh endpoint returns a base64-encoded Credential. See
[Credential Issuance](/world-id/reference/credential) for issuance details.
</Note>

## 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"]
}
}
```

<Note>
The `claims` array is fixed-length (16 entries) and padded with the zero field
element. The example above is shortened for readability.
</Note>

### 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. |

<Note>
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.
</Note>

### 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.0 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs)