> ## Documentation Index
> Fetch the complete documentation index at: https://docs.neobank.proofof.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Tags, transitions, and one identity read

> Operator routes get their own tag, every customer-requestable state change is a POST transition, and onboarding is the one identity read.

# ADR 0016: Split the audiences, name the transitions, keep one identity read

## Context

The `Operations` tag held two unrelated APIs: `/v1/operations/*`, the
customer-facing asynchronous write tracker, and `/v1/ops/*`, the operator
console that is view-and-suspend only. Different audiences, different
authorization classes, path prefixes one letter apart. `/v1/activity` sat
under `Accounts` although the projection deliberately does not attribute
rows to an account. A state change reached the API in four shapes —
revocation as a `DELETE`, suspension and card lifecycle as `POST`
sub-paths, reveal as a collection `POST`. And identity was three
overlapping reads: `/v1/me`, `/v1/session`, and `/v1/onboarding`, with
`email` published twice, the customer embedded twice, and `kyc_status`
duplicated between onboarding and `/v1/kyc`.

`OperationView.kind` and `.state` were bare strings shadowing two closed
vocabularies — the dispatcher's kind set and the operation state machine.

## Decision

`Operator` tags the four `/v1/ops/*` routes; `Operations` keeps the
customer tracker; `Activity` stands alone.

One transition rule, applied consistently: a state change the customer can
request is a `POST` to a named transition sub-path; a resource creation is
a `POST` to a collection. Revocation stops being a `DELETE` — it is a
transition with an audit trail and an upstream consequence, and modelling
it as a deletion implies the mandate disappears. `POST
/v1/mandates/{mandate_id}/revoke` joins the existing suspend, freeze,
unfreeze, and close transitions. Reveal stays a collection `POST` because
it genuinely creates a short-lived session object, and the connection
disconnect stays a `DELETE` because that row genuinely ceases to exist —
the discriminator is whether the resource survives the call. Polls that
read a side effect into local state are `POST`s to a named `poll`
sub-path, the shape a purchase poll inherits later. The requirement is the
one rule, not this specific rule.

The route string keys idempotency records, so a revocation record written
before the rename replays as a fresh request. Every state in which a
revocation is already in flight or complete rejects the replay through the
revocation gate; the one state that passes, `revocation_failed`, is one
where the upstream revoke definitively did not happen and a fresh
revocation is the correct answer — no duplicate revocation can start.

`GET /v1/session` stays separate — it is about the browser session and
carries the CSRF token, a different concern with a different lifetime.
`/v1/me` merges into `/v1/onboarding`, which already embeds the customer
and is the single "who am I and what can I do" read. The duplicated
`kyc_status` leaves onboarding: `GET /v1/kyc` is its single source, and
onboarding keeps `kyc_session_available`, which is a different fact. With
the field gone, the onboarding read no longer calls the provider at all —
its former degrade-to-unavailable behaviour is retired, and the KYC read's
own error contract is propagation.

`OperationView.kind` and `.state` become closed `Literal`s over the
dispatcher's eleven kinds and the eight operation states, on the customer
and operator views alike. The vocabulary is enforced at the write side —
`create_operation` takes the kind `Literal`, and the worker fails an
unlisted kind closed as `operation_kind_unsupported` — and two tests pin
the state vocabulary to the model enum and the kind vocabulary to the
worker's dispatched set.

## Consequences

Operator tooling generated from the contract no longer interleaves
customer routes. The web dashboard sources identity verification from
`GET /v1/kyc` with its own query, so a KYC read failure degrades one
readiness row instead of the whole onboarding read. Clients revoking
mandates send a `POST` to the transition path. An out-of-vocabulary
operation kind now fails type-checking at the write side and the view at
serialization instead of passing silently; an out-of-vocabulary state
fails the view.

## Rejected alternatives

Modelling every transition as a `PATCH` of a state field: it hides the
audit-relevant intent in a request body and invites partial updates of a
machine the worker owns.

Keeping `/v1/me` for API symmetry: it duplicated two fields of a read the
client already needs on every page, and nothing consumed it.

Degrading KYC inside onboarding after the merge: it would re-couple the
identity read to provider availability, which is the coupling the split
removes.
