platform-api-docs

Deprecations and removals

What the platform’s published APIs have stopped offering, when, and what replaced it.

A deprecation is an operation still served but signalled as going — marked deprecated: true in the contract, with the replacement named in its description. A removal is an operation or document that is gone. Both are recorded here, and in the owning repository’s CHANGELOG.md.

If an entry affects you and no replacement is named, raise it with the platform team rather than assuming the behaviour moved.


Nexus Flow — Identity, Analytics and Automation APIs

Removed 2026-09-18 — the Errors array and the Error object, now one RFC 7807 Problem

These three APIs answered errors with a bare array of { code, title, detail, instance } — or, on automation, a single object of the same shape. The other four APIs on the estate answer with RFC 7807 application/problem+json. A generic client cannot read both, so D-37 converged the estate on one shape rather than documenting the divergence and converting later.

Every 4xx and 5xx response on all three now carries a single application/problem+json object:

{
  "type": "https://gradera.dev/problems/not-found",
  "title": "Not Found",
  "status": 404,
  "detail": "No active group exists with slug \"reviewers\".",
  "instance": "/identity/v1/groups/reviewers",
  "code": "IDENTITY_GROUP_NOT_FOUND_404",
  "errorId": "c56246b6-d666-4717-8a21-bd850c8bc2b6"
}

code is unchanged on all three. It is what a client branches on, so every value these APIs have published survives — IDENTITY_GROUP_NOT_FOUND_404, ADMIN_ASSIGNMENT_DUPLICATE_409, INVALID_REQUEST, INTERNAL_SERVICE_ERROR, SUBSCRIBER_DUPLICATE_409, SORT_INVALID_400 and the rest. What changed is the envelope around it. A client that read body[0].code must read body.code; a client that read code off a parsed problem needs no change at all.

Three smaller changes go with it:

errors is added: a map of field name to messages, present only on a validation failure. Analytics previously sent one Error object per failed field; one problem object now carries them all.

Automation’s contract was wrong before this change, which the other two were not. It published a bare { code, title, detail, instance } for most statuses and an Errors array for 400, while the service sent a problem-shaped object with type, status, correlationId and timestamp. The contract and the code had never agreed.

correlationId and timestamp have left automation’s response body. The correlation ID is on the x-correlation-id response header, which all three services now set from the exception filter — before this, a guard rejecting a request returned no correlation header at all, because the interceptor that set it runs after guards.

Contracts moved: nexus-flow/v1-identity.yaml 1.0.0 → 2.0.0, nexus-flow/v1-automation.yaml 1.0.0 → 2.0.0, nexus-flow/v1-analytics.yaml 0.2.1 → 0.3.0.

A sweep of every UI repo and BFF under the platform root found nothing that read the array: the hits were test fixtures and BFF stubs, and the shared UI SDK’s toProblemDetail discarded array payloads outright. v1-alerts, v1-audit and v1-notifications still publish the old Error shape, which their services still send; they are named as exemptions in .spectral.yaml so the remaining divergence can be counted.


Nexus Flow — Messages API

GET /messages/v1/inbox and GET /messages/v1/inbox/subscriber returned their page navigation under links. Every other API on the estate uses _links, and this contract has published _links for these two endpoints all along — so a client generated from the contract read _links, found nothing, and had no page to follow.

The cause was a missing [JsonPropertyName] on one model property, which left it taking the camelCase default. The contract did not change; the service caught up to it.

No dual-emit window. D-03 chose a straight rename: the consumer check found nothing reading either spelling — the inbox front end pages by offset arithmetic and has zero references to links or _links — so emitting both for a release would have been ceremony protecting nobody. It is recorded here for any external caller the survey cannot see.

Two related changes shipped with it, neither of them contract-visible:

Not a deprecation: a contract that under-described the service. The endpoint has always returned the house pagination envelope including _links (self, next, prev), on the empty page as well as a full one. Its schema listed offset, limit, total and data and stopped there, so a generated client had no links to follow — while its sibling GET /alerts/v1/alerts/rules declared them.

