---
title: "Get a signed root commitment"
method: GET
path: "/por/roots/{asset}/{snapshot}"
tags: ["Proof of Reserves"]
---

# Get a signed root commitment

`GET /por/roots/{asset}/{snapshot}`

## Request

- Base URL: `https://api.cert.zerohash.com`
- URL: `https://api.cert.zerohash.com/por/roots/{asset}/{snapshot}`
- Auth: API key in header `X-SCX-API-KEY` and API key in header `X-SCX-PASSPHRASE`

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.

## Path parameters

- `asset` string, required
- `snapshot` string, required

## Headers

- `X-SCX-SIGNED` string, required
- `X-SCX-TIMESTAMP` string, required
- `X-Request-Id` string, uuid

## Response `200`

Signed root commitment

- PorRootCommitment: A signed Merkle Sum Tree root for one `(platform, asset)` pair. **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. The 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.
  - `platform_code` string, required: Platform the tree belongs to. Always your own platform — it is taken from your authenticated identity, never from the request.
  - `asset` string, required: Chain-qualified asset identifier, used in every leaf exactly as returned here — apply no normalization of your own.
  - `spec_version` string, required: Version of the Proof of Reserves commitment schema. A change here can change the leaf preimage or the signed payload layout.
  - `publication_seq` string, required: 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.
  - `db_snapshot_id` string, required: 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.
  - `snapshot_ts` string, date-time, required: 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.
  - `root_hash` string, required: Merkle root, 32 bytes as lowercase hex.
  - `root_sum` string, required: 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.
  - `commitment_cbor` string, byte, required: 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. Decode 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. In 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. **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.
  - `signature` string, byte, required: Detached Ed25519 signature over the decoded `commitment_cbor` bytes, base64-encoded.
  - `signing_key_id` string, required: 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.

## Other responses

- `304`: Your `If-None-Match` matched the current weak `ETag`. No body is returned.
- `400`: Bad Request
- `403`: Authorization failed. Three distinct causes share this status: - authentication rejected (missing, malformed, or expired API-key headers or signature) — the body is a plain string served as `text/html`, not JSON; - device verification could not resolve your platform, or your platform requires device identification headers that were not supplied; - a platform other than your own was named on the request. The body shape depends on which check rejected the call, so branch on the response content type rather than assuming JSON.
- `404`: No root is available. Either Proof of Reserves is not yet enabled for your platform, or no commitment exists for that asset and snapshot. While 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.
- `500`: Internal Server Error
- `503`: Service Unavailable

## Changes

- **2026-11-03** `47cf70ae701c`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/zerohash/apis/zerohash-api/changes/por/roots/:asset/:snapshot/get.md)

---

[API](https://skmtc.dev/zerohash/apis/zerohash-api.md) - [All operations](https://skmtc.dev/zerohash/apis/zerohash-api/llms.txt) - [OpenAPI document](https://skmtc.dev/zerohash/apis/zerohash-api/revisions/47cf70ae701c?raw)
