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
Section titled “Prerequisites”- A cycle to publish. See Cycles.
- A client collaborator on that project, if you want anyone outside the agency to see it — Client access.
- Any agency role can create, publish and unpublish. A
clientmay do exactly one thing: record the decision they were asked for.
Two separate statuses
Section titled “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
Section titled “Publish work to a customer”Write the customer-facing description of a cycle.
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:
{ "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.
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:
{ "id": "…", "visibility": "client", "submittedRevision": 1, "publishedAt": "2026-08-27T16:02:44.000Z"}POST …/unpublish withdraws it again.
The projection plus derived progress.
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:
{ "id": "…", "title": "Checkout redesign", "visibility": "client", "clientStatus": "in_review", "progress": { "percent": 75, "evidence": { "totalTasks": 4, "completedTasks": 3, "cycleStatus": "review" } }, "approval": { "…": "…" }}Ask for a decision
Section titled “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.
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:
{"id": "…", "status": "pending", "revision": 1, "revisionHash": "…"}The reviewer records their answer from the portal, or through the one write a client is allowed:
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:
{"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.
What a client can and cannot do here
Section titled “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, never403— “forbidden” would confirm that unpublished work exists.
If publishing or deciding fails
Section titled “If publishing or deciding fails”403with{"error": "Deliverables are managed by the agency"}— a client attempted any write other than its own decision.404with{"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.404with{"error": "Cycle not found"}— the cycle id is not in this Workspace.400onPUT— a field the request does not accept.progressandvisibilityare the usual ones; neither can be set directly.400withchanges_requested needs a comment— a rejection with no words is not actionable. Say what is wrong.409with{"error": "already_decided"}— the request was already answered. Decisions are immutable.409with{"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.409with{"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.409with{"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
Section titled “Where to go next”| If you want to… | Read |
|---|---|
| Give a customer access to the project first | Client access |
| See what the customer sees | The client portal |
| See the agency’s own pending requests | Approvals |
| Charge for the accepted work | Invoicing your client |