GET /v1/merkle/:artifactId
Retrieve a self-contained Merkle inclusion proof for an artifact.
Request
GET /v1/merkle/:artifactId| Parameter | Type | Description |
|---|---|---|
:artifactId | string | The artifact ID (e.g., art_f7e6d5c4b3a2f7e6) |
No authentication required. Proofs are public so anyone can verify.
Response
Returns a self-contained ProofFile JSON. This includes everything needed to verify that the artifact is included in a specific Merkle tree checkpoint -- no further requests to Hub required.
{
"artifact_id": "art_f7e6d5c4b3a2f7e6",
"artifact_summary": {
"actor": "agent://ci",
"action": "test suite",
"timestamp": "2026-03-28T14:30:00Z",
"key_id": "ship_a1b2c3d4"
},
"inclusion_proof": {
"leaf_index": 2048,
"leaf_hash": "9d2fa1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0",
"path": [
{ "direction": "Right", "hash": "1a2b3c4d..." },
{ "direction": "Left", "hash": "5e6f7a8b..." },
{ "direction": "Right", "hash": "9c0d1e2f..." }
],
"algorithm": "sha256-rfc9162"
},
"checkpoint": {
"index": 42,
"root": "sha256:7e3a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0",
"tree_size": 4096,
"height": 12,
"signed_at": "2026-03-28T15:00:00Z",
"signer": "ship_a1b2c3d4",
"public_key": "base64url-encoded-ed25519-public-key",
"signature": "base64url-encoded-ed25519-signature",
"algorithm": "sha256-rfc9162"
}
}ProofFile fields
| Field | Type | Description |
|---|---|---|
artifact_id | string | The artifact this proof covers |
artifact_summary | object | Human-readable context about the artifact |
artifact_summary.actor | string | Actor URI that created the artifact |
artifact_summary.action | string | Label describing the action |
artifact_summary.timestamp | string | RFC 3339 timestamp of the artifact |
artifact_summary.key_id | string | Key ID of the signer |
inclusion_proof | object | Merkle inclusion proof from leaf to root |
inclusion_proof.leaf_index | integer | Position of the artifact in the tree |
inclusion_proof.leaf_hash | string | Hex-encoded SHA-256 hash of the artifact ID |
inclusion_proof.path | array | Sibling hashes forming the path from leaf to root |
inclusion_proof.path[].direction | string | Left or Right -- position of the sibling relative to the current node |
inclusion_proof.path[].hash | string | Hex-encoded hash of the sibling node |
inclusion_proof.algorithm | string | Merkle algorithm: sha256-rfc9162 (current) or sha256-duplicate-last (v1 legacy) |
checkpoint | object | Signed snapshot of the Merkle tree at proof time |
checkpoint.index | integer | Checkpoint sequence number |
checkpoint.root | string | Root hash in sha256:<hex> format |
checkpoint.tree_size | integer | Number of leaves in the tree |
checkpoint.height | integer | Tree height (ceil of log2 of tree_size) |
checkpoint.signed_at | string | RFC 3339 timestamp of the checkpoint |
checkpoint.signer | string | Key ID of the checkpoint signer |
checkpoint.public_key | string | Base64url-encoded Ed25519 public key |
checkpoint.signature | string | Base64url-encoded Ed25519 signature |
checkpoint.algorithm | string | Merkle algorithm used to build the tree |
How to verify
The proof is self-contained. To verify locally:
- Recompute the leaf hash:
sha256(artifact_id)and compare toinclusion_proof.leaf_hash - Walk the
inclusion_proof.pathfrom leaf to root, combining hashes at each level based ondirection - Compare the computed root against
checkpoint.root - Verify the checkpoint signature using the embedded
public_key
CLI verification
treeship merkle verify proof.jsonmerkle proof emits the proof and merkle verify checks it; there is no
single-step --verify flag. Pipe one into the other:
treeship merkle proof art_f7e6d5c4b3a2f7e6 > proof.json
treeship merkle verify proof.jsonErrors
| Status | Body | Cause |
|---|---|---|
400 | {"error": "missing artifact id"} | No artifact ID in the URL path |
404 | {"error": "proof not found"} | No proof exists for this artifact |
Example
curl https://api.treeship.dev/v1/merkle/art_f7e6d5c4b3a2f7e6POST /v1/merkle/proof
Publish an inclusion proof binding an artifact to a checkpoint. DPoP-authenticated
— treeship merkle publish calls this; you rarely call it by hand.
Body
| Field | Type | Required | Description |
|---|---|---|---|
artifact_id | string | Yes | Artifact the proof is for |
checkpoint_id | integer | Yes | Checkpoint the artifact is proved into |
leaf_index | integer | No | Index of the leaf in the tree |
leaf_hash | string | No | Hash of the leaf |
proof_json | string | Yes | The serialized inclusion proof |
Response 200 — { "artifact_id": "art_…", "stored": true }
The caller must own both the artifact and the checkpoint. This is object-level
authorization, not a formality: because proofs are stored per
(artifact_id, checkpoint_id) and the highest checkpoint wins, any authenticated
dock that could write a proof it did not own could shadow another dock's proof and
make anchored artifacts read as un-anchored.
| Status | Cause |
|---|---|
400 | Invalid JSON, or artifact_id / checkpoint_id / proof_json missing |
401 | Missing or invalid DPoP Authorization header |
403 | The checkpoint or the artifact belongs to a different dock |
404 | Checkpoint or artifact not found |