Skip to content
Updated Sep 12, 2026 by Barča Dvořáková · Owner: analysisactiveconceptgenericapi-a Edit on GitHub

Versioning — API Analysis (API-A) ​

Concept layer — frozen. The Versioning generic. Nothing here is written by a normal playbook run; a project's own feature analysis is the live document and takes every edit. This layer names no project and links to none — the dependency runs one way, from an application to its concept.

Feature: Versioning

Paths below are written without any version or gateway prefix: how a project versions and mounts the routes is an application decision, not part of the concept. Success envelope: { data, status, message?, requestId }. Error envelope: { status, error: { code, message, details }, requestId }.

Endpoints are written here as one route set operating on a caller-determined scope. A project that separates the default scope from per-scope overrides — different owners, different permissions, different screens — mounts two route sets of this same shape; see the decision below.


Decisions this surface forces ​

There is no update endpoint, and that absence is the whole contract. A change is made by creating a new version. If a project adds a general update endpoint, immutability stops being true the moment it does — see Immutability on the feature page — and every reading of the history downstream becomes unreliable, including the ones this API returns. Where a project needs an editing experience, the two shapes that preserve the guarantee are a draft state whose content freezes on first taking effect, and an update restricted to the description. Both must be enforced below this API; an endpoint that promises to touch only the description is one refactor away from touching more.

One route set or two? Where a default and per-scope overrides have different owners:

  • Two route sets — one for the default, one scoped to the caller's own scope, each with its own permissions. The scoped set cannot address the default at all, so no parameter and no defect in parameter handling lets a scope owner edit what every other scope falls back to. The cost is two near-identical sets to document and keep in step; documenting one as "same as the other" is how they drift.
  • One route set with the scope as a parameter, whose permitted values depend on the caller. One contract to learn, and isolation now rests entirely on a correct permission check.

The separation is worth more here than in most features, because the two operations differ in blast radius by orders of magnitude.

Pagination. Rows in these lists flip their in-effect flag while a reader is paging through them, and new versions insert at the top. Cursor pagination stays stable under that; page/pageSize gives the management screens the totals and page numbers they render. Offset pagination's failure here is a row appearing twice or not at all across a page boundary, which for a version list is cosmetic — the reason the choice can safely follow the project's house convention. Either is defensible. These examples use a cursor; whichever is chosen, the page size is bounded.

The lifecycle history is not on this surface. Who created, activated, withdrew or removed a version is read through the audit trail's own endpoints, filtered to this entity type. A dedicated history endpoint here would be a second answer to a question that already has one, and the two disagree the first time one of them is missed.


1. List versions ​

Endpoint: GET /versions?identity=$identity&inEffect=$inEffect

Description ​

Lists versions, newest label first within each identity. Backs the management screen, which groups the rows by identity.

Authorization ​

Requires the read permission for the scope being read.

Query Parameters ​

ParameterTypeRequiredDescription
identityStringNoFilter to one artefact
inEffectBooleanNoFilter to the version in effect, or to those that are not
limitIntegerNoPage size, bounded
cursorStringNoOpaque pagination token

Request Logic ​

Select the scope's versions that have not been removed, apply the filters, order by identity and then by the version order descending. Content is not returned here — a list of long content bodies is slow to fetch and useless to read; the single-version read returns it.

The list of identities is a different question from the list of versions, and this endpoint answers the second. An identity whose every version has been removed still exists — its key is still taken, and a create against it is a new version, not a new identity — so a picker that offers "which identity to add a version to" cannot be derived from this list's rows. A project that needs it returns the set of known identities beside the page, or from an identity table where it has one.

Success Response (200 OK) ​

json
{
  "data": {
    "items": [
      {
        "id": "018fa51f-1c22-7c05-9a70-3f0b0f2f77aa",
        "identity": "orderConfirmation",
        "description": "Tightened the summary wording",
        "version": 3,
        "inEffect": true,
        "updatedAt": "2026-07-05T00:00:00Z",
        "updatedBy": "user:018fa51f-9a10-7f31-8bd4-2c1a9f5e0d33"
      }
    ],
    "cursor": null,
    "totalCount": 1
  },
  "status": 200,
  "requestId": "a8123fa1-2045-42b4-83fc-97dafe11f4a5"
}

Error Responses ​

400 — a malformed filter or a page size out of range. 403 — the caller lacks the read permission for this scope.


