Treeship
Hub API

GET /v1/receipt/{session_id}

Fetch a Session Receipt by session id. Fully public, no authentication required.

Returns the Session Receipt v1 JSON for a given session id. This endpoint is fully public: no authentication, no token, no expiry. The URL is permanent.

It is the A2A verification endpoint and the shareable-link primitive for Session Receipts. Any client (human, browser, agent, crawler) can fetch this URL programmatically and verify the receipt locally.

Request

GET /v1/receipt/{session_id}

No headers required. No authentication.

Path parameters

NameDescription
session_idThe session id to fetch

Response shapes

200 OK

{
  "type": "treeship/session-receipt/v1",
  "session": { ... },
  "participants": { ... },
  "agent_graph": { ... },
  "timeline": [...],
  "side_effects": { ... },
  "artifacts": [...],
  "merkle": { ... },
  "render": { ... }
}

The body is the raw receipt.json stored at upload time, returned verbatim. See the Session Receipts concept page for the full schema.

Response headers include:

Content-Type: application/json
Cache-Control: public, max-age=86400, immutable

The immutable cache directive reflects write-once storage: the owning dock's PUT is idempotent only for a byte-identical replay (anything else is a 409), and a different dock's PUT is rejected with 403, so a session id's bytes never change while the receipt is live -- except by the one deliberate exception, its publisher's own takedown, which clears the body for good and answers 410 from then on.

400 Bad Request

{ "error": "missing session_id in path" }

Also returned, with "session_id too long", for a path parameter over 128 characters.

403 Forbidden

{ "error": "session still open" }

Returned when a row exists for this session_id but no receipt has been uploaded yet. This shape lets A2A consumers distinguish "session exists but is still running" from "no such session".

410 Gone

The publishing dock took the receipt down with DELETE /v1/receipt/{session_id}. The body is gone for good; the response names the takedown and, when one was given, the reason. It is served with Cache-Control: no-store.

{
  "error": "this receipt was taken down by its publisher; the session id is retired",
  "session_id": "ssn_42e740bd9eb238f6",
  "tombstoned_at": 1758931200,
  "reason": "leaked local paths"
}

404 Not Found

{ "error": "session not found" }

Returned when no row exists for this session_id.

Local verification

Fetching the receipt from the hub is a convenience. The trust anchor is running treeship package verify against the downloaded bytes:

# Fetch
curl -o receipt.json https://api.treeship.dev/v1/receipt/ssn_42e740bd9eb238f6

# Place it inside a .treeship directory so the verifier can read it
mkdir -p verify-work/ssn.treeship
mv receipt.json verify-work/ssn.treeship/

# Verify offline
treeship package verify verify-work/ssn.treeship

The determinism and merkle_root checks here are self-consistency only, not a signature check, and cover only the artifact ids in merkle: they confirm the JSON round-trips and the stated root matches, not that the receipt is genuine. The timeline, side_effects, tool_usage and narrative are unsigned and untouched by either check -- a party who can write the stored bytes (a compromised hub, a MITM before TLS) can fabricate a receipt.json from scratch with correct internal hashes and pass both. The check that reaches a real, actor-signed anchor is the session's record.json (via package verify's receipt_binding row), present only in a full .treeship package, never in a bare fetched receipt.json.

Example

curl https://api.treeship.dev/v1/receipt/ssn_42e740bd9eb238f6
# Programmatic fetch for A2A trust filtering
curl -sS https://api.treeship.dev/v1/receipt/$SESSION_ID \
  | jq '.participants.total_agents, .merkle.root'

Share URLs of the form https://treeship.dev/receipt/<session_id> in blog posts, dashboards, pull requests, and incident reports. Every recipient can fetch without auth and verify locally. No keys, no tokens, no expiry.