---
updatedAt: 2026-08-12T17:35:34.000Z
agentTools:
  projectIndex: https://docs.zerohash.com/llms.txt
---

# Get an inclusion proof for a participant

Return the Merkle inclusion proof for one participant in your platform's tree for an asset: the leaf preimage, the sibling path, and the signed root the path resolves to.

This is a POST because the participant code is enumerable and sensitive; keeping it in the body keeps it out of URLs, proxy access logs, and browser history. The response is per-participant and is not cacheable.

The tree is always your own — the platform comes from your authenticated identity. Only `participant_code` is read from the body. Send nothing else: a stray field is normally rejected with 400, and where request validation runs at the edge instead it is ignored — but a platform identifier that is not your own is denied with 403 rather than honoured.

To verify: recompute the leaf hash from `leaf`, walk `proof_path` applying each sibling on the side given by `direction` and adding sums with checked u128 arithmetic, then require the resulting hash and sum to equal the values inside the verified `commitment_cbor` of `root`. Bind the check to the expected platform, asset, and `spec_version` — a mathematically valid root for the wrong tree still fails the assertion you care about.

This document describes the wire contract, not the hash construction. The exact preimage bytes — hash function, domain separation, field encoding and ordering, the participant-slot derivation, sentinel padding, the internal-node preimage, and the full set of signed CBOR entries — are defined in the versioned Proof of Reserves verification spec issued with the reference verifier at onboarding. You need that document to implement a verifier; this one is not sufficient on its own. If the two ever disagree, treat it as a defect on our side and raise it — do not pick one and proceed.

Note that a Merkle Sum Tree proof conceals which participants the siblings represent, but not their aggregated balances: each sibling's subtree sum is disclosed.

No rate limit is applied on these routes by this API. Edge limits may still apply, so handle `429` and honour `Retry-After` rather than assuming an unlimited budget.

# OpenAPI definition

