Skip to content

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.

  • 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 client may do exactly one thing: record the decision they were asked for.

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.

Write the customer-facing description of a cycle.

Terminal window
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
}

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.

Terminal window
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:

Terminal window
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.

  • 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.
  • 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.
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