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

# Get a signed root commitment

Return the signed Merkle Sum Tree root commitment for one asset in your platform's tree.

The tree is always your own: the platform is taken from your authenticated identity and cannot be supplied in the request, so another platform's roots are not addressable through this endpoint.

Verify the response by checking `signature` over the raw bytes of `commitment_cbor` using the key whose `key_id` matches `signing_key_id` in `GET /por/keys`, then decoding `commitment_cbor` and reading the committed values from it. The sibling JSON fields are an unsigned convenience view, and they are not a complete rendering of the signed payload.

This API does not cache these responses and sets no `Cache-Control`, but responses do carry a weak `ETag`, so a request sending `If-None-Match` can receive `304 Not Modified` with no body. Nothing here suppresses caching by an intermediary, so treat `latest` as a moving target.

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/roots/{asset}/{snapshot}": {
      "get": {
        "tags": [
          "Proof of Reserves"
        ],
        "summary": "Get a signed root commitment",
        "description": "Return the signed Merkle Sum Tree root commitment for one asset in your platform's tree.\n\nThe tree is always your own: the platform is taken from your authenticated identity and cannot be supplied in the request, so another platform's roots are not addressable through this endpoint.\n\nVerify the response by checking `signature` over the raw bytes of `commitment_cbor` using the key whose `key_id` matches `signing_key_id` in `GET /por/keys`, then decoding `commitment_cbor` and reading the committed values from it. The sibling JSON fields are an unsigned convenience view, and they are not a complete rendering of the signed payload.\n\nThis API does not cache these responses and sets no `Cache-Control`, but responses do carry a weak `ETag`, so a request sending `If-None-Match` can receive `304 Not Modified` with no body. Nothing here suppresses caching by an intermediary, so treat `latest` as a moving target.\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 root to return. 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 response and pass it here — the service looks up that exact snapshot.",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "example": "latest"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Signed root commitment",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PorRootCommitment"
                }
              }
            }
          },
          "304": {
            "description": "Your `If-None-Match` matched the current weak `ETag`. No body is returned."
          },
          "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 root is available. Either Proof of Reserves is not yet enabled for your platform, or no commitment exists for that asset and snapshot.\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"
          }
        }
      },
      "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": []
          }
        }
      }
    }
  }
}
```