Documents

Get a signed download URL for a document

Returns a short-lived **signed download URL** for the document plus its metadata — the read counterpart of the presigned upload (`/projects/{id}/documents/upload-url`). The file bytes are fetched out of band by GETting `downloadUrl`; they never transit this JSON response. The Project-Information audience rule is enforced: a document the caller may not see returns `404` (indistinguishable from a missing one). `?kind` optionally asserts the library (`ohs`/`info`); a mismatch is `404`.

GET
/documents/{id}/download-url

Returns a short-lived signed download URL for the document plus its metadata — the read counterpart of the presigned upload (/projects/{id}/documents/upload-url). The file bytes are fetched out of band by GETting downloadUrl; they never transit this JSON response. The Project-Information audience rule is enforced: a document the caller may not see returns 404 (indistinguishable from a missing one). ?kind optionally asserts the library (ohs/info); a mismatch is 404.

Authorization

x-api-key<token>

The per-tenant API key, copied from Settings → API & integrations. Sent as the x-api-key request header. The key is tenant-scoped and acts with Admin-equivalent, tenant-wide access.

In: header

Path Parameters

id*string

Resource id.

Query Parameters

kind?string

Optionally assert the document's library.

Response Body

application/json

application/json

application/json

application/json

curl -X GET "https://example.com/documents/497f6eca-6276-4993-bfeb-53cbbbba6f08/download-url"
{  "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",  "kind": "ohs",  "title": "string",  "projectId": "5a8591dd-4039-49df-9202-96385ba3eff8",  "downloadUrl": "http://example.com",  "filename": "string",  "fileKind": "pdf"}
{  "error": {    "code": "unauthorized",    "message": "Missing or invalid API key."  }}
{  "error": {    "code": "not_found",    "message": "Not found."  }}
{  "error": {    "code": "validation",    "message": "One or more inputs are invalid.",    "fields": {      "fieldName": "A message explaining what's wrong with this field."    }  }}

Register an uploaded document POST

**Step 2 of the two-step presigned document upload.** Call this *after* you have PUT the file bytes to the `uploadUrl` from `POST /projects/{id}/documents/upload-url`. Pass the `uploadRef` from step 1 (or the `storagePath` + `kind`) plus the `title` (and `audience` for `kind: info`). Verifies the object actually landed in storage (and re-checks its real size/type against the ≤25 MB, PDF/JPG/PNG cap), then creates the document record. The new document then appears in `GET /documents?projectId={id}`. Reading document metadata stays on the read-only `/documents` collection; this project-scoped route is the upload (write) side. Admin or the project's Site Manager; blocked on read-only/archived projects.

Request a presigned document upload URL POST

**Step 1 of the two-step presigned document upload.** Validates the declared file (PDF/JPG/PNG, ≤25 MB intent) and returns a short-lived, single-use **signed PUT URL** (`uploadUrl`), the computed `storagePath`, and an opaque `uploadRef`. **The binary bytes never travel through this API call.** After this returns, the caller PUTs the raw file bytes directly to `uploadUrl` (a plain HTTP `PUT` with the file as the request body and the matching `Content-Type`) — out of band — then calls `POST /projects/{id}/documents` with the `uploadRef` to register the document (step 2). This is how binary upload is supported via the API/MCP without bytes passing through the JSON request (ADR 0001 §5). Admin or the project's Site Manager; blocked on read-only/archived projects. `audience` applies to Project Information (`kind: info`) only.