Treeship
Hub API

PUT /v1/receipt/{session_id}

Upload a Session Receipt v1 to the hub. DPoP authenticated. Idempotent.

Uploads a Session Receipt v1 to the hub and makes it available at the public URL https://treeship.dev/receipt/{session_id}, until the publishing dock takes it down with treeship hub unpublish (DELETE /v1/receipt/{session_id}).

In practice you do not call this directly. Run treeship session report and the CLI handles package discovery, body serialization, and DPoP signing for you.

Request

PUT /v1/receipt/{session_id}
Authorization: DPoP dck_9f8e7d6c
DPoP: eyJhbGciOiJFZERTQSIsInR5cCI6ImRwb3Arand0In0...
Content-Type: application/json

The request body is the canonical receipt.json from the .treeship package, sent verbatim.

Path parameters

NameDescription
session_idThe session id to upload. Must match the session.id field inside the request body.

Body validation

The hub parses the body as a Session Receipt v1 and rejects the request if:

  • The type field is not treeship/session-receipt/v1
  • session.id is missing
  • session.id does not match the path parameter (prevents cross-session overwrite via routing trick)
  • The body is empty or invalid JSON

Authentication

DPoP-authenticated. The Authorization header must be DPoP {dock_id}, and the DPoP header must be a valid JWT signed by the dock's private key with:

  • htm: "PUT" matching the HTTP method
  • htu: "https://api.treeship.dev/v1/receipt/{session_id}" matching the full request URL

See the Hub API overview for full DPoP details.

Write-once, not overwrite

A session_id slot is filled exactly once. A second PUT from the owning dock is idempotent only for a byte-identical replay -- the exact same request body succeeds again with the same response, refreshing the agent registry; anything else (a different receipt for the same id) is rejected with 409, never silently replaces the first.

A PUT from a different dock for a session_id that already exists is rejected with 403. Session ids are owned by the first dock to upload a receipt for them.

A PUT for a session_id the publishing dock has taken down is rejected with 410: the slot is retired permanently and is never refilled, by any dock.

Side effects

On success, the hub:

  1. Upserts the session into its sessions table with name, timestamps, status, participant counts, and the raw receipt JSON.
  2. Refreshes the per-ship agent registry by upserting every agent_graph.nodes entry into ship_agents with last_seen bumped to now.
  3. Returns the canonical receipt URL.

Response

{
  "session_id": "ssn_42e740bd9eb238f6",
  "receipt_url": "https://treeship.dev/receipt/ssn_42e740bd9eb238f6",
  "agents": 6,
  "events": 42,
  "uploaded_at": 1744185600
}
FieldTypeDescription
session_idstringEchoed session id
receipt_urlstringPublic URL for the uploaded receipt
agentsintegerNumber of agent nodes extracted from the receipt's agent graph
eventsintegerNumber of timeline events in the receipt
uploaded_atintegerUnix timestamp of the upload

Errors

StatusBodyCause
400{"error": "missing session_id in path"}Empty path parameter
400{"error": "session_id too long (max 128 chars)"}Path parameter over 128 characters
400{"error": "empty request body"}Zero-length body
400{"error": "invalid receipt JSON: ..."}Body does not parse as valid JSON
400{"error": "unsupported receipt type: ..."}type field is not treeship/session-receipt/v1
400{"error": "receipt missing session.id"}session.id field is missing
400{"error": "session_id in path does not match receipt body"}Path parameter does not match session.id in the body
401(DPoP error body)DPoP proof invalid, expired, replayed or bound to another URL
403{"error": "session_id is owned by another dock"}A different dock already published a receipt for this session_id
409{"error": "receipt already uploaded for this session; receipts are write-once"}Same dock, but this exact body doesn't match what's already stored (a byte-identical replay succeeds instead)
410{"error": "this receipt was taken down by its publisher; the session id is retired"}The publishing dock took this receipt down; the slot is never refilled
413{"error": "receipt body exceeds 10 MB limit"}Request body over 10 MB
401{"error": "invalid DPoP proof"}DPoP JWT invalid or expired
403{"error": "session_id is owned by another dock"}A different dock has already uploaded a receipt for this session id
500{"error": "failed to store receipt"}Database error

Example

SESSION_ID=ssn_42e740bd9eb238f6
curl -X PUT https://api.treeship.dev/v1/receipt/$SESSION_ID \
  -H "Authorization: DPoP dck_9f8e7d6c" \
  -H "DPoP: eyJhbGciOiJFZERTQSJ9..." \
  -H "Content-Type: application/json" \
  --data @.treeship/sessions/$SESSION_ID.treeship/receipt.json

The receipt body is sent verbatim from disk. Do not re-serialize it, because re-serializing would produce different whitespace and break the determinism check that treeship package verify runs on the receiving end.