CatalogFlowListResponse now carries _links: HATEOASLinks, matching AlertRuleListResponse.

No service change, and no caller behaviour changes. The links were already on the wire.

Found by the One Pagination Envelope survey (H-09), which grades every paged GET in these contracts against {offset, limit, total, data, _links}. That survey now runs in CI as a report-only step.

Removed 2026-09-17 — 403 on DELETE /messages/{messageId}/locks

Releasing a lock is now idempotent in every case. The operation answers 204 whenever the caller ends up holding no lock on the message, including when another user holds it — that holder’s lock is left in place. It previously answered 403 (NF403.Message.Lock, “You are not the lock holder for this message.”) in that one case.

Replacement: none needed. A client that released a lock it did not hold was already getting the end state it asked for; it now gets a success code for it. Clients that special-cased the 403 can drop that branch. Acquiring a lock is unchanged: POST /messages/{messageId}/locks still answers 409 when someone else holds it, which is the code to check if you need to know who may work on a message.

Removed 2026-09-11 — PATCH /messages/{messageId}

The message update operation was retired as obsolete (NF-69, gradera-platform-api-messages #13).

Replacement: none directly. A message’s status is set by the platform as the flow advances; the client-facing actions are POST /messages/{messageId}/responses to answer a message and POST /messages/{messageId}/locks / DELETE …/locks to hold and release it.

If you hold an older copy of v1-messages.yaml, it may still describe this operation. It appears in fifteen unmerged branches of platform-api-docs. The contract published here is authoritative.

Removed 2026-09-16 — GET /messages/v1/example

A scaffold stub the service template left behind — summary “Example GET endpoint stub”, returning {"message": "Gradera Platform Messages API is running."}, and marked anonymous. It was never in this published contract, so no documented behaviour changes. Recorded here because it was live on the deployed service and anyone who found it by probing will see it disappear.

Replacement: none needed. Use GET /messages/health to check the service is up.

Not a removal — published 2026-09-16 — two operations that were always served

GET /message-types/{slug} and POST /messages/{messageId}/locks have always been served by the Messages API and were missing from this contract. The lock contract previously documented only the release half (DELETE), which made acquiring a lock undiscoverable to anyone reading it.

No behaviour changed. v1-messages.yaml moved 0.4.0 → 0.4.1.

Not a removal — published 2026-09-16 — the alert feed

v1-alerts.yaml described 13 operations against 26 the service serves. Rule configuration was published; the user-facing feed was not — the inbox, unread count, status changes, preferences, settings, alert types, delivery channels, the SSE badge stream and the action endpoints.

All of it is now in the contract. No behaviour changed; every one of these operations has always been served. The contract was half of one.

The document is retitled from NexusFlow Configure Alerting API to NexusFlow Alerts API, since it no longer describes only configuration. v1-alerts.yaml moved 0.1.0 → 0.2.0.

POST /admin/demo/alerts is documented too, as what it is: an administrative seeding endpoint for demonstrations and local testing, not intended for client use. It is served, so the contract says so.


Nexus Flow — Flows API

Deprecated 2026-09-17 — sort_by on GET /flows/v1/flows

Use sort instead. Same grammar, same fields, different spelling — and sort is the one this contract has named all along, under #/components/parameters/sort.

sort_by remains accepted and applied for one release, after which it is removed. If both are sent, sort wins and sort_by is ignored.

Why it moved. sort is what the Identity API implements, what the SLA screen already sends, and what this contract published for /flows and /slas while the service bound sort_by — so a caller following the contract was silently ignored. It is also the only underscored query parameter across the .NET services; every other one is userName, groupSlug, flowSlug.

What else changed at the same time, none of it a deprecation:

Changed 2026-09-17 — Flow.owner is no longer required, and now names the real owner

owner was built from the flow’s created_by string: a GUID parse falling back to all zeros, with an email and a niqUri invented from whatever text was there. On development that presented four of five flows as owned by “System”. It is now read from the flow’s owning user row.

What a consumer sees change: owner.userGuid, displayName, email and niqUri all become real where they were invented, and niqUri now matches niq://{tenant}/user/{email} rather than the niq://user/{name}/{version} shape only the fabrication ever produced.

owner also leaves the required list. An owner that cannot be resolved is absent rather than invented — a foreign key makes that unreachable in practice, so its absence means the data is broken, not that the flow has no owner. A client that assumed owner was always present should guard it.

Not a removal — corrected 2026-09-16 — nexus-flow/v1.yaml and nexus-flow/latest.yaml

These two URLs were linked from this site’s landing page and its README for several months. They were never published. A combined Nexus Flow document was authored on a feature branch and the merge that would have delivered it did not happen, so the links resolved to 404 for their whole life.

Replacement: the per-service documents, which are and always were the real contracts — v1-flows.yaml, v1-messages.yaml, v1-audit.yaml, v1-alerts.yaml, v1-analytics.yaml, v1-notifications.yaml, v1-automation.yaml, v1-identity.yaml. The landing page now links these directly and the two aggregate URLs have been retired rather than restored: the branch copies date from May–July 2026 and describe a surface that has moved a long way since.

Not a removal — published 2026-09-16 — twelve operation groups and POST /flows/{flowSlug}/flow-instances

Recorded here because it looks like a change and is not one. The /properties, /message-types, /complex-types and /folders groups, and POST /flows/{flowSlug}/flow-instances, have always been registered and functional in the Flows API. They were held out of the published contract under NF-141 pending a review of whether any client depended on them; that review completed and every one has a live consumer, so the contract now describes them. No behaviour changed and nothing is deprecated.

v1-flows.yaml moved 0.3.1 → 0.3.2.

Changed 2026-09-16 — GET /flows/{flowSlug}/versions/{…} path parameter

The parameter was published as flowVersionId, typed format: uuid. The endpoint has always accepted either a sequential version number or a version UUID — its own summary reads “Get a published flow version by version number or version ID” — so the contract made half the endpoint undiscoverable, and a validating client or gateway generated from it would reject …/versions/3.

Now published as versionNumberOrId, typed string, with an example of each form. v1-flows.yaml moved 0.3.2 → 0.3.3.

If you generated a client from 0.3.2 or earlier, the method argument for this one operation may be named flowVersionId and typed as a UUID. Regenerate. Calls that pass a UUID keep working unchanged — the widening is additive at the wire level; only the generated signature and any client-side validation change.

Deliberately absent — /flows/health, /flows/health/live, /flows/health/ready

These are served, but they sit outside API versioning and therefore outside the /flows/v1 server base every published operation is written against. They cannot be expressed in this contract and their absence is not drift. Call them directly on the service host.

Nexus Flow — Alerts API

Deprecated 2026-09-17 — sortKey and sortDir

Use sort instead, on both GET /alerts/rules and GET /alerts/rules/{ruleId}/fires: field:direction, with the direction optional and asc by default. Both old parameters remain accepted for one release and are marked deprecated in the contract; sort wins if both are sent. With neither, each list keeps its own default — rules by scope ascending, fires by fired-at descending — so nothing a current caller does changes.

Why it moved. This was the only list on the platform taking two parameters where every other one takes sort=field:direction. The two-parameter form shipped first because this contract had never been published at all and something had to go out; it was recorded at the time as needing revision, and this is that revision.

Nothing is lost by the change. The enum-typed sortKey was this API’s one real advantage — a generated client could see which fields were sortable, where field:direction is an opaque string. The sortable set is still typed into the contract, sort is validated against the same enum, and the 400 names its values:

GET /alerts/v1/alerts/rules?sort=bogus:asc
→ 400  Unsupported sort field "bogus". Supported fields: name, trigger, scope, notify, status, lastFired, owner.

SoS Admin API

Deprecated 2026-09-17 — sort_by on GET /users/{userId}/groups/details

Use sort instead — same fields (groupName, flowCount), same default (groupName:asc), and now the same grammar as every other list in the estate. sort_by remains accepted for one release, after which it is removed; sort wins if both are sent.

One widening came with it: the direction is now optional, so sort=flowCount means flowCount:asc. sort_by previously required field:direction and rejected a bare field. No caller breaks.

Corrected 2026-09-17 — what GET /invitations actually accepts

Nothing about the endpoint changed. The contract was wrong about it, in three places:

Corrected 2026-09-17 — what search on GET /groups covers

search matches the group’s display name and slug. The contract described it as matching “display name or description text”; the description has never been searched.

Added 2026-09-17 — sort on the user, group and invitation lists, search on users

Not a deprecation, but the same release: all three lists accept sort in the estate’s grammar, and GET /users accepts a search matched against userName or email. Every list keeps createdDts descending as its default, so a caller who sends no sort sees nothing move. Paging links on all three now carry their filters and the sort, and take their scheme from X-Forwarded-Proto.

Removed 2026-09-16 — DELETE /groupUserAssignments/{guaId}

The camelCase management route is retired (sos-admin-api, API-Spec-Reconciliation X-30a). It was the only route under that prefix, while the contract had described the same resource as /group-user-assignments since it was first written — the code had never caught up.

Replacement: DELETE /group-user-assignments/{groupUserAssignId} (deleteGroupAssignment). Same behaviour, same identifier; only the path changed. The old path now answers 404. The Location header on every assignment create, and the self link on assignment resources, already point at the new path.

Not a removal — served 2026-09-16 — the flat assignment resource

GET /group-user-assignments, POST /group-user-assignments and GET /group-user-assignments/{id} were in this contract but not in the service. They are now served, over the same handlers the nested routes use. The contract’s assignment schemas (GroupAssignment, GroupAssignmentsPage) were corrected to the shape the service has always returned — guaId, groupName, userFullName, userEmail, userStatus, and a paging object keyed links — which the nested routes now reference too.

Not a removal — published 2026-09-16 — six operations that were always served

GET /users/me, GET /users/me/groups, GET /users/{userId}/groups/details, GET /groups/{groupSlug}/flows, POST /groups/{groupSlug}/duplicate and PATCH /invitations/{invitationId} have always been served and were missing from this contract (X-30b). No behaviour changed. sos-admin/v1.yaml moved 1.0.0 → 1.1.0.


Tenant Management API

Corrected 2026-09-16 — paths are /tenants/…, not /tenant-man/…

The contract described every operation under /tenant-man/v1/… and /tenant-man/access-controls/v1/…. The service has only ever served /tenants/v1/… and /tenants/access-controls/v1/… (its health route is /tenants/health), and the gateway routes /tenants to it; /tenant-man/* on staging falls through to the gateway’s 401, the same as any path that does not exist (X-31).

No behaviour changed — the document was wrong, not the service. Every path was rewritten to the /tenants/… form and tenant-management/v1.yaml moved 1.0.0 → 1.0.1. The one remaining /tenant-man/ reference in the estate, the demo compose file’s health check, was corrected in the same change.

Published 2026-09-17 — the last four operations, and two operationIds corrected

GET/POST /tenants/v1/tenants/{tenantId}/role-group-bindings and GET/POST /tenants/v1/tenants/{tenantId}/widgets have always been served and were missing from this contract (X-32). No behaviour changed.

Two operationIds are corrected to match the code: getHostById → getHost and getTenantById → getTenant. The paths, parameters and responses are unchanged — only the identifier a generated client uses to name the method. If you generate a client from this contract, two method names change.

tenant-management/v1.yaml moved 1.0.1 → 1.0.2, and tenant-management is now in exact parity with its generated document. All ten published contracts are.


Errors across the estate

Corrected 2026-09-18 — every error response is one RFC 7807 object, not an array

If you parse an error body from any of these four APIs, check what you are parsing. Four contracts described error responses as a JSON array of a bespoke Error object. No service has ever sent one. All four serve a single RFC 7807 object, on application/problem+json, with exactly these seven members:

{ "type": "https://gradera.dev/problems/not-found", "title": "Not Found", "status": 404,
  "detail": "…", "instance": "/sos-admin/v1/users/…", "code": "ADM404",
  "errorId": "4a5b6c7d-8e9f-0a1b-2c3d-4e5f6a7b8c9d" }

Confirmed by calling all four on development: sos-admin, flows, messages and tenant-management each return those same seven keys and that same content type.

What the contracts said instead, and it was different in each one:

Contract Declared Missing from what is served
nexus-flow/v1-flows.yaml Errors array of {errorId, code, title, detail} type, status, instance
nexus-flow/v1-messages.yaml Error object of {code, title, detail, instance} type, status, errorId
sos-admin/v1.yaml Errors array of {id, code, title, detail} type, status, instance; and id is not a member — the field is errorId
tenant-management/v1.yaml Errors array of {id, code, title, detail}, id required as above

What changed here. Each contract gains one Problem schema — the seven members above, with type, title, status, code and errorId required — and every error response points at it and declares application/problem+json instead of application/json. Error and Errors are removed; nothing references them. SoS Admin’s ten error examples and six inline examples in Messages were rewritten, since an example that does not satisfy a required property fails Spectral.

code is not always this API’s prefix. A framework error takes it (ADM404); a domain error carries its own (Flow.NotFound, Tenant.NotFound, MESSAGE_NOT_FOUND_404). Match on status and read code as a detail, rather than the other way round.

Why it went unnoticed for so long. spec-parity.py compares method and path only, and reported sos-admin: OK — 38 operations match throughout. It is the same blind spot that let latest.yaml sit two envelope shapes out of date: a check that compares one dimension says nothing about the others.

Four APIs changed the value of every _links.self, _links.next and _links.prev href on their paged lists. No schema moved and no published contract changed, so nothing here is visible in a spec diff — but a caller that treated the href as a path rather than a URL is affected.

API Was Is now
Identity /identity/v1/users?limit=25&offset=0 https://<host>/identity/v1/users?limit=25&offset=0
Alerts /alerts/catalog/flows?offset=0&limit=25 https://<host>/alerts/catalog/flows?offset=0&limit=25
Analytics /analytics/v1/system/audit-log?offset=0&limit=25 https://<host>/analytics/v1/system/audit-log?offset=0&limit=25
Automation http://<host>/automation/v1/subscribers?… https://<host>/automation/v1/subscribers?…

Why it is a correction and not a change of mind. Flows, messages, SoS Admin and tenant-management have always sent absolute URLs, and every one of these four APIs’ own published examples already showed absolute https:// URLs — the documents were right and the services disagreed with them. Automation was the near miss: it built an absolute URL but took the scheme from req.protocol, which behind TLS termination is http, so it handed https callers http:// links.

All four now read the first hop of X-Forwarded-Proto and ignore a scheme the API does not serve, so a header cannot rewrite the links a service emits. This completes Q-16, which had reached flows, sos-admin and messages in the sort/filter scope and none of these four.

Follow the href as given. A consumer that concatenated a base URL onto these values will now produce a double prefix; one that follows the href as a URL needs no change.

The docs site itself

Removed 2026-09-18 — the hand-written index.html at the repository root

A second landing page, 697 lines, listing v1 and latest links for each API by hand.

Nothing served it. The site is generated by site/build.mjs into dist/, and the Dockerfile copies site/, the contract directories and dist/ — never the root index.html. Every reference to index.html in this repository points at the generated one. It had also gone stale twice over: it still advertised the latest.yaml URLs removed above, and listed neither the alerts, analytics, automation nor identity contracts that nexus-flow/ has carried since 2026-09-16.

Replacement: the generated landing page at /docs/, which is built from the contract files themselves and so cannot fall behind them.

Removed 2026-09-18 — sos-admin/latest.yaml and tenant-management/latest.yaml

Both are gone, and with them the …/sos-admin/latest.yaml and …/tenant-management/latest.yaml URLs.

Use the v1.yaml of each API. sos-admin/v1.yaml and tenant-management/v1.yaml are unchanged and have always carried the same content the alias was supposed to mirror.

Why. docs/deployment.md described the pair as “byte-identical aliases”. They were not: on 2026-09-18 sos-admin/latest.yaml differed from sos-admin/v1.yaml by 512 lines and tenant-management/latest.yaml by 106. The stale copies still described the nested { data, paging: { … } } envelope and bare-string pagination links — two shapes behind the services they documented, and published at their own URL the whole time.

Three things had to line up for that to go unnoticed, and all three did:

The estate already had the answer. Flows, messages, alerts, analytics, automation, identity, notifications and audit publish exactly one document each and have never had a latest alias — and the nexus-flow/latest.yaml URLs removed on 2026-09-16 turned out never to have been published at all. Two APIs out of ten carried an alias; both were wrong.

Removed 2026-09-17 — ten superseded per-domain contract files

sos-admin/v1-{users,groups,group-user-assignments,group-role-assignments,entity-user-roles,permissions,entitlements}.yaml and tenant-management/v1-{tenants,hosts,access-controls}.yaml are deleted (X-34, D-17).

They were merged into each directory’s v1.yaml in 92b96c2 and left behind. Nothing referenced them — not the landing page, not the site builder, not a README — but CI linted them, and after X-30 and X-31 they still described the retired DELETE /groupUserAssignments/{guaId} and the /tenant-man/… paths that were never routed.

Replacement: sos-admin/v1.yaml and tenant-management/v1.yaml, which have been the published contracts throughout. Nothing a reader could reach has changed. (Each also had a latest.yaml at the time; both were removed on 2026-09-18 — see below.)

Corrected 2026-09-17 — six sos-admin schemas rewritten to the shape the service serves

If you generate a client from sos-admin/v1.yaml, regenerate it. The contract’s response schemas were hand-authored in a convention the service never used, so a generated client deserialised these resources into empty or partly-empty objects (X-33). Operation-level parity could not see this, by design — it compares METHOD /path, not fields.

Schema Was documented as Is actually served as
Group groupName, groupSlug, groupDesc, created_dts, updated_dts groupId, displayName, slug, description, type, createdDts, updatedDts, roles
Invitation sent_dts, expires_dts sentDts, expiresDts
User — gains createdDts, updatedDts, status
GroupRoleAssignment role, groupSlug, graId gains createdDts, _links
GroupRoleAssignmentCollection { data, paging } { data, total, offset, limit, _links }
EntityUserRole role, entityType, entityId, userId, eurId gains createdDts, _links

Group, User and Invitation are the item type of GroupsPage, UsersPage and InvitationsPage, so the list responses are corrected by the same change. Every example was rewritten to match, including the assignments links, which pointed at /group-assignments?groupSlug=… — a path that has never existed.

One operationId is corrected: getEntityUserRoles → listEntityUserRoles, to match the code. GroupRoleAssignmentCreate is removed — it described a request body carrying groupSlug, which that endpoint takes from the path; the operation already documents its real inline { role } body.

sos-admin/v1.yaml moved 1.1.0 → 1.2.0. No service behaviour changed: the service already served all of this. Only the description of it was wrong.

Repository — deploy/k8s/ reference manifests

Corrected 2026-09-18 — three HealthCheckPolicies named a path no service serves

deploy/k8s/ holds copies of three other repositories’ GKE policies. All three checked requestPath: /health/ready. No service on this estate serves that path: health routes are registered as /{serviceName}/health/{live,ready}, and only the bare status body is mapped at the root — so /health answers 200 while /health/live and /health/ready answer 404.

These manifests are not applied, and never have been from here (N-10). Nothing in this repository reconciles deploy/k8s/: the two deploy workflows run helm upgrade platform-api-docs ./deploy/helm/app and touch nothing else, and there is no kustomization, ArgoCD Application or Flux Kustomization pointing at the directory. A read-only kubectl get against the cluster on 2026-09-18 confirms they could not apply if something tried — the namespace they target, sos-platform, does not exist, and neither do the Services they name.

The live policies for these APIs are deployed by each service’s own Helm chart, under different names, and were already correct:

This directory named Live Service Live policy requestPath
sos-admin-api (7001) gradera-platform-api-admin /sos-admin/health/ready
tenant-management-api (7003) gradera-platform-api-tenants /tenants/health/ready
nexusflow-api (7002) retired — no Service, nothing on 7002 —

No cluster behaviour changed, because nothing here was ever reconciled. The two manifests that still map to a running service were corrected to the path that service serves, so the reference stops contradicting the Helm chart it copies, and each file now carries a header saying it is not applied — matching qa-apis-httproute.yaml, which has been marked that way in the same directory since June 2026.

/health was considered as the target instead, since it is the one health path that does answer 200. It was rejected: it reports the bare status body and answers 200 whatever the state of the service’s dependencies, so a backend check against it can never fail and would mark a pod with a dead database as ready to serve. /health/ready runs the dependency checks and answers 503 when one is down, which is what lets the load balancer drain the backend.

Retired — the nexusflow-api HealthCheckPolicy

deploy/k8s/nexusflow-api-policies.yaml names a Service that no longer exists in any environment, and no Service in the estate listens on its port 7002. The nexus-flow monolith was split into gradera-platform-api-flows (7004, /flows/health/ready) and gradera-platform-api-messages (7005, /messages/health/ready), each shipping its own HealthCheckPolicy from its own Helm chart.

Its requestPath was left as it was rather than corrected — there is no path to correct it to, and inventing one would be worse than recording the history. The file is kept as a historical record and marked as retired. It is a candidate for deletion once that history is no longer wanted.

All four .NET contracts — error examples

Published 2026-09-18 — an example on every shared error response, and the real code each carries

gradera-error-response-has-an-example is now enforced at error severity. It fired on 22 shared error responses — sos-admin (1), tenant-management (10), flows (6), messages (5) — each of which declared application/problem+json and a $ref to Problem with nothing to show for it. A client author could see which members exist but not what this API puts in them.

No behaviour changed and no contract surface changed. Only examples were added, generated by scripts/problem-components.py --examples.

The code values are not uniform, and the obvious guess is wrong. All four services set CodePrefix = "ADM", so ADM{status} is only the fallback the shared factory applies when the caller passes no code of its own. What the documented operations actually return:

  400 401 403 404 409 415 422 500
sos-admin ADM400 ADM401 ADM403 not_found ADM409 ADM415 ADM422 ADM500
tenant-management ADM400 ADM401 ADM403 domain, e.g. Tenant.NotFound domain, e.g. RoleGroup.NameAlreadyExists ADM415 domain, e.g. Host.HasTenants ADM500
flows domain, e.g. FLOW400.Sort.Invalid ADM401 FLOW403.FlowControl.NotOwnerOrAdmin domain, e.g. ADM404.Flow domain ADM415 domain ADM500
messages NF400 ADM401 ADM403 not_found ADM409 ADM415 ADM422 ADM500

Three mechanisms produce that spread, all of them deliberate in the services’ own source:

Each contract’s Problem.code description already warned that a domain error carries its own code. The examples now show it.

Known gaps, recorded rather than papered over