Decision commit

How Cenelira binds approval to the version reviewers actually saw.

Every Cenelira external review link captures a SHA-256 fingerprint of the schedule's content, delivery settings and verified media at issue time. That fingerprint is the schedule's current reviewVersion. The reviewer's decision is committed inside a transaction that recomputes the fingerprint and whose WHERE clause also matches the current reviewVersion.

If the schedule or the media it sends changed after the link was issued, the fingerprint changed. No decision is saved, the link is reclassified as SUPERSEDED, and the external decision endpoint returns HTTP 410 with a named supersessionReason. This page describes that binding, names the code that enforces it, and maps it to the proof fields you keep when you export.

Definition

What is version-bound approval?

A reviewer's decision on a Cenelira external review link is bound to the SHA-256 fingerprint of the schedule's content, delivery settings and verified media at the moment the link was issued. The fingerprint is written as rv2: followed by the digest.

Mechanism

Where is the binding enforced?

It is enforced in the decision transaction. The link carries the captured scheduleVersion. The server recomputes the fingerprint of the post as it is now and compares the two, and the update targets the schedule row with a WHERE clause that also matches the current reviewVersion. If the content, the settings or the selected media changed, the link is marked SUPERSEDED and no decision is saved.

Limit

What is out of scope?

On a shared review link, reviewer identity is self-declared: the reviewer types their own name and email, which are not verified. A saved client contact on a personal link types nothing and is identified only by holding that link. A decision already saved against a matching earlier version stays saved, and this page is about refusing a new decision whose captured version has since changed. An approval is bound to the verified original file, or the verified rendition that publishing selects, together with a versioned ID for the rules that prepare media at publish time. It does not claim that the bytes a platform displays, or the bytes produced by on-the-fly image preparation at publish time, are identical to what the reviewer saw.

Who this is for

Built for operators who have been caught out by an out-of-date approval

Version-bound approval helps most where an approval of the version someone read must be kept apart from a general yes. It helps less when the content rarely changes between approval and publishing.

For
  • Agency operators whose clients approve through an external link, and whose content changes between approval and publishing as feedback on the copy arrives.
  • Brand teams who need a record that the reviewer approved the exact version that went out, including the caption, first comment, destination and settings for each platform.
  • Operations leads who want the refusal enforced in the decision transaction rather than in a written review procedure.
Not for
  • Teams that approve once and never edit. The binding only acts on an edit made after the link is issued and before the decision is saved.
  • Workflows where a later edit should quietly keep the earlier approval. This page describes the opposite choice.
  • Teams that rely on an external reviewer's identity as a legal signature. On a shared review link that identity is self-declared: the reviewer types their own name and email, and Cenelira does not verify them. On a personal link a client contact is identified only by holding the link.

Failure scenario

How an out-of-date approval happens, step by step

A tool without version-bound approval attaches the "yes" to the schedule rather than to the version. The reviewer approves Monday's draft, an operator changes the caption on Tuesday, and on Wednesday the changed draft publishes under Monday's approval, so the reviewer never saw the copy that went out.

  1. A scheduler issues an external review link. The link captures the schedule's current reviewVersion as scheduleVersion and writes that value onto the link record.
  2. The reviewer opens the link, reads the caption and media, and approves. The server resolves the link token to the link row.
  3. Before the decision is saved, an operator edits the caption, swaps a media file or changes the destination. refreshScheduleReviewVersion in src/lib/schedules/reviewVersion.ts recomputes the SHA-256 fingerprint and writes a new reviewVersion onto the schedule row.
  4. The decision transaction recomputes the fingerprint of the post as it is now and compares it with the link's scheduleVersion; its updateMany also has a WHERE clause that matches the schedule id, the item state, and reviewVersion: item.scheduleVersion. Because the current reviewVersion has changed, no decision is saved.
  5. The link is marked SUPERSEDED with supersessionReason: 'review_version_changed', and the external decision API returns HTTP 410 Gone. The reviewer sees a final card titled "Content changed" with the badge "Superseded".
  6. A new review link has to be issued for the new version, and no decision is ever recorded against a fingerprint that did not match.

Mechanism

Three version-bound supersession reasons, each answered with HTTP 410 Gone

The fingerprint is computed by computeScheduleReviewVersion and written by refreshScheduleReviewVersion in src/lib/schedules/reviewVersion.ts. The review link service carries the captured scheduleVersion on the link record. When the decision is saved, the decision transaction recomputes the fingerprint and refuses to update any row whose current reviewVersion does not equal the captured one. The ScheduleReviewLinkSupersessionReason union has eight values, and the three below are the ones about the version.

  • review_version_changed

    When the external decision is saved, the server recomputes the reviewVersion of the post as it is at that moment, from its content, delivery settings and verified media. It does not equal the scheduleVersion captured on the link, so the link is superseded before a decision is recorded.

    HTTP 410 link_superseded
  • review_version_missing

    The schedule has no reviewVersion when the decision is saved, because the media that publishing would send is not a verified copy, so the link cannot check the captured fingerprint against the current one and the decision is refused.

    HTTP 410 link_superseded
  • delivery_set_changed

    The delivery set captured on the link no longer matches the schedule's current scope, because a delivery was added or removed after the link was issued.

    HTTP 410 link_superseded

Proof record

The proof record names the version and who decided

The binding is visible outside the server. Five fields in the 17-field proof record carry the version and the approval state, and they are kept in every export.