```json
{
  "openapi": "3.1.0",
  "info": {
    "title": "zerohash API",
    "description": "\n### Authentication\n\nzerohash Uses HMAC SHA-256 verification to ensure the authenticity of API requests, follow instructions by link [https://docs.zerohash.com/reference/api-authentication](https://docs.zerohash.com/reference/api-authentication)\n\n<a href=\"/zh-swagger.json\">Download zerohash OpenAPI Schema as JSON</a>\n",
    "version": "1.7.0"
  },
  "servers": [
    {
      "url": "https://api.cert.zerohash.com",
      "description": "Certification API server"
    }
  ],
  "x-readme": {
    "proxy-enabled": false
  },
  "tags": [
    {
      "name": "Proof of Reserves",
      "description": "Signed Merkle Sum Tree root commitments and participant inclusion proofs. Each root commits to per-participant balances as recorded in the zerohash liability ledger, so a verified proof shows that a balance was included in the published total — it is not by itself evidence of the assets held against those liabilities."
    }
  ],
  "paths": {
    "/por/proofs/{asset}/{snapshot}": {
      "post": {
        "tags": [
          "Proof of Reserves"
        ],
        "summary": "Get an inclusion proof for a participant",
        "description": "Return the Merkle inclusion proof for one participant in your platform's tree for an asset: the leaf preimage, the sibling path, and the signed root the path resolves to.\n\nThis is a POST because the participant code is enumerable and sensitive; keeping it in the body keeps it out of URLs, proxy access logs, and browser history. The response is per-participant and is not cacheable.\n\nThe tree is always your own — the platform comes from your authenticated identity. Only `participant_code` is read from the body. Send nothing else: a stray field is normally rejected with 400, and where request validation runs at the edge instead it is ignored — but a platform identifier that is not your own is denied with 403 rather than honoured.\n\nTo verify: recompute the leaf hash from `leaf`, walk `proof_path` applying each sibling on the side given by `direction` and adding sums with checked u128 arithmetic, then require the resulting hash and sum to equal the values inside the verified `commitment_cbor` of `root`. Bind the check to the expected platform, asset, and `spec_version` — a mathematically valid root for the wrong tree still fails the assertion you care about.\n\nThis document describes the wire contract, not the hash construction. The exact preimage bytes — hash function, domain separation, field encoding and ordering, the participant-slot derivation, sentinel padding, the internal-node preimage, and the full set of signed CBOR entries — are defined in the versioned Proof of Reserves verification spec issued with the reference verifier at onboarding. You need that document to implement a verifier; this one is not sufficient on its own. If the two ever disagree, treat it as a defect on our side and raise it — do not pick one and proceed.\n\nNote that a Merkle Sum Tree proof conceals which participants the siblings represent, but not their aggregated balances: each sibling's subtree sum is disclosed.\n\nNo rate limit is applied on these routes by this API. Edge limits may still apply, so handle `429` and honour `Retry-After` rather than assuming an unlimited budget.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/Timestamp"
          },
          {
            "$ref": "#/components/parameters/RequestId"
          },
          {
            "name": "asset",
            "in": "path",
            "description": "Chain-qualified asset identifier. A bare symbol denotes the asset on its home chain; a dotted form denotes the bridged asset on that chain.",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "example": "ETH"
            }
          },
          {
            "name": "snapshot",
            "in": "path",
            "description": "Which tree to prove against. Either the literal `latest` or a `db_snapshot_id` hex string to pin a specific historical snapshot.\n\n`latest` resolves to the commitment with the highest `publication_seq` for this tree. To pin: take `db_snapshot_id` from a prior roots response and pass it here — the proof is built against that exact snapshot.",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "example": "latest"
            }
          }
        ],
        "requestBody": {
          "description": "Participant whose inclusion proof is requested",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PostPorProofsBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Inclusion proof and the signed root it resolves to",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PostPorProofsResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Code400"
                }
              }
            }
          },
          "403": {
            "description": "Authorization failed. Three distinct causes share this status:\n\n- authentication rejected (missing, malformed, or expired API-key headers or signature) — the body is a plain string served as `text/html`, not JSON;\n- device verification could not resolve your platform, or your platform requires device identification headers that were not supplied;\n- a platform other than your own was named on the request.\n\nThe body shape depends on which check rejected the call, so branch on the response content type rather than assuming JSON.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string",
                  "example": "Missing API Key header"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PorAuthError"
                }
              }
            }
          },
          "404": {
            "description": "No proof is available. Either Proof of Reserves is not yet enabled for your platform, no tree exists for that asset and snapshot, or the participant is not a leaf in that tree.\n\nWhile the feature is disabled for your platform the response is a plain-text `Not Found` with no body schema; once enabled, a genuine miss returns the JSON error below. Both cases are 404 by design — the feature is invisible until enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PorUpstreamError"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "example": "Not Found"
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Code500"
                }
              }
            }
          },
          "503": {
            "description": "Service Unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Code503"
                }
              }
            }
          }
        }
      }
    }
  },
  "security": [
    {
      "apiKey": [],
      "apiPassphrase": []
    }
  ],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "apiKey",
        "description": "Your API public key. See the [Authentication guide](https://docs.zerohash.com/reference/api-authentication) for how to obtain keys.",
        "in": "header",
        "name": "X-SCX-API-KEY"
      },
      "apiPassphrase": {
        "type": "apiKey",
        "description": "The passphrase associated with your API key, set when the key was created.",
        "in": "header",
        "name": "X-SCX-PASSPHRASE"
      }
    },
    "parameters": {
      "Signature": {
        "name": "X-SCX-SIGNED",
        "in": "header",
        "description": "HMAC-SHA256 signature of the request, base64-encoded. See the [Authentication guide](https://docs.zerohash.com/reference/api-authentication) for the exact signing formula.",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "Timestamp": {
        "name": "X-SCX-TIMESTAMP",
        "in": "header",
        "description": "Current Unix timestamp in seconds. Must be within 60 seconds of server time or the request is rejected.",
        "required": true,
        "schema": {
          "type": "string",
          "example": "1678901234"
        }
      },
      "RequestId": {
        "name": "X-Request-Id",
        "in": "header",
        "description": "Optional client-supplied request ID used for tracing and idempotency. Must be a [UUID v4](https://www.uuidtools.com/uuid-versions-explained#version-4). When omitted, zerohash generates one and returns it in the response `X-Request-Id` header.",
        "required": false,
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      }
    },
    "schemas": {
      "Code400": {
        "type": "object",
        "description": "Request was rejected by validation or a downstream service. `errors` is an array of human-readable messages; each entry describes a single validation failure or business-rule violation.",
        "required": [
          "errors"
        ],
        "properties": {
          "errors": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "body/amount must be >= 0",
              "body/asset is required"
            ]
          }
        }
      },
      "Code500": {
        "type": "object",
        "description": "An unexpected error occurred on the server. The request can be retried after a short delay, but the same inputs may reproduce the error.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "example": "Internal Server Error"
          }
        }
      },
      "Code503": {
        "type": "object",
        "description": "A downstream dependency was unavailable, timed out, or returned a retryable error. Safe to retry; the response carries a `zh-allow-retry: true` header.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "example": "Service Unavailable"
          }
        }
      },
      "PorRootCommitment": {
        "type": "object",
        "description": "A signed Merkle Sum Tree root for one `(platform, asset)` pair.\n\n**Only `commitment_cbor` and `signature` are authoritative.** Verify the signature over the raw `commitment_cbor` bytes, then decode those bytes and read the values from them. The JSON fields below are an unsigned convenience view and must not be trusted on their own.\n\nThe two are not a one-to-one mapping: the signed payload commits to fields that have no JSON counterpart here, and `signing_key_id` is not inside the signed payload at all — it identifies which key to check the signature with, so treat it as a hint and reject the response if the signature does not verify.",
        "required": [
          "platform_code",
          "asset",
          "spec_version",
          "publication_seq",
          "db_snapshot_id",
          "snapshot_ts",
          "root_hash",
          "root_sum",
          "commitment_cbor",
          "signature",
          "signing_key_id"
        ],
        "properties": {
          "platform_code": {
            "type": "string",
            "description": "Platform the tree belongs to. Always your own platform — it is taken from your authenticated identity, never from the request.",
            "example": "FP2F20"
          },
          "asset": {
            "type": "string",
            "description": "Chain-qualified asset identifier, used in every leaf exactly as returned here — apply no normalization of your own.",
            "example": "ETH"
          },
          "spec_version": {
            "type": "string",
            "description": "Version of the Proof of Reserves commitment schema. A change here can change the leaf preimage or the signed payload layout.",
            "example": "1.0.0"
          },
          "publication_seq": {
            "type": "string",
            "pattern": "^[0-9]+$",
            "description": "Publication counter for this `(platform, asset)` series, rendered as a decimal **string** (not a JSON number) because it is a 64-bit integer. Monotonically increasing — a higher value supersedes a lower one for the same tree. Use it to detect a replayed or withheld root.",
            "example": "1"
          },
          "db_snapshot_id": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "Content identifier of the whole normalized balance snapshot this root was built from, as lowercase hex. It is computed across every platform and asset in that snapshot, of which this tree is one subset — so trees built in the same run share one value, and it does not by itself identify this tree. See the `snapshot` path parameter for how to address a specific root.",
            "example": "10edfd6c58bcae73b5c6bf9f9e88b69cb47173271cb5fc1362121b1544d04f3c"
          },
          "snapshot_ts": {
            "type": "string",
            "format": "date-time",
            "description": "Time the balance snapshot was taken, RFC 3339 UTC. Currently emitted as the Unix epoch (`1970-01-01T00:00:00Z`) — treat it as unset and do not use it for freshness checks until it carries a real value.",
            "example": "1970-01-01T00:00:00Z"
          },
          "root_hash": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "Merkle root, 32 bytes as lowercase hex.",
            "example": "b72605925891e8d8f9c1a9b28f662f34b66f7f9ee9ece8c187cc206c06ac49c4"
          },
          "root_sum": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "Sum of every leaf balance in the tree, as a 16-byte **big-endian u128 in lowercase hex** — the same bytes that are hashed into the tree. Note this differs from `balance` and `sibling_sum`, which are decimal. Phase 1 sums `available` balances only.",
            "example": "000000000005a9d6eb0ecc04a85ed3a0"
          },
          "commitment_cbor": {
            "type": "string",
            "format": "byte",
            "description": "The exact bytes that were signed, canonical CBOR (RFC 8949 §4.2.1), base64-encoded for transport. Verify `signature` over the decoded bytes — never over a re-encoding of the JSON fields above.\n\nDecode it as a whole map rather than reading fixed offsets: it commits to more entries than are mirrored in this response, and the entry set is tied to `spec_version`. The authoritative field list for a given `spec_version` is in the Proof of Reserves verification spec issued with the reference verifier at onboarding.\n\nIn the example below three signed entries are still placeholders and must not be relied on: `asset_precision` is `0`, `evidence_pkg_sha256` is 32 zero bytes, and `asset_config_version` is the empty string.\n\n**Note:** the example value below must be regenerated after the BG-5368 proto rename (`platform_id` → `platform_code` in the signed CBOR) is deployed. The current bytes encode the old key name and will not match the live service once that spec bump lands.",
            "example": "rGhhc3NldF9pZGNFVEhocm9vdF9zdW1QAAAAAAAFqdbrDswEqF7ToGlyb290X2hhc2hYILcmBZJYkejY+cGpso9mLzS2b3+e6ezowYfMIGwGrEnEa3BsYXRmb3JtX2lkZkZQMkYyMGtzbmFwc2hvdF90cwBsYWNjb3VudF90eXBlaWF2YWlsYWJsZWxzcGVjX3ZlcnNpb25lMS4wLjBuZGJfc25hcHNob3RfaWRYQDEwZWRmZDZjNThiY2FlNzNiNWM2YmY5ZjllODhiNjljYjQ3MTczMjcxY2I1ZmMxMzYyMTIxYjE1NDRkMDRmM2NvYXNzZXRfcHJlY2lzaW9uAG9wdWJsaWNhdGlvbl9zZXEAc2V2aWRlbmNlX3BrZ19zaGEyNTZYIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdGFzc2V0X2NvbmZpZ192ZXJzaW9uYA=="
          },
          "signature": {
            "type": "string",
            "format": "byte",
            "description": "Detached Ed25519 signature over the decoded `commitment_cbor` bytes, base64-encoded.",
            "example": "vcJ7xd0OSR75rkBKW/yc5pO0n3FYdjjExFz+8CqgHvIPtZlQ9vf37wkntHgVCTDFNBttRJCfnNct9sI7W4DxDQ=="
          },
          "signing_key_id": {
            "type": "string",
            "description": "The key that produced `signature`. Look it up by `key_id` in `GET /por/keys` and verify against that entry's `public_key`. Do not gate on the key validity window in this release — see `GET /por/keys` for why it is not yet checkable.",
            "example": "Wallet:3b779fac-a9c2-4bef-a7b2-05ee8807af96"
          }
        }
      },
      "PostPorProofsBody": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "participant_code"
        ],
        "properties": {
          "participant_code": {
            "type": "string",
            "minLength": 1,
            "description": "Participant code whose inclusion proof is requested, within your own platform tree. This is the only field read. Send nothing else: a stray field is normally rejected with 400, and where request validation is handled at the edge instead it is ignored — but supplying a platform identifier that is not your own is denied with 403 rather than honoured.",
            "example": "NHQAYQ"
          }
        }
      },
      "PorProofLeaf": {
        "type": "object",
        "description": "The four inputs to the leaf hash. They are **not** hashed in the form shown here — each is encoded first (the strings are length-prefixed, the participant code is reduced to a fixed-width slot, and the balance becomes a fixed-width integer). Use the exact byte layout given in the Proof of Reserves verification spec issued with the reference verifier at onboarding; do not infer it from this field order. Phase 1 leaves carry no per-participant salt.",
        "required": [
          "platform_code",
          "asset",
          "participant_code",
          "balance"
        ],
        "properties": {
          "platform_code": {
            "type": "string",
            "example": "FP2F20"
          },
          "asset": {
            "type": "string",
            "example": "ETH"
          },
          "participant_code": {
            "type": "string",
            "description": "The participant code as requested. It is not carried into the hash in the clear — it is reduced to a fixed-width slot first.",
            "example": "NHQAYQ"
          },
          "balance": {
            "type": "string",
            "pattern": "^[0-9]+$",
            "description": "Balance in **base units** as a decimal string (`display value × 10^decimals`), not display units. Convert it to a 16-byte big-endian u128 before hashing.",
            "example": "1000000000000000000"
          }
        }
      },
      "PorProofStep": {
        "type": "object",
        "description": "One sibling on the path from the leaf up to the root. The leaf level is padded to a power of two with zero-sum sentinel leaves, so a step may be padding rather than another participant subtree; fold it exactly the same way either way.",
        "required": [
          "sibling_hash",
          "sibling_sum",
          "direction"
        ],
        "properties": {
          "sibling_hash": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "Sibling node hash, 32 bytes as lowercase hex.",
            "example": "fabbcb492818c929538f98f3df7c8db3c7d56a05197a73dbfd460c577fac29c5"
          },
          "sibling_sum": {
            "type": "string",
            "pattern": "^[0-9]+$",
            "description": "Sum of the sibling subtree in base units, as a decimal string. Note this is decimal while `root_sum` is hex. Use checked u128 addition at every step — an overflow is a verification failure, not a wrap.",
            "example": "6836672574609458722427808"
          },
          "direction": {
            "type": "string",
            "enum": [
              "LEFT",
              "RIGHT"
            ],
            "description": "Which side the **sibling** sits on at this level. `RIGHT` means the running node is the left input.",
            "example": "LEFT"
          }
        }
      },
      "PostPorProofsResponse": {
        "type": "object",
        "required": [
          "leaf",
          "proof_path",
          "root"
        ],
        "properties": {
          "leaf": {
            "$ref": "#/components/schemas/PorProofLeaf"
          },
          "proof_path": {
            "type": "array",
            "minItems": 1,
            "description": "Sibling path from the leaf to the root, ordered leaf-first. Never empty: the leaf level is padded to a power of two with a floor of two, so even a tree holding a single participant returns one step. The path length is therefore the log base 2 of the padded leaf count, not of the number of participants.",
            "items": {
              "$ref": "#/components/schemas/PorProofStep"
            }
          },
          "root": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PorRootCommitment"
              }
            ],
            "description": "The same signed commitment `GET /por/roots` returns for this tree and snapshot. Recompute the root from `leaf` + `proof_path` and require it to equal the `root_hash` and `root_sum` inside the verified `commitment_cbor`."
          }
        }
      },
      "PorAuthError": {
        "type": "object",
        "description": "Authorization failure raised by the Proof of Reserves routes themselves.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "example": "You are not allowed to access proof-of-reserves data for that platform."
          }
        }
      },
      "PorUpstreamError": {
        "type": "object",
        "description": "Error body forwarded verbatim from the Proof of Reserves service. Its shape differs from the `errors` envelope used elsewhere in this API.",
        "required": [
          "code",
          "message",
          "details"
        ],
        "properties": {
          "code": {
            "type": "integer",
            "description": "gRPC status code. This body is only returned on 404, where the value is always `5` (not found). Upstream server errors never reach you in this shape — this API replaces their body with its own `{\"error\": ...}` envelope.",
            "example": 5
          },
          "message": {
            "type": "string",
            "example": "commitment not found"
          },
          "details": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "example": []
          }
        }
      }
    }
  }
}
```