---
title: "Start an upload for a document"
method: POST
path: "/api/v1/documents/{id}/content"
tags: ["Files"]
---

# Start an upload for a document

`POST /api/v1/documents/{id}/content`

## Request

- Base URL: `http://localhost:5180`
- URL: `http://localhost:5180/api/v1/documents/{id}/content`
- Auth: API key in cookie `better-auth.session_token`

Mints the key the bytes will go to and records it against the document, then answers with the largest body this endpoint takes so a client can refuse an oversized file before it streams anything. The key is the SHA-256 you declare, so starting the same upload twice is the same key twice and the bytes are written once. Send the file to PUT on this same address afterwards. A file larger than one request (a PDF or office document of up to 104857600 bytes, or a clip) opens an upload in parts instead: `upload` names the part size and count, the parts go to PUT …/content/parts/{part} in order, and POST …/content/complete closes it, with the declared sha256 being the chain of the parts’ hashes. A document that already holds bytes is not replaced here.

The bytes go to the store in the region of the property the document is filed against, read through a lease, a unit or a tenant to the building, and stay there. A document about no property goes to the account’s own region.

## Path parameters

- `id` string, required

## Request body

- object
  - `contentType` string, required: What the client says the file is. Nothing opens the file to check it, and it is handed back on download exactly as it is given here, which is why it takes no parameters and why a download is always an attachment.
  - `size` integer, required: How long the file is. The body of the PUT that follows has to be exactly this many bytes or it is refused, and what the account records once they land is R2’s own count of them.
  - `sha256` string, required: The SHA-256 of the file, lower-case hex. It is the last part of the key the bytes go to, so sending the same file twice writes the same object once, and bytes that hash to anything else are refused.

## Response `200`

The upload is open, or the bytes were already there.

- object
  - `ok` unknown, required
  - `data` object, required
    - `documentId` string, required: Stable record id, e.g. `lse_012`.
    - `state` 'pending' | 'stored', required: `pending` once a key is minted and before any bytes land, `stored` once they have. Only a `stored` document can be downloaded.
    - `bytes` integer, required: The size that was claimed while pending, and R2’s own count once stored. A body that is not the claimed length never becomes the second.
    - `contentType` string, required: What the client said the file is while the upload is pending, and what the store holds against the object once it is stored. Nothing anywhere opens the file to check it, so it stays a claim, which is why a download is served as an attachment whatever it says.
    - `checksum` string, required: The digest the object is addressed by, lower-case hex.
    - `checksumAlgo` 'md5' | 'sha256', required: Which digest `checksum` is. R2 answers a single PUT with an MD5 and a multipart upload with a composite ETag, so it is a field rather than an assumption.
    - `uploadedAt` string, nullable, required: ISO 8601 timestamp.
    - `mediaKind` 'photo' | 'video' | 'document', required: What the account calls this file, derived from its content type. A gallery asks for `photo` rather than reading types itself.
    - `maxBytes` integer, required: The largest body this endpoint takes. Read it before streaming: a file over it is refused by the platform, and on some plans that refusal never reaches the API to be explained.
    - `upload` object: Present when the file is too large for one PUT: send it to PUT …/content/parts/{n} instead, then POST …/content/complete.
      - `partBytes` integer, required: Every part but the last is exactly this long.
      - `parts` integer, required: How many parts the declared size splits into.
  - `meta` object, required
    - `requestId` string, required: Echoed in the x-request-id header. Quote it in a bug report.

## Other responses

- `400`: The body was not in the shape this endpoint expects, or the file is not a kind the account takes.
- `401`: No session. Sign in first.
- `404`: No document with that id belongs to you.
- `409`: This document already holds bytes and the ones declared are different, or the account is already holding its 536870912-byte limit and the file would not fit. Replacing a file is not something this endpoint does.
- `429`: Over the file budget, which is counted per owner and separately from the rest of the API.

---

[API](https://skmtc.dev/dardaris/apis/dardaris-api.md) · [All operations](https://skmtc.dev/dardaris/apis/dardaris-api/llms.txt) · [OpenAPI document](https://skmtc.dev/dardaris/apis/dardaris-api/revisions/4aff33835c73?raw)