versionFingerprint is resolved by collectWorkspaceProofRecords in src/lib/exports/workspaceProofExport.ts as the first non-empty value in this order: the SCHEDULE_HANDOFF_COMPLETED event's reviewVersion, the latest REVIEW_APPROVED or EXTERNAL_REVIEW_APPROVED event's reviewVersion, or the current schedule row's reviewVersion. When none of those sources carry a value, the field is null.

approverIdentity stores the label of who decided, read from the latest approval event. For an internal decision that is the workspace user label, and for a decision on a shared review link it is the self-declared reviewer label captured on the link. For a saved client contact using a personal link it is the contact's name, and for an approval recorded for the client it is the team member who recorded it.

approverSource records how the decision was captured. It is internal for REVIEW_APPROVED events and external_self_declared for EXTERNAL_REVIEW_APPROVED events made on a shared review link. It is client_contact when a saved client contact approved through the personal link issued to them, and team_on_behalf_of_client when a workspace owner or admin recorded that the client approved, in which case the approver named is that team member. The row always shows where the decision came from.

approvalTimestamp records when the decision was committed, taken from the approval event's createdAt. Paired with versionFingerprint, it answers "which version was approved, and when" from a single row.

invalidationReason is derived from a REVIEW_LINK_EXPIRED or REVIEW_LINK_SUPERSEDED event for the same schedule. Expired links write expired; superseded links write the supersession reason recorded on the event. The field is null when no such event exists for the schedule. The signed PDF and the unsigned CSV file both carry these columns. See the proof record schema for all 17 fields.

What this does not claim

Where the binding stops

Version-bound approval is one check on the way to publishing, and this page says where it stops.

  • External reviewer identity on a shared review link is self-declared, and a personal link identifies a client contact only by possession. The binding refuses out-of-date decisions, but it does not verify the person who approved.
  • A decision that was already committed against a matching earlier reviewVersion remains committed. The later edit does not erase the prior approval from the proof record.
  • An approval is bound to the verified original file, or the verified rendition that publishing selects, together with a versioned ID for the rules that prepare media at publish time. It does not claim that the bytes a platform displays, or the bytes produced by on-the-fly image preparation at publish time, are identical to what the reviewer saw.
  • The binding runs when an external decision is saved. A workspace member's decision is checked the same way, and a publish that relies on an approval recomputes the fingerprint first: an approved post that is no longer the approved version is held and needs approval again. Status changes and other refusals at publish time have their own supersession reasons, outside the three about the version.
  • This page describes how Cenelira enforces the binding when a decision is saved, and it makes no claim about what other approval tools store in their own version history.
  • The fingerprint covers the fields in ScheduleReviewVersionPayload, and an edit to a field outside that payload does not supersede a link on its own.

FAQ

Short answers for operators

What counts as "an edit" for version-bound approval?

Anything that changes the schedule's reviewVersion fingerprint: caption, first comment, publishAt, destination, publish mode, connected account, media bundle order, the verified digest of each media file or of the rendition that publishing selects, per-platform settings (for example TikTok privacy level and YouTube upload kind), composition signatures, and the versioned ID of the publish-time media rules. These are the fields in ScheduleReviewVersionPayload that the SHA-256 fingerprint is computed from.

What does the external reviewer see on a stale link?

The review page replaces the content with a final card that carries the badge "Superseded" and the title "Content changed", and tells the reviewer that a newer link is needed to review the current version. Below the card the page shows the 8-character link fingerprint, the target account label and the decision timestamp when they were recorded, the supersession reason with underscores replaced by spaces, and a prompt to contact the team that sent the link. The original caption, media and version are no longer shown, so the link becomes a receipt rather than a copy of the content the reviewer first saw.

Does this invalidate approvals that were already recorded?

No. A decision already saved against a matching reviewVersion stays saved. Version-bound approval refuses a new decision whose captured version no longer matches, and the later edit does not erase the earlier approval from the proof record.

Where does the version fingerprint show up on the proof record?

In the 17-field proof record, versionFingerprint is resolved by collectWorkspaceProofRecords in src/lib/exports/workspaceProofExport.ts as the first non-empty value across the SCHEDULE_HANDOFF_COMPLETED event's reviewVersion, the latest REVIEW_APPROVED or EXTERNAL_REVIEW_APPROVED event's reviewVersion, or the current schedule row's reviewVersion. approverIdentity, approverSource, and approvalTimestamp are read from the latest approval event. approverSource defaults to internal for REVIEW_APPROVED events and external_self_declared for EXTERNAL_REVIEW_APPROVED events. invalidationReason is derived from a REVIEW_LINK_EXPIRED or REVIEW_LINK_SUPERSEDED event for the same schedule: expired links write 'expired'; superseded links write the supersession reason. The field is null when no such event exists.

Is this different from version history in other tools?

This page describes how Cenelira enforces the version binding when a decision is saved, and it makes no claim about what other approval tools store in their own version history. The difference you can test is that the decision transaction refuses to save when the captured fingerprint does not equal the current one.

Last reviewed 2026-04-24

Get notified when the approval integrity checklist ships.

A short checklist for teams whose clients approve by link. It covers what should cancel an approval, how to read the proof fields that record the decision, and what an external reviewer sees on an out-of-date link. Leave an email and we will send it when the checklist is ready.

We use this only to tell you when the relevant update ships. Privacy.

Version-bound approval | Cenelira