# Deliverables & approvals

**A deliverable is the client-facing face of one cycle.** It carries a title, a summary, a due date, the status you have declared to your customer, and a progress figure derived from the cycle's own tasks. Publishing it is what makes the work visible to a client collaborator; nothing reaches them before that.

## Prerequisites

- A cycle to publish. See [Cycles](/running/cycles/).
- A client collaborator on that project, if you want anyone outside the agency to see it — [Client access](/clients/access/).
- Any agency role can create, publish and unpublish. A `client` may do exactly one thing: record the decision they were asked for.

## Two separate statuses

A cycle's status describes what the **work** is doing. A deliverable's client status describes what the **customer has been told**. The two can differ: work can be merged internally and still be `in_review` for a customer who has not seen it yet.

| Client status | Meaning to your customer |
| --- | --- |
| `draft` | Not yet presented. |
| `in_review` | Shown, being looked at. |
| `delivered` | Handed over. |
| `accepted` | The customer accepted it. |

**Progress is derived, not stored.** It is computed from the cycle's own tasks on every read and returned with the evidence it was computed from — total tasks, completed tasks, and the cycle's status — so you can check the arithmetic. There is no progress field to set; sending one is refused.

Visibility changes only through publish and unpublish. Sending a `visibility` field is refused, so there is no other way to make internal work visible to a customer.

## Publish work to a customer

**Write the customer-facing description of a cycle.**

```bash
curl -sS -X PUT "$HLIX_BASE_URL/v1/api/deliverables/$CYCLE_ID" \
  -H "x-api-key: $HLIX_API_KEY" \
  -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \
  -H 'content-type: application/json' \
  -d '{"title": "Checkout redesign", "summary": "New payment step and address validation.", "clientStatus": "in_review"}'
```

Expected output — the deliverable, still internal until you publish it:

```json
{
  "id": "…",
  "cycleId": "…",
  "title": "Checkout redesign",
  "summary": "New payment step and address validation.",
  "dueDate": null,
  "visibility": "internal",
  "clientStatus": "in_review",
  "submittedRevision": null,
  "publishedAt": null
}
```
  **Make it visible to the project's client collaborators.**

```bash
curl -sS -X POST "$HLIX_BASE_URL/v1/api/deliverables/$CYCLE_ID/publish" \
  -H "x-api-key: $HLIX_API_KEY" \
  -H "X-Organization-Id: $HLIX_WORKSPACE_ID"
```

Expected output — `visibility` flips, a revision number is stamped, and `publishedAt` is set on the first publish:

```json
{
  "id": "…",
  "visibility": "client",
  "submittedRevision": 1,
  "publishedAt": "2026-08-27T16:02:44.000Z"
}
```

`POST …/unpublish` withdraws it again.
  **The projection plus derived progress.**

```bash
curl -sS "$HLIX_BASE_URL/v1/api/deliverables/$CYCLE_ID" \
  -H "x-api-key: $HLIX_API_KEY" \
  -H "X-Organization-Id: $HLIX_WORKSPACE_ID"
```

Expected output:

```json
{
  "id": "…",
  "title": "Checkout redesign",
  "visibility": "client",
  "clientStatus": "in_review",
  "progress": {
    "percent": 75,
    "evidence": { "totalTasks": 4, "completedTasks": 3, "cycleStatus": "review" }
  },
  "approval": { "…": "…" }
}
```
  ## Ask for a decision

An approval request names **one reviewer**, who must already be a collaborator on that project. The request binds itself to whatever is published at that moment — the revision number and a hash of its contents. You cannot name a different revision, so the decision always refers to exactly what the reviewer can see.

```bash
curl -sS -X POST "$HLIX_BASE_URL/v1/api/deliverables/$CYCLE_ID/approvals" \
  -H "x-api-key: $HLIX_API_KEY" \
  -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \
  -H 'content-type: application/json' \
  -d '{"reviewerId": "…"}'
```

