# Proposals

> The Proposals operations of the Hub REST API (9 operations).

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

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

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

List proposals

Returns proposals for the authenticated user / a workspace. Default status filter is `pending`; `auto_approved` lists the audit receipts of agent writes that executed immediately under governance. `view=basic` returns the summary representation (identity + provenance, no `data`); the default `view=full` returns the detailed representation unchanged.

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `userId` | query | string |  |  |
| `workspaceId` | query | string |  |  |
| `status` | query | ProposalStatus |  | Defaults to `pending`. Use `all` to return every status. `auto_approved` returns the audit receipts of agent writes that were executed immediately under governance rather than queued for review. |
| `view` | query | "full" \| "basic" |  | `full` (DEFAULT) returns the complete proposal row including the unbounded `data` payload. `basic` returns identity + provenance scalars and a capped `summary`, with NO `data` — use it to enumerate a queue without paying for every payload. |
| `limit` | query | integer |  | Page size. Defaults to 50, clamped to 200. |
| `offset` | query | integer \| null |  | Rows to skip (native SQL OFFSET). Defaults to 0. The response carries `total` and `hasMore`, so a caller never has to infer the size of the queue from the size of the page. |

Responses:

- `200`: Proposals — full rows by default, basic rows when `view=basic`
- `400`: Malformed sessionId/workspaceId, or unknown status / view
- `403`: Missing scope
- `500`: Internal error

## POST /proposals

Create a proposal

Creates a pending proposal on behalf of an agent. Returns the new proposal id with status `pending`.

Request body (`application/json`): CreateProposalRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `workspaceId` | string \| null |  |  |
| `agentUserId` | string |  |  |
| `channelId` | string |  | Channel/thread originating the proposal. |
| `targetType` | string | yes | Subject type the proposal mutates (e.g. entity, view). |
| `targetId` | string | yes | ID of the target subject (existing record or stable temp id). |
| `proposalType` | string | yes | Sub-action, e.g. entity.create, view.update, vault.request. |
| `data` | object | yes | Free-form proposal payload — shape is targetType-specific. |
| `summary` | string |  |  |
| `sessionId` | string |  | Focus session ID to link this proposal to. |
| `sourceMessageId` | string |  | Message that originated the proposal — for event chain causality. |

Responses:

- `200`: Created proposal
- `400`: Missing required fields
- `403`: Forbidden
- `500`: Internal error

## GET /proposals/{id}

Get a single proposal by id

Fetches one proposal's full current state — including `data` (e.g. a capability.run proposal's `runResult` once approved). Callers previously had no single-lookup door and had to page through GET /proposals to find one by id.

Parameters:

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

Responses:

- `200`: The proposal
- `403`: Forbidden
- `404`: Proposal not found
- `500`: Internal error

## PATCH /proposals/{id}

Revise a pending proposal

Replaces a pending proposal's payload (e.g. AI revising its proposal after user feedback). Does NOT re-run the event pipeline.

Parameters:

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

Request body (`application/json`): UpdateProposalRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | object | yes | Updated proposal payload (replaces existing data). |
| `summary` | string |  | Human-readable summary of the revision. |

Responses:

- `200`: Updated proposal
- `400`: Bad request
- `403`: Forbidden
- `404`: Proposal not found
- `500`: Internal error

## POST /proposals/{id}/revert

Revert an approved proposal

Undoes the effect of an approved/auto-approved proposal. A create: deletes the entities/relations/documents the approval created. An entity update: restores each field and property from the record stamped when it applied, compare-and-set — a key edited since is kept and reported in `skipped`. An entity delete: restores the soft-deleted entity. An update with no apply-time record (applied before undo existed, or a non-entity update), and a delete of anything but an entity (a hard delete), cannot be reverted and return 501.

Parameters:

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

Request body (`application/json`): object

| Field | Type | Required | Description |
|---|---|---|---|
| `reason` | string |  |  |

Responses:

- `200`: Revert applied
- `400`: Not revertible (wrong status)
- `403`: Forbidden
- `404`: Proposal not found
- `500`: Internal error
- `501`: Revert not supported

## POST /proposals/{id}/approve

Approve a pending proposal

Approves a pending proposal, executing its effect immediately. Delegates to the canonical `proposals.approve` tRPC mutation, then re-fetches the proposal so the response carries its post-execution state (e.g. a capability.run's `data.runResult` — success + returned data, or the exact denial reason on failure) in the SAME round trip instead of requiring a separate GET.

Parameters:

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

Request body (`application/json`): object

| Field | Type | Required | Description |
|---|---|---|---|
| `identityResolution` | object |  | Weak same-name choice for a single entity/create. fill_empty, keep_existing, and use_capture require existingEntityId. separate does not. Absent ⇒ today's create. |
| `identityResolutionByRef` | object |  | Composite per-create choice, keyed by the entity ref dispositions use. One entry is not applied to any other op. |

Responses:

- `200`: Proposal approved
- `400`: Bad request
- `403`: Forbidden
- `404`: Proposal not found
- `500`: Internal error

## POST /proposals/{id}/reject

Reject a pending proposal

Rejects a pending proposal. Delegates to the canonical `proposals.reject` tRPC mutation.

Parameters:

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

Request body (`application/json`): object

| Field | Type | Required | Description |
|---|---|---|---|
| `reason` | string |  |  |
| `reasonCode` | "wrong_entity" \| "duplicate" \| "wrong_kind_or_facet" \| "wrong_link_type" \| "wrong_workspace" \| "bad_data" \| "not_relevant" \| "other" |  |  |

Responses:

- `200`: Proposal rejected
- `400`: Bad request
- `403`: Forbidden
- `404`: Proposal not found
- `500`: Internal error

## POST /proposals/dev-approval

File a dev-loop human gate (plan or deploy approval)

The typed door for the server-side dev loop's two stops: `dev.plan_approval` before the agent writes code, `dev.deploy_approval` before it ships a verified commit. Files a normal governed proposal against the FOCUS SESSION, so it inherits the `proposal.created` push (with its `synap://open/proposal/<id>` deep link), the `sessionId` provenance FK and every review surface. APPROVAL ONLY RECORDS THE DECISION on the session — the pod never runs `gateCommand` / `deployCommand`; the agent polls and acts on its own machine.

Request body (`application/json`): DevApprovalRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `type` | "dev.plan_approval" \| "dev.deploy_approval" | yes | Which dev-loop gate is being filed. |
| `workspaceId` | string \| null |  |  |
| `projectId` | string \| null |  |  |
| `channelId` | string |  |  |
| `sourceMessageId` | string |  |  |
| `payload` | object \| object | yes | The gate payload — shape is determined by `type`. |

Responses:

- `200`: The filed gate proposal
- `400`: Unknown `type`, or a payload the type's schema rejects
- `403`: Missing scope
- `500`: Internal error
