# PUT /v1/receipt/{session_id}
Source: https://docs.treeship.dev/api/receipt-put

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

Uploads a [Session Receipt v1](/docs/concepts/session-receipts) to the hub and makes it available at the permanent public URL `https://treeship.dev/receipt/{session_id}`.

In practice you do not call this directly. Run [`treeship session report`](/docs/cli/session#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

| Name         | Description                                                                          |
| ------------ | ------------------------------------------------------------------------------------ |
| `session_id` | The 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](/docs/api/overview) for full DPoP details.

## Idempotency

PUT is idempotent for the owning dock. A second PUT from the same dock for the same `session_id` overwrites the first and refreshes the agent registry.

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.

## 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

```json
{
  "session_id": "ssn_42e740bd9eb238f6",
  "receipt_url": "https://treeship.dev/receipt/ssn_42e740bd9eb238f6",
  "agents": 6,
  "events": 42,
  "uploaded_at": 1744185600
}
```

| Field         | Type    | Description                                                    |
| ------------- | ------- | -------------------------------------------------------------- |
| `session_id`  | string  | Echoed session id                                              |
| `receipt_url` | string  | Permanent public URL for the uploaded receipt                  |
| `agents`      | integer | Number of agent nodes extracted from the receipt's agent graph |
| `events`      | integer | Number of timeline events in the receipt                       |
| `uploaded_at` | integer | Unix timestamp of the upload                                   |

## Errors

| Status | Body                                                          | Cause                                                               |
| ------ | ------------------------------------------------------------- | ------------------------------------------------------------------- |
| `400`  | `{"error": "missing session_id in path"}`                     | Empty path parameter                                                |
| `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`  | `{"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

```bash
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
```

<Callout type="info">
  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.
</Callout>