# Documents

> The Documents operations of the Hub REST API (6 operations).

URL: https://docs.synap.live/docs/reference/hub-api/documents · Generated from Synap-core/backend@c31e261ab (main)

Paths are relative to `/api/hub` on a pod.

## POST /documents

Create a document

Request body (`application/json`): CreateDocumentRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `userId` | string | yes |  |
| `workspaceId` | string \| null |  |  |
| `title` | string | yes |  |
| `content` | string |  |  |
| `type` | DocumentType |  |  |
| `reasoning` | string |  |  |
| `agentUserId` | string |  |  |
| `sourceMessageId` | string |  |  |
| `sessionId` | string |  |  |
| `expectedLabel` | string |  |  |
| `url` | string |  |  |
| `entityId` | string |  |  |
| `idempotencyKey` | string |  |  |

Responses:

- `200`: Created document
- `400`: Bad request
- `403`: Forbidden
- `500`: Internal error

## GET /documents/{documentId}

Fetch a document

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `documentId` | path | string | yes |  |
| `userId` | query | string | yes |  |
| `format` | query | "raw" \| "readable" |  |  |

Responses:

- `200`: Document
- `400`: Bad request
- `403`: Forbidden
- `500`: Internal error

## PATCH /documents/{documentId}

Replace a document's content (alias of the patch door)

Replaces a document's content with `content` — one `replace_all` op through the document patch door (prefer POST /documents/{documentId}/patch for section or text edits). Governed: an agent's full replacement is always a proposal for the document's editors; a person's is applied. It may not change a person's section or drop an embed (`allowRemovingEmbeds`). Pass `baseRevision` (GET → `revision`); without it the revision at filing is pinned. `content` is required; a `title` is refused with 400 (this route never renames).

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `documentId` | path | string | yes |  |

Request body (`application/json`): object

| Field | Type | Required | Description |
|---|---|---|---|
| `userId` | string | yes |  |
| `title` | string |  |  |
| `content` | string |  |  |
| `baseRevision` | integer |  |  |
| `allowRemovingEmbeds` | boolean |  |  |
| `agentUserId` | string |  |  |
| `sourceMessageId` | string |  |  |
| `sessionId` | string |  |  |

Responses:

- `200`: Updated document
- `400`: Bad request
- `403`: Forbidden
- `404`: Not found
- `500`: Internal error

## POST /documents/proposals

Replace a document's content (alias of the patch door)

A full replacement — one `replace_all` op through the document patch door (see POST /documents/{documentId}/patch). Governed: an agent's full replacement is always a proposal, and it may not change a person's section or drop an embed.

Request body (`application/json`): CreateDocumentProposalRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `documentId` | string | yes |  |
| `userId` | string | yes |  |
| `agentUserId` | string |  |  |
| `sourceMessageId` | string |  |  |
| `sessionId` | string |  |  |
| `proposedContent` | string | yes |  |
| `baseRevision` | integer |  |  |
| `allowRemovingEmbeds` | boolean |  |  |
| `reasoning` | string |  |  |

Responses:

- `200`: Created proposal
- `400`: Bad request
- `403`: Forbidden
- `500`: Internal error

## GET /documents/{documentId}/raw

Serve raw document content

Returns the document's raw bytes with the correct Content-Type (text/html, text/markdown, application/pdf, etc.). Useful for iframes, CLI downloads, and external consumers. Requires hub-protocol.read scope. Pass userId as a query param.

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `documentId` | path | string | yes |  |
| `userId` | query | string | yes |  |

Responses:

- `200`: Raw document bytes
- `400`: Bad request
- `403`: Forbidden
- `404`: Not found
- `500`: Internal error

## POST /documents/{documentId}/patch

Edit a document with ops

THE document edit door. Ops, applied in order: `upsert_section {id,title,body}` (one `::::synap-section` block by id), `replace_text {old,new}` (`old` must match EXACTLY once — otherwise 400 with the count), `append {body}`, `replace_all {content}` (needs `baseRevision`). Pass `baseRevision` from GET /documents/{id} (`revision`); a document that moved answers 409. Refused (403): an agent changing a person's section (replace_all included); removing an embed without `allowRemovingEmbeds: true`. Governed: applies or files a proposal (202); an agent's replace_all is always a proposal. The response carries `preview` (per section, before/after) and `diagnostics` (advisory: what will not render — never a refusal; `null` + `diagnosticsError` when the check could not run).

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `documentId` | path | string | yes |  |

Request body (`application/json`): PatchDocumentRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `userId` | string |  |  |
| `agentUserId` | string |  |  |
| `sourceMessageId` | string |  |  |
| `sessionId` | string |  |  |
| `baseRevision` | integer |  |  |
| `ops` | array<object \| object \| object \| object> | yes |  |
| `allowRemovingEmbeds` | boolean |  |  |
| `reasoning` | string |  |  |

Responses:

- `200`: Applied (or proposed — 202)
- `400`: Bad request
- `403`: Forbidden
- `404`: Not found
- `409`: The document moved
- `412`: Sections cannot be located
- `500`: Internal error
