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..f266a55 100644
--- a/world-id/concepts.mdx
+++ b/world-id/concepts.mdx
@@ -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)
- **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.
diff --git a/world-id/credential-issuance/refresh.mdx b/world-id/credential-issuance/refresh.mdx
new file mode 100644
index 0000000..ad29bc9
--- /dev/null
+++ b/world-id/credential-issuance/refresh.mdx
@@ -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.
+
+
+ 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]` 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)
+
diff --git a/world-id/reference/credential.mdx b/world-id/reference/credential.mdx
new file mode 100644
index 0000000..ed4dc80
--- /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](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
+ [Credential Issuance](/world-id/reference/credential) 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.0 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs)
+