2. Read one version ​

Endpoint: GET /versions/{versionId}

Description ​

Returns one version including its full content. This is the read a reviewer uses to see what a version actually said, and the read an editor's "new version" form pre-fills from.

Authorization ​

Requires the read permission for the scope the version belongs to.

Request Logic ​

Select the version by id within the caller's scope; a version outside it is not found, not forbidden — telling a caller that a version they cannot read exists is itself a disclosure.

Success Response (200 OK) ​

json
{
  "data": {
    "id": "018fa51f-1c22-7c05-9a70-3f0b0f2f77aa",
    "identity": "orderConfirmation",
    "description": "Tightened the summary wording",
    "content": "Thank you for your order…",
    "version": 3,
    "inEffect": true,
    "createdAt": "2026-07-01T00:00:00Z",
    "createdBy": "user:018fa51f-9a10-7f31-8bd4-2c1a9f5e0d33",
    "updatedAt": "2026-07-05T00:00:00Z",
    "updatedBy": "user:018fa51f-9a10-7f31-8bd4-2c1a9f5e0d33"
  },
  "status": 200,
  "requestId": "3ab34d88-65d1-4c10-897a-237c9a5b116f"
}

updatedAt on an immutable row deserves a note rather than a shrug. It moves when the in-effect flag changes, never when the content does. A project whose audit columns cannot express that distinction should say so here, because a reader who sees a modification timestamp on a version reasonably concludes the version was modified.

Error Responses ​

404 — no such version in this scope. 403 — the caller lacks the read permission.


3. Create a version ​

Endpoint: POST /versions

Description ​

Creates a new version of an existing identity, optionally putting it into effect immediately. This is the only way content changes.

Authorization ​

Requires the create permission for the scope being written.

Request Body ​

json
{
  "identity": "orderConfirmation",
  "description": "Tightened the summary wording",
  "content": "Thank you for your order…",
  "activate": true
}
FieldTypeRequiredDescription
identityStringYesAn existing identity key
descriptionStringNoWhat this version changes — the field that makes a history readable
contentStringYesThe version's content; must not be empty
activateBooleanNoPut the new version into effect immediately, withdrawing the previous one in the same transaction

Request Logic ​

  • Verify the identity exists. Creating a version of an unknown identity is rejected rather than silently creating the identity: a typo would otherwise produce an artefact nothing consumes and nobody notices.
  • Assign the next label in the sequence for this identity and this scope — the sequences are independent, so a scope's first override is its own first version regardless of how many versions the default has.
  • Insert; where activate is set, withdraw the previously effective version and put this one into effect in the same transaction.
  • Record the lifecycle events.

Success Response (201 Created) ​

json
{
  "data": { "id": "018fa51f-1c22-7c05-9a70-3f0b0f2f77ab", "version": 4 },
  "status": 201,
  "message": "Version created.",
  "requestId": "16ddc487-1092-496e-b9a4-97d3e3082ee6"
}

Error Responses ​

400 — missing or empty content. 404 — the identity does not exist. 403 — missing permission. 409 — the assigned label collided with a concurrent save; this is the database constraint doing its job, and the correct handling is to retry the assignment, not to remove the constraint.


4. Put a version into effect ​

Endpoint: POST /versions/{versionId}/activate

Description ​

Makes the version the one in effect for its identity in its scope. This is also the rollback operation — rolling back is activating an earlier version, not a separate mechanism.

Authorization ​

Requires the activate permission for the scope. This is deliberately a permission of its own: creating a version changes nothing that runs, while activating one changes behaviour immediately, and a project may well grant the first more widely than the second.

Request Logic ​

  • Load the version within the caller's scope; not found otherwise. Activating the version already in effect succeeds and does nothing.
  • In one transaction: withdraw the currently effective version of the same identity and scope, put the requested one into effect, and record both lifecycle events.
  • The partial unique constraint is what makes this safe under concurrency; a simultaneous activation surfaces as a constraint violation rather than two effective versions.

Success Response (200 OK) ​

json
{
  "data": { "id": "018fa51f-1c22-7c05-9a70-3f0b0f2f77aa", "inEffect": true },
  "status": 200,
  "message": "Version put into effect.",
  "requestId": "ad9cd51f-d993-4014-a2b2-8f563fe887fd"
}

Error Responses ​

404 — no such version in this scope. 403 — missing permission. 409 — a concurrent activation won the race.