Expected output — a pending request pinned to the published snapshot:

```json
{"id": "…", "status": "pending", "revision": 1, "revisionHash": "…"}
```

The reviewer records their answer from the portal, or through the one write a client is allowed:

```bash
curl -sS -X POST "$HLIX_BASE_URL/v1/api/deliverables/$CYCLE_ID/approvals/$APPROVAL_ID/decide" \
  -H "x-api-key: $CLIENT_API_KEY" \
  -H "X-Organization-Id: $HLIX_WORKSPACE_ID" \
  -H 'content-type: application/json' \
  -d '{"decision": "changes_requested", "comment": "The address step still accepts an empty postcode."}'
```

Expected output:

```json
{"id": "…", "status": "changes_requested", "comment": "The address step still accepts an empty postcode.", "decidedAt": "2026-08-27T17:41:10.000Z"}
```

The two decisions are `approved` and `changes_requested`. A `changes_requested` **must** carry a comment. The comment is also posted into the cycle's own conversation, so your team reads it alongside everything else said about that work.

A decision is immutable once made.

**An approval does not ship anything by default:** A customer saying "this looks right" is feedback about work they were shown, not an instruction to deploy. By default a client approval changes exactly one thing: the approval record.

To deploy a preview when a customer approves, opt in **per project** in the project's [delivery policy](/configure/delivery-policy/). It is off by default, and the response always says why nothing was deployed — the policy is off, previews are disabled, or the project has several repositories and none is marked primary.

**Not the same thing as a pending request:** A [pending request](/running/approvals/) is internal: an agent's question or a proposal for your own team. A deliverable approval asks your **customer** about work already shown to them. A client never sees the pending-request queue.

## What a client can and cannot do here

- **Reads** are scoped to projects they were invited to, and to **published** deliverables only. A draft is the agency's working state.
- **Every write is agency-only**, with the single exception of recording the decision they were asked for.
- A deliverable they may not see answers `404`, never `403` — "forbidden" would confirm that unpublished work exists.

## If publishing or deciding fails

- **`403` with `{"error": "Deliverables are managed by the agency"}`** — a client attempted any write other than its own decision.
- **`404` with `{"error": "Deliverable not found"}`** — no deliverable for that cycle, or the caller has no grant on its project. Do not read this as proof it does not exist.
- **`404` with `{"error": "Cycle not found"}`** — the cycle id is not in this Workspace.
- **`400` on `PUT`** — a field the request does not accept. `progress` and `visibility` are the usual ones; neither can be set directly.
- **`400` with `changes_requested needs a comment`** — a rejection with no words is not actionable. Say what is wrong.
- **`409` with `{"error": "already_decided"}`** — the request was already answered. Decisions are immutable.
- **`409` with `{"error": "not_published"}`** — the deliverable was withdrawn from the customer's view since they were asked. Whatever they were looking at is no longer visible, so a decision now would describe nothing. Publish and re-request.
- **`409` with `{"error": "revision_moved"}`** — you republished after asking. The reviewer is looking at a different snapshot than the one the request was pinned to. Ask again.
- **`409` with `{"error": "content_changed"}`** — the revision number matches but the contents do not. Same fix: ask again against what is published now.
- **The approval landed but nothing deployed.** That is the default. Check the project's delivery policy, and read the reason returned with the decision.

## Where to go next

| If you want to… | Read |
| --- | --- |
| Give a customer access to the project first | [Client access](/clients/access/) |
| See what the customer sees | [The client portal](/clients/portal/) |
| See the agency's own pending requests | [Approvals](/running/approvals/) |
| Charge for the accepted work | [Invoicing your client](/clients/invoicing/) |

## Next steps

[The client portal](/clients/portal/)
  [Client access](/clients/access/)
  [Approvals](/running/approvals/)
  [Invoicing your client](/clients/invoicing/)