Skip to content

Usage Statement Already Superseded

URI: https://docs.ankatech.co/errors/usage-statement-already-superseded
HTTP Status: 409 Conflict

When you see this

You issued a correction naming the statement it supersedes, and that statement is already superseded by another retained statement for the same month.

The correction is refused and nothing is written. The statement you named, and the one that already supersedes it, both remain retained and both remain verifiable against their own bytes.

Why a correction chain is kept linear

A correction is a new statement naming the one it replaces, and both documents are retained — the ordering lives inside the signed bytes, in basis.issuedAt and basis.supersedes.

If two corrections were allowed to supersede the same statement, the chain would fork. Neither leaf would supersede the other, and which statement is current for this month would have no answer inside the signed chain — the reader would be left with issuedAt, which orders the two documents without saying that either one replaces the other. That is the question a disputed true-up invoice asks, so it has to stay answerable.

The remedy for the sibling refusal usage-statement-already-issued — issue a correction naming whichever statement is being superseded — is precisely what a fork destroys: a third correction can name only one of the two leaves, and the other would stay unsuperseded for ever. So the chain is kept linear at the point the fork would be created, which is the only point at which it is still cheap.

Not the same refusal as a second original

Two problem types, because they are answered by two different caller actions:

Type The condition What you do
usage-statement-already-issued The month is already stated and this request declares no correction Name what you are correcting
usage-statement-already-superseded The statement you named is no longer the current one Correct the current one instead

A distinct type is the machine-readable discriminator between them. A caller that branches on type does not have to parse detail to tell the two apart.

Common Causes

  • Two operators corrected the same month independently, and the second correction still names the original statement rather than the first correction.
  • An automated re-issue replays a stored supersedes identity from before an earlier correction was accepted.
  • A correction was composed against a cached listing rather than against the current chain.

What the detail names

Two statement identities, and nothing else: the statement you named, and the current leaf of its correction chain — the retained statement that no other statement supersedes, however many corrections deep it sits. It is not merely the named statement's direct successor: on a chain A ← B ← C, naming A is answered with C, because B has itself been corrected and naming it would be refused in turn. Naming the leaf is what makes the refusal actionable in one step — a correction naming it is accepted, unless another correction lands first.

Both identities are retained in your own scope for the period you stated — the refusal is only reachable once that has been established — so neither is a disclosure. No tenant, no period figure, no counter.

Response Example

{
  "type": "https://docs.ankatech.co/errors/usage-statement-already-superseded",
  "title": "Usage Statement Already Superseded",
  "status": 409,
  "detail": "Statement 7f3c2a1e-9b4d-4c8e-a6f5-2d1b0e9c8a7f has already been corrected, and the current statement of its correction chain is b4e81d06-3a5f-4d92-8c17-0e2a9f5b6c31. A correction chain is kept linear, because two statements superseding the same one leave no answer to which statement is current for that month: correct b4e81d06-3a5f-4d92-8c17-0e2a9f5b6c31 instead, or read it if it already carries the figure you intended.",
  "instance": "/api/v3/admin/usage/statements",
  "extensions": {
    "requestId": "9c72e158-40ab-4d36-8e15-b3097fa25c64"
  }
}

Content-Type: application/problem+json — the response follows RFC 9457 Problem Details.

The refusal is raised by the licence server and reaches you through the Admin API unchanged: 409 is a member of {400, 402, 404, 409, 422}, the set the relay propagates with its own type, title, detail and status. You therefore read this refusal rather than a 502 reporting an outage. instance and the correlation identifier are always the Admin API's own; it carries the latter as extensions.requestId. See Error Index for where each service puts it.

How to Resolve

  1. Read the current leaf of the chain. The second identity in detail is the chain's current leaf, and it is the statement to work from. GET /api/v3/admin/usage/statements lists this tenant's statements; every correction and the statement it supersedes are present, and basis.supersedes names the one each correction replaces.
  2. If that statement already carries the figure you intended, stop. Somebody else has already made the correction, and a second one would say nothing new.
  3. If it does not, re-issue your correction naming that statement in supersedes, not the one you originally named. One re-issue is enough, however long the chain: the chain stays linear and the month keeps a single current statement.

A near-simultaneous pair can still both be accepted. The refusal is decided by reading the retained statements for the period inside the same transaction as the write, so two genuinely concurrent corrections of the same statement can both observe it unsuperseded and both be written. The check narrows the window; it is not a uniqueness constraint. Treat the listing, not the absence of this refusal, as the authority on which statement is current.

For full schema definitions, examples, and interactive testing, see the Developer Hub Reference.

* restricted content — see higher-tier documentation