5. Withdraw the version in effect ​

Endpoint: POST /versions/{versionId}/deactivate

Description ​

Leaves the identity with no version in effect in this scope — falling back to the default where one exists and the project layers scopes. Guarded, because it is the one operation that can strand a consumer.

Authorization ​

Requires the same activate permission as putting a version into effect: both change what runs.

Request Logic ​

  • Load the version; not found where it is outside the scope, conflict where it is not the one in effect.
  • Run the guard inside the same transaction as the write. A guard that runs before the transaction lets a reference added concurrently slip between the check and the update, which is the failure the guard exists to prevent. Where the project's answer is to fail at use time instead, this step is absent by design and the choice is stated on the feature page rather than left to be inferred from its absence here.
  • Where the guard rejects, the response names the consumers that would have been stranded. A refusal an administrator cannot act on is only marginally better than a failure later.
  • Otherwise clear the flag and record the lifecycle event.

Success Response (200 OK) ​

json
{
  "data": { "id": "018fa51f-1c22-7c05-9a70-3f0b0f2f77aa", "inEffect": false },
  "status": 200,
  "message": "Version withdrawn.",
  "requestId": "9a4f0f7c-0b19-4c6a-9a45-6c9b4a1f6f21"
}

Error Responses ​

409 — in use. The guard refused: consumers would be left with nothing in effect.

json
{
  "status": 409,
  "error": {
    "code": "ERR_VERSION_IN_USE",
    "message": "The artefact is still referenced and no fallback would remain.",
    "details": {
      "identity": "orderConfirmation",
      "consumers": ["<the references that would be stranded>"]
    }
  },
  "requestId": "9a4f0f7c-0b19-4c6a-9a45-6c9b4a1f6f21"
}

409 — not in effect. The version is not the one currently in effect, so there is nothing to withdraw. 404 / 403 as above.


6. Remove a version ​

Endpoint: DELETE /versions/{versionId}

Description ​

Soft-deletes a version that is not in effect. The version in effect can never be removed.

Authorization ​

Requires the delete permission for the scope.

Request Logic ​

Load the version; conflict where it is in effect. Set the removal marker and record the lifecycle event. The lifecycle records of a removed version are themselves preserved — removing a version must not remove the evidence that it existed.

Where executions record the version they used, removal is in tension with traceability. A project either exempts any version an execution referenced, or accepts that some execution records will point at a version that can no longer be read, and says which.

Success Response (200 OK) ​

json
{
  "data": { "id": "018fa51f-1c22-7c05-9a70-3f0b0f2f77aa" },
  "status": 200,
  "message": "Version removed.",
  "requestId": "ad9cd51f-d993-4014-a2b2-8f563fe887fd"
}

Error Responses ​

409 — the version is in effect. 404 / 403 as above.


7. Read the effective version per identity ​

Endpoint: GET /effective

Description ​

For every identity, the version that would actually be used in the caller's scope, and where it came from — the scope's own override or the default. This is the screen that answers "what is running right now", and the same resolution consumers perform.

Authorization ​

Requires the read permission for the scope.

Request Logic ​

Resolve, per identity: the scope's effective version where one exists, otherwise the effective default. The response reports the source alongside the resolution, because "we are on the default" and "we are on our own override that happens to match" are different situations that look identical without it.

Success Response (200 OK) ​

json
{
  "data": {
    "items": [
      {
        "identity": "orderConfirmation",
        "versionId": "018fa51f-1c22-7c05-9a70-3f0b0f2f77aa",
        "version": 2,
        "source": "scope"
      },
      {
        "identity": "shipmentNotice",
        "versionId": "018fa51f-77a4-7b60-8e2c-91d0f3b0a7de",
        "version": 7,
        "source": "default"
      }
    ],
    "cursor": null,
    "totalCount": 2
  },
  "status": 200,
  "requestId": "a8123fa1-2045-42b4-83fc-97dafe11f4a5"
}

Error Responses ​

400 — a malformed page size. 403 — missing the read permission.


Internal resolution (no endpoint) ​

Consumers do not call the API above. They resolve through an internal service — identity plus scope in, content plus the version identifier out — and they record the version identifier alongside whatever they produced. Two properties matter and neither is optional: resolution happens at the moment of use, so a change takes effect without a restart, and the version used is recorded, without which no outcome can ever be attributed to the content that produced it.