# APX — APDS Parking eXtensions

**The written standard, complete, in one document.**

Edition 0.16.0 · draft · compiled 2026-11-02

This document is generated by concatenating the parts in `docs/standard/`
of the APX repository. It is a convenience for review: those parts remain
the editable source, and the bundled OpenAPI document is normative over
prose for APX-defined paths (Part 0 §0.5).

Repository: <https://github.com/rneubauer/apx>  
API reference: <https://rneubauer.github.io/apx/reference.html>  
Registries: <https://rneubauer.github.io/apx/registries/>  
Edition manifest: `spec/edition.json`, the exact artifacts this edition pins

---

## Contents

1. [APX Part 0 — Scope and Overview](#00-scope)
2. [APX Part 1 — References](#01-normative-references)
3. [APX Part 2 — Terms, Definitions, Notation](#02-terms)
4. [APX Part 3 — Conformance, Versioning, Governance](#03-conformance)
5. [APX Part 4 — Identity, Provenance, Extensions](#04-identity)
6. [APX Part 5 — Data Profile (over APDS native routes)](#05-data)
7. [APX Part 6 — Control (command plane, lanes, validations, devices)](#06-control)
8. [APX Part 7 — Alerts](#07-alerts)
9. [APX Part 8 — Delivery Fabric (webhooks + SSE)](#08-events)
10. [APX Part 9 — Security Profile](#09-security)
11. [APX Part 10 — Extensibility](#10-extensibility)
12. [APX Part 11 — Registries](#11-registries)
13. [APX Part 12 — Error Model](#12-errors)
14. [APX Part 13 — Accounts, Payments, LPR (optional classes)](#13-accounts-payments-lpr)
15. [APX Part 14 — Reservations & Permits (optional classes)](#14-reservations-permits)
16. [APX Part 15 — Tolling (optional class `apx-tolling`)](#15-tolling)
17. [APX Part 16 — Discovery (optional class `apx-discovery`)](#16-discovery)
18. [APX Part 17 — Customer Service & Resolution (optional class `apx-resolution`)](#17-resolution)
19. [APX Part 18 — Aggregation and Onboarding](#18-aggregation)
20. [APX Part 19 — Violations (optional class `apx-violations`)](#19-violations)
21. [APX Part 20 — Validations (optional class `apx-validations`)](#20-validations)
22. [APX Part 21 — Credentials (optional class `apx-credentials`)](#21-credentials)
23. [APX Part 22 — Valet (optional class `apx-valet`)](#22-valet)
24. [APX Part 23 — EV Charging (optional class `apx-charging`)](#23-charging)
25. [APX Part 24 — Plate Lists (optional class `apx-plate-lists`)](#24-plate-lists)
26. [APX Part 25 — UI Launch (optional class `apx-ui-launch`)](#25-ui-launch)
27. [APX Part 26 — Site Configuration (optional class `apx-site-config`)](#26-site-configuration)
28. [APX Annex A — Conformance Requirements and ICS Template (normative)](#annex-a-ics)

---
<a id="00-scope"></a>

# APX Part 0 — Scope and Overview

**Status:** Draft, edition 0.16.0. The exact set of artifacts this edition
pins is `spec/edition.json` (Part 3 §3.4) · **Normative unless marked
otherwise.** The key words
MUST, MUST NOT, REQUIRED, SHALL, SHOULD, MAY are to be interpreted as
described in RFC 2119 / RFC 8174.

## 0.1 What APX is

APX (APDS Parking eXtensions) is an **additive companion standard** to the
APDS 4.1 API specification (Alliance for Parking Data Standards; core data
model published as ISO/TS 5206-1). APX defines the interoperable API surface
that APDS deliberately leaves out: real-time delivery, operational control,
alerting, discovery, and the operational domains built on them (accounts &
payments, LPR, reservations, permits, tolling, violations/enforcement,
validation programs, credential lifecycle, valet, EV charging, plate
lists).

## 0.2 Prime directive: APDS-first

1. Wherever APDS 4.1 defines a route, schema, or convention, an APX
   implementation MUST use it verbatim. The APDS 4.1 OpenAPI document is
   vendored unmodified in this repository and reused by `$ref`.
2. APX MUST NOT redefine, subset, or re-shape any APDS entity. APX resources
   reference APDS entities via `Reference` / `VersionedReference`.
3. A plain APDS 4.1 client MUST work against an APX implementation without
   modification (for the routes/features it uses).

## 0.3 In scope

- A normative **data profile** over APDS's native routes (Part 5): Full vs
  Change update semantics, tombstones, cursor-based change feeds.
- A normative **delivery fabric** (Part 8): a superset-compatible profile of
  APDS `/webhooks` adding signed delivery, retry policy, ledger, and SSE.
- A **command plane** for devices/lanes (Part 6), an **alert domain**
  (Part 7), **credential-scoped discovery**, and the operational domains.
- **Security profile** (Part 9): OAuth2 client-credentials, APX scopes, and
  grant claims.
- **Extensibility and governance** (Parts 3, 10, 11) for multi-vendor
  adoption.

## 0.4 Out of scope

- Everything APDS itself declares out of scope that APX does not explicitly
  add (data storage/aggregation policy, business logic, general geolocation).
- Physical/electrical device protocols (OSDP, vendor PLC protocols). APX
  defines the *API* that fronts such systems, not the wire protocol to the
  hardware.
- Payment card processing (PCI) mechanics. APX carries payment *records* and
  orchestration references; card capture stays in the implementer's PCI scope.

## 0.5 Documents

| Part | File | Contents |
|---|---|---|
| 0 | 00-scope.md | This document |
| 1 | 01-normative-references.md | Normative and informative references |
| 2 | 02-terms.md | Terms, definitions, notation |
| 3 | 03-conformance.md | Conformance classes, versioning, partner rules |
| 4 | 04-identity.md | Identity, provenance, extensions container |
| 5 | 05-data.md | Data profile over APDS native routes |
| 6 | 06-control.md | Command plane, lanes, validations, devices |
| 7 | 07-alerts.md | Alerts |
| 8 | 08-events.md | Delivery fabric (webhooks + SSE) |
| 9 | 09-security.md | Security profile |
| 10 | 10-extensibility.md | Extension rules for implementers and vendors |
| 11 | 11-registries.md | Code-list registries |
| 12 | 12-errors.md | Error model |
| 13 | 13-accounts-payments-lpr.md | Accounts, payments, LPR, analytics eventing |
| 14 | 14-reservations-permits.md | Reservations and pooled permits |
| 15 | 15-tolling.md | Toll transactions and disputes |
| 16 | 16-discovery.md | Credential-scoped discovery |
| 17 | 17-resolution.md | Customer Service & Resolution (contexts, policy, actions) |
| 18 | 18-aggregation.md | Aggregation and onboarding (multi-site platforms) |
| 19 | 19-violations.md | Violations — automated and guided enforcement, notices/citations, appeals |
| 20 | 20-validations.md | Validations — merchant programs, instrument issuance, redemption ledger, billing statements |
| 21 | 21-credentials.md | Credentials — keycard/fob/RFID/mobile lifecycle, replacement, access-event history |
| 22 | 22-valet.md | Valet — custody, condition evidence, retrieval queue and ETA, staging, verified handback |
| 23 | 23-charging.md | EV charging, the parking-side view: live point status with bay-presence fusion, ChargingSession on the APDS Session, idle after complete, settlement, charger control via Part 6 |
| 24 | 24-plate-lists.md | Plate lists — watch, scofflaw, allow, and enforcement-vehicle lists with per-place flags, their entries, and the change feed |
| 25 | 25-ui-launch.md | UI launch — a vendor's own screens opened for an agent with automatic login (one-time URL or signed token), and everything done there looped back as APX data |
| 26 | 26-site-configuration.md | Site configuration — equipment roles and detail, areas, cameras and views, comms points, escalation roles, external identifiers, display records, and the change notices that describe a location from APX alone |
| A | annex-a-ics.md | Conformance requirements and ICS template |

The machine-readable OpenAPI 3.1 document (`spec/openapi/apx.yaml`, bundled
as `spec/dist/apx-v1.*`) and the **APX data-profile overlay**
(`spec/openapi/overlays/apx-data-overlay.yaml`, OpenAPI Overlay 1.0) are
**normative**; applying it to the bundle — done automatically by the
build — produces the effective API description. That overlay exists so the
Part 5 additions to APDS-native routes are machine-readable without editing
the vendored APDS document.

Two further overlays are applied by the same build step and are
**informative**: `apx-docs-overlay.yaml` carries the reader's orientation,
the per-domain narrative on each tag, and the navigation grouping;
`apx-examples-overlay.yaml` carries one worked payload per resource schema,
each lifted from a scenario and validated in CI. Neither adds, removes, or
constrains anything in the contract. The distinction is deliberate: were
narrative normative, requirements would live in two places, which is
precisely what the precedence rule below exists to prevent.

Precedence: for **APX-defined paths and schemas**, where prose and OpenAPI
disagree, the effective OpenAPI description prevails and the prose defect
MUST be corrected. For **APDS-native paths**, the vendored APDS 4.1
document (plus the overlay's additive decorations) prevails — APX prose
never overrides APDS.

---

<a id="01-normative-references"></a>

# APX Part 1 — References

## 1.1 Normative

- **APDS API Specification 4.1** — `apds-api-4.1.yaml` (vendored at
  `spec/vendor/apds/4.1/`, MIT, © Alliance for Parking Data Standards);
  source: https://github.com/parkingdata/spec, pinned at upstream commit
  `e10dcfc4cf5e45aaa2f46641a235333fb5585b1b` (2026-03-03). The vendored
  copy is byte-identical to that revision (verified 2026-09-02;
  checksum-guarded by `tools/check-vendor.mjs`). 4.1 is the latest
  published APDS API release as of this edition.
- **APDS Information Model v4.1** and **APDS Data Dictionary v4.1** —
  Alliance for Parking Data Standards (member/request distribution)
- **RFC 2119 / RFC 8174** — Key words for use in RFCs
- **RFC 3339** — Date and Time on the Internet
- **RFC 9562** — Universally Unique IDentifiers (obsoletes RFC 4122;
  mentions of "RFC 4122 UUID" in schema prose denote the same format)
- **RFC 6749** — OAuth 2.0 (client credentials grant)
- **RFC 7240** — Prefer Header for HTTP
- **RFC 8615** — Well-Known URIs
- **RFC 8705** — OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens (class `apx-mtls`, Part 16 §16.3)
- **RFC 9457** — Problem Details for HTTP APIs
- **OpenAPI Specification 3.1.0** and **Overlay Specification 1.0.0**
- **JSON Schema 2020-12**
- **HTML Server-Sent Events** (WHATWG HTML Living Standard, snapshot as of
  this edition's date) — normative for the `apx-events-sse` class only

## 1.2 Informative

- **APDS Overview v4.1**, **APDS Use Case Document v4.1** (esp. §C.1.1.6
  Full/Change updates, §C.2.2 enforcement vocabularies, §C.2.5 extension
  mechanism)
- **ISO/TS 5206-1:2023** — Parking — Part 1: Core data model
- **CloudEvents 1.0** — informative mapping for the APX EventEnvelope
- **CEN/TS 16157-6 (DATEX II parking)** — related European data exchange

## 1.3 Known APDS 4.1 errata

APX vendors the APDS 4.1 OpenAPI document **verbatim** (checksum-guarded),
including its errata. Filing-ready reports for all fourteen below, each
with the offending snippet and a minimal suggested fix, are in
[`docs/errata/`](../errata/README.md), which tracks their status upstream. Verified against `parkingdata/spec` at `master` on
2026-09-23: the published document was byte-identical to the vendored copy,
so all of them are live in the current release. Where APX needs a working
shape before APDS ships a fix, the data overlay (Part 5 §5.6) supplies it
in the bundled document, narrowly, citing the erratum; the vendored file
is never touched.

Implementers validating payloads against the raw schemas should be aware
of:

1. **`Reference` is unsatisfiable as written.** The schema declares
   `maxProperties: 1` while requiring *both* `id` and `className` — no
   object can satisfy it. APX follows the schema's stated intent (and every
   APDS example): a reference is `{"id", "className"}`. Validators MUST
   relax the `minProperties`/`maxProperties` constraints on `Reference`
   (see `tools/validate-scenarios.mjs` for the reference treatment). Filed
   upstream as [parkingdata/spec#33](https://github.com/parkingdata/spec/issues/33);
   remove this entry once a corrected release is vendored.
2. **`POST /observations` `single-element` example is invalid against its
   own schema.** The embedded request example omits the required `id` and
   matches no branch of the request `oneOf`. The schema is authoritative;
   the example is defective. Lint tooling scopes an exemption to this path
   (`tools/.spectral.yaml`) rather than editing the vendored file. Filed
   upstream as [parkingdata/spec#34](https://github.com/parkingdata/spec/issues/34);
   remove on a corrected release.
3. **Discriminator mapping typo on `POST /observations`.** The mapping key
   for the observation-set branch is spelled `ObvservationSet` (extra `v`),
   so the legal `ObservationDataType` value `ObservationSet` has no mapping
   entry. Tolerant tooling resolves the subschema by name anyway; strict
   tooling does not. APX needs no workaround for this one. Filed upstream as
   [parkingdata/spec#35](https://github.com/parkingdata/spec/issues/35);
   remove on a corrected release.
4. **`RateTable` requires two members it never defines**
   (`validityStart`, `activeTimes`), so no rate table validates. The data
   overlay drops them from `required`. [Erratum 004](../errata/004-ratetable-phantom-required.md).
   Filed upstream as [parkingdata/spec#37](https://github.com/parkingdata/spec/issues/37).
5. **`POST /observations` declares no responses.** The overlay declares
   201/400/409 `ResponseStatus`, as on the other native creates.
   [Erratum 005](../errata/005-observations-post-no-responses.md).
   Filed upstream as [parkingdata/spec#38](https://github.com/parkingdata/spec/issues/38).
6. **`POST /observations`: the wrapper `type` collides with
   `ObservationElement.type`**, so no single observation validates. The
   overlay replaces the request schema with a plain
   `oneOf [ObservationElement, ObservationSet]`.
   [Erratum 006](../errata/006-observations-discriminator-collision.md).
   Filed upstream as [parkingdata/spec#39](https://github.com/parkingdata/spec/issues/39).
7. **`POST /quotes` declares no request body**, and `GET /quotes` returns
   one object from a list query. The overlay adds an optional request
   body; the `GET` is documented as is (Part 5 §5.7).
   [Erratum 007](../errata/007-quotes-post-no-request-body.md).
   Filed upstream as [parkingdata/spec#40](https://github.com/parkingdata/spec/issues/40).
8. **`ReferenceToQuote` is a `oneOf` of two identical branches**, so
   booking from a quote reference never validates. No additive
   workaround exists; book with the full `AssignedRight`.
   [Erratum 008](../errata/008-referencetoquote-identical-branches.md).
   Filed upstream as [parkingdata/spec#41](https://github.com/parkingdata/spec/issues/41).
9. **`Identifiers` requires `rateTableId` but declares `rateTableID`.**
   The overlay declares the required spelling.
   [Erratum 009](../errata/009-identifiers-ratetableid-spelling.md).
   Filed upstream as [parkingdata/spec#42](https://github.com/parkingdata/spec/issues/42).
10. **No entity schema declares `extensions`** (Use Case §C.2.5). The
    overlay declares it on the eight native entity schemas, and on
    `Payment` for the `apds-ext:apx:payment-detail@1.0` decoration
    (Part 13 §13.6a).
    [Erratum 010](../errata/010-entities-no-extensions.md).
    Filed upstream as [parkingdata/spec#43](https://github.com/parkingdata/spec/issues/43).
11. **`GeoJsonObject` declares no `coordinates`.** The overlay declares an
    optional array. [Erratum 011](../errata/011-geojsonobject-no-coordinates.md).
    Filed upstream as [parkingdata/spec#44](https://github.com/parkingdata/spec/issues/44).
12. **Response-code irregularities** on `POST /rates` (200, no 400/409),
    `PUT /rights/assigned/{id}` (201, no 404), and the contact reads
    (only 500s). The overlay adds the missing codes; Part 5 §5.7 records
    the rest. [Erratum 012](../errata/012-native-response-code-irregularities.md).
    Filed upstream as [parkingdata/spec#45](https://github.com/parkingdata/spec/issues/45).
13. **`HierarchyElement` maps only three of its ten types**, so
    `VehicularAccess` (a lane) validates as a bare `HierarchyElement` and
    its `accessType` is never checked. APX binds its own
    `apds-ext:apx:lane-cameras@1.0` decoration (Part 4 §4.3), so camera
    configuration is validated; the APDS lane fields are carried as sent.
    Devices are affected the same way: a `SupplementalEquipment` (the
    Part 6 control target) is also unmapped, so only its
    `apds-ext:apx:devicestatus@1.0` decoration is checked.
    [Erratum 013](../errata/013-hierarchyelement-subtypes-unmapped.md).
    Filed upstream as [parkingdata/spec#46](https://github.com/parkingdata/spec/issues/46).
14. **No identifiable `RightHolder`.** The APDS Information Model defines
    the RightHolder as the entity an AssignedRight is issued to, but the
    OpenAPI document carries it only as `AssignedRightHolder`, embedded in
    each AssignedRight with no `id`, so nothing can reference a holder.
    APX references one as `{"id", "className": "RightHolder"}`, using the
    Information Model's class name, with the id local to the issuing
    implementation (Part 14 §14.1a). APX defines no schema for the holder;
    an APDS definition, once published, governs. Raised as a question,
    not an error. [Erratum 014](../errata/014-rightholder-not-identifiable.md).
    Filed upstream as [parkingdata/spec#47](https://github.com/parkingdata/spec/issues/47).

Remove an entry, and its overlay action, in the same change that vendors a
corrected APDS release.

---

<a id="02-terms"></a>

# APX Part 2 — Terms, Definitions, Notation

Terms defined by APDS 4.1 (Place, Campus, SubplaceElement, IdentifiedArea,
Space, RightSpecification, AssignedRight, RightHolder, RightPool, PlannedUse,
Session, Segment, Observation, Credential, RateTable, Quote, Supply, Demand,
Distributing Party, Receiving Party) are used with their APDS meanings and
are NOT redefined here. RightHolder is defined by the APDS Information
Model; the 4.1 OpenAPI document has no identifiable schema for it, so the
`className: "RightHolder"` convention APX uses to reference one is
described in Part 1 §1.3 item 14.

## 2.1 APX-defined terms

- **APX implementation (server)** — a system exposing APDS 4.1 routes plus
  one or more APX conformance classes.
- **APX client** — a system consuming an APX implementation under an OAuth2
  client registration.
- **Lane** — the operational entry/exit position addressed by control
  commands; modeled as an APDS `VehicularAccess` IdentifiedArea, optionally
  narrowed to a `SupplementalEquipment` device.
- **Device** — a physical unit inventoried as APDS `SupplementalEquipment`;
  APX overlays live state on it (Part 6).
- **Command** — an APX resource requesting an operational action against a
  target lane/device/place (Part 6).
- **Alert** — an APX resource describing an operational condition requiring
  attention (Part 7).
- **Subscription** — an APX-extended APDS `EventSubscription` (Part 8).
- **Event** — one `EventEnvelope` delivery (Part 8).
- **Topic** — the event type string; either an APDS `EventTypeEnum` value or
  an APX topic `apx.<domain>.<event>.vN`.
- **Conformance class** — a named, testable unit of APX functionality
  (Part 3), registered in `apx-conformance-classes`.
- **Grant** — the pair of token claims (`apx_org`, `apx_places`) bounding
  what a client may see/do (Part 9).
- **Charging point** (Part 23) — an APDS
  `ElectricChargingEquipment` hierarchy element; APX overlays live
  availability and bay presence on it (`ChargingPointStatus`).
- **Charging session** (Part 23) — an APX resource recording
  one charge of one vehicle at one charging point as it relates to the
  parking `Session` it happened inside (`ChargingSession`).
- **Plate list** (Part 24) — a list of vehicle credentials kept by an
  operator or a city, with a category (`allow`, `watch`, `scofflaw`,
  `enforcementVehicle`) and per-place flags saying what a match does.
- **Pairing engine** — the system that turns reads into Sessions and
  matches Sessions to rights, tickets, and lists; when it is a separate
  client of the APX server it writes what it decided with the Part 5
  §5.8 decoration.

## 2.2 Notation

- All timestamps are RFC 3339 in UTC unless a Place-local time is explicitly
  indicated (APDS convention). Intervals are start-inclusive, end-exclusive.
- **APX-defined resource** identifiers are RFC 4122/9562 UUIDs. APDS entity
  identifiers are whatever APDS defines them to be (unique strings within
  the issuing implementation — UUIDs by convention, not by contract; APX
  cannot and does not re-type them, Part 4 §4.1a). Cross-object references
  use APDS `Reference` / `VersionedReference` and are typed by `className`.
- JSON field names are camelCase. Paths are lowercase kebab-case.
- `apx.<domain>:<verb>` denotes an OAuth scope; `apx.<domain>.<event>.vN`
  denotes a topic; `apds-ext:<ns>:<class>@<M.m>` denotes an extension key.

---

<a id="03-conformance"></a>

# APX Part 3 — Conformance, Versioning, Governance

## 3.1 Conformance classes

APX functionality is partitioned into named conformance classes, registered
in `spec/registries/apx-conformance-classes.json`. An implementation:

1. MUST implement the APDS 4.1 routes required by each class it claims.
2. MUST advertise its supported classes in `/.well-known/apx-configuration`.
3. MUST satisfy every normative requirement (MUST/MUST NOT) that this
   standard maps to those classes.
4. MUST NOT partially implement a claimed class.

`apx-data` and `apx-events` are the **base classes**; every other class
builds on them. `apx-control`, `apx-alerts`, `apx-events-sse`,
`apx-discovery`, `apx-accounts`, `apx-payment-history`, `apx-lpr`,
`apx-reservations`, `apx-permits`, `apx-tolling`, `apx-resolution`
(requires `apx-control`), `apx-violations`, `apx-validations`,
`apx-credentials`, `apx-valet`, `apx-charging`, `apx-plate-lists`,
`apx-ui-launch`, `apx-site-config`, and `apx-mtls` are optional.

**Dependency table (normative).** A claimed class requires every class in
its row:

| Class | Requires |
|---|---|
| `apx-data`, `apx-events` | — (base; both required by every other class) |
| `apx-events-sse` | `apx-events` |
| `apx-control`, `apx-alerts`, `apx-discovery`, `apx-accounts`, `apx-lpr`, `apx-reservations`, `apx-permits`, `apx-tolling`, `apx-violations`, `apx-validations`, `apx-credentials`, `apx-valet` | base classes |
| `apx-payment-history` | `apx-accounts` |
| `apx-resolution` | `apx-control` |
| `apx-charging` | base classes; `apx-control` when the Part 23 §23.9 charger commands are offered |
| `apx-plate-lists` | base classes |
| `apx-ui-launch` | base classes; `apx-resolution` when the Part 25 §25.5 execution descriptor is offered |
| `apx-site-config` | base classes |
| `apx-mtls` | — (composable with any set) |

A claim that is not closed under this table is non-conforming; a client
reading one treats the dependent class as not offered (Part 16 §16.1).

**Vendor classes.** A vendor that defines its own conformance class (§3.3)
MUST name it `<vendor-ns>-<class>` (e.g. `acmecorp-loyalty`), where
`<vendor-ns>` is the namespace of its extension keys, and MUST NOT begin it
with `apx-`, which is reserved for classes in the registry. Vendor classes
MAY be advertised alongside APX classes in the bootstrap and discovery
documents (Part 16); clients MUST ignore classes they do not recognise.

The numbered requirements for each class, and the Implementation
Conformance Statement (ICS) template a certification body or implementer
fills in, are in **Annex A**.

## 3.2 Versioning

- The APX major version is carried in the URL (`/v1`). APDS-native routes
  are versioned by APDS itself.
- Within a major version, all changes are **additive only**: new optional
  fields, new endpoints, new code-list entries, new conformance classes.
  Removing or re-typing anything requires a new major version.
- Event topics carry independent versions (`.v1`, `.v2`); a topic's payload
  contract never changes within its version.
- Each registry (UserDefinedCodeList) versions independently per APDS
  PkCommon rules: any entry addition increments the list version.
- The written standard is published in **editions**; an edition pins the set
  of module and registry versions it comprises.
- Deprecation: a feature marked deprecated remains functional for at least
  one further published edition AND 12 months, whichever is longer.

## 3.3 Rules for partner/vendor extensions (normative)

Companies extending APX (the "extension-of-the-extension" path):

1. Vendors MUST use their own namespace in extension keys:
   `apds-ext:<vendor-ns>:<class>@<M.m>`. The `apx` namespace is reserved for
   this standard.
2. Vendors MUST NOT add unregistered keys outside an `extensions` container.
3. Vendor-specific endpoints MUST live under `/apx/x/<vendor-ns>/…`.
4. Vendor event topics MUST be prefixed `<vendor-ns>.` — never `apx.`.
5. Vendors MAY publish their own UserDefinedCodeLists and MAY reference APX
   lists via `ReferencedCodeListEntry`.
6. Implementations MUST preserve unknown `extensions` keys on round-trip
   (tolerant reader, faithful writer).
7. A vendor extension adopted by the working group is renamed into the `apx`
   namespace in the next edition, with the vendor key kept as a documented
   alias for one deprecation window.
8. **APDS reconciliation (the other direction).** When APDS itself
   standardizes a concept APX occupies (e.g. native payment events or an
   actuation surface in a future APDS release), the APDS construct WINS:
   the next APX edition adopts it verbatim per the prime directive, marks
   the APX construct deprecated per §3.2's deprecation window, and
   documents the migration. APX is a proving ground for APDS, never a
   competitor — this clause is the mechanism.

## 3.4 Publication

The normative artifacts of an APX edition are: the bundled OpenAPI
document, the APX data-profile overlay
(`spec/openapi/overlays/apx-data-overlay.yaml`, whose application to the
bundle produces the effective API description — Part 0 §0.5), the registry
JSON files, Parts 0–22 of this written standard, and Annex A (conformance
requirements and ICS template).

Each edition names that set exactly, by checksum, in **`spec/edition.json`**:
the bundle and every overlay, each registry with its version, the vendored
APDS release and its checksum, the Parts, and the conformance classes. The
manifest is generated from the artifacts themselves and CI fails if it goes
stale, so an edition is a fixed set of files rather than the state of a
branch. Cite it when proposing adoption.

The documentation overlays (`apx-docs-overlay.yaml`,
`apx-examples-overlay.yaml`) appear in the manifest for completeness but are
**informative**, per Part 0 §0.5: they carry narrative and examples, not
contract. The spec tooling in `tools/` is supporting material, not
normative.

## 3.5 APDS version policy

APX pins exactly ONE APDS release per edition — currently 4.1, vendored
byte-identical, checksum-guarded, and commit-pinned in Part 1.

1. **When APDS ships a new release (4.2, 5.0…):** re-vendoring it is a new
   APX edition. The rebase diff runs through the same breaking-change gate
   as any change; APDS-caused breaking changes make the new edition a new
   APX major. §3.3(8) reconciliation applies to any APX construct the new
   APDS release absorbs.
2. **No in-band version negotiation.** A server implements one APDS
   version and advertises it as `apdsVersion` in
   `/.well-known/apx-configuration`; clients check the bootstrap document
   before first use. (OCPI-style `/versions` negotiation was considered
   and rejected: dual-mounting two APDS releases on one host doubles every
   implementation for a transition case better served by running the new
   edition on a new deployment and cutting over per Part 18.)
3. **Support window.** When a new edition ships, the prior edition remains
   published and its registries frozen; implementers get at least the
   §3.2 deprecation window (one edition AND 12 months) before any APX
   feature removed by reconciliation stops being valid to serve.

---

<a id="04-identity"></a>

# APX Part 4 — Identity, Provenance, Extensions

## 4.1 Identity

Every APX-defined resource carries the `ApxResourceCore` identity:

- `id` — RFC 4122 UUID, unique within the resource class. Client-supplied
  ids follow APDS convention (server returns HTTP 409 on collision).
- `version` — positive integer, incremented on every state-changing update.

This is deliberately the same shape APDS uses for `D2VersionedIdentifiable`
objects, so **any APX resource can be the target of an APDS
`Reference`/`VersionedReference`** with `className` set to the APX class name
(e.g. `{"id": "…", "className": "Command"}`).

APX resources reference APDS entities the same way — never by embedding or
copying them. Example: a Command targets
`{"id": "<uuid>", "className": "SupplementalEquipment"}`.

## 4.1a Identifier locality and aggregation (normative)

**All identifiers — APX resource ids and APDS entity ids alike — are LOCAL
to the issuing implementation.** Clients MUST NOT assume an id minted by
one implementation resolves at another. (This generalizes Part 14 §14.1a,
which states the rule for RightHolders; it holds for HierarchyElements and
every other class.) APDS entity ids are strings with per-implementation
uniqueness — UUIDs by convention only.

For **aggregating implementations** (one endpoint fronting many locations,
Part 18):

1. On import, an aggregator SHOULD preserve the source implementation's
   HierarchyElement ids. When a collision with an already-hosted id makes
   that impossible, it MUST mint new RFC 9562 UUIDs for the colliding
   subtree.
2. Whenever an imported element's id differs from its source id, the
   aggregator MUST record the source id in the element's APDS
   `operatorDefinedReference` — the designated alias field — so existing
   integrations, grants, and warehouse keys can be migrated by lookup
   rather than guesswork.
3. Re-minting is a new identity: previously issued `apx_places` grants,
   subscription `filters.places`, and stored references DO NOT carry over
   automatically (Part 18 defines the onboarding sequence).

## 4.2 Provenance

Every APX resource SHOULD carry `recordInfo` (`RecordInfo` schema), aligned
with the APDS Information Model's RecordType: `creationTime`, `creator`
(Reference to Organisation/Contact), `creationUser`, `lastUpdate`,
`lastUpdateUser`. Where a resource has a lifecycle audit (Commands, Alerts,
TollTransactions), the immutable `statusHistory[]` is the authoritative
audit record and MUST NOT be truncated or rewritten.

## 4.3 The extensions container

Every APX resource schema includes an optional `extensions` object — the
official APDS extension mechanism (Use Case §C.2.5):

```json
"extensions": {
  "apds-ext:apx:devicestatus@1.0": {
    "device": { "id": "c1000000-0000-4000-8000-000000000011", "className": "SupplementalEquipment" },
    "deviceState": "fault"
  },
  "apds-ext:acmecorp:loyalty@2.1": { "tier": "gold" }
}
```

- Keys MUST match `^apds-ext:[a-z0-9-]+:[a-z0-9-]+@[0-9]+\.[0-9]+$`.
- The version component is the extension class's own contract version.
- Readers MUST ignore unknown keys; writers MUST preserve them (Part 3 §3.3).
- An **APX decoration key** (`apds-ext:apx:…`) MUST carry a value valid
  against that decoration's schema wherever the container appears — the
  `Extensions` schema binds each one (`devicestatus`, `ratepolicy`,
  `lpr-read`, `lane-cameras`, `reservation`, `permit`, `session-detail`,
  `match-evidence`, `violation-source`, `payment-detail`,
  `credential-digest`, `equipment-detail`, `camera`, `external-ids`,
  `place-operations`, `stay-compliance`), so a validator
  checks them on every payload. `alert-evidence` and `correlation` are
  defined in prose only (Part 7 §7.1, Part 17) and are not yet bound.

APX also uses this container in the **other direction**: attaching APX data
to APDS entities (e.g. `apds-ext:apx:devicestatus@1.0` on a
`SupplementalEquipment` in a Place payload). These Level B decorations are
the only way APX data appears inside APDS payloads.

## 4.2a Optimistic concurrency and idempotent replay (normative)

Every APX write that replaces or amends a versioned resource (a `PUT`, a
`PATCH`, or a state-changing action on a resource that carries `version`)
follows one rule, so a stale writer never silently wins:

1. **The precondition is the version the client last read.** A client
   SHOULD send it as `If-Match: "<version>"` (the version integer as an
   entity tag). A client MAY instead send the resource's `version` member
   in the body, with the same meaning; `version` is marked readOnly because
   the server assigns it, and this is the one place a client echoes it
   back. `version` in a body is never "the next version".
2. **A stale precondition is 409 `version-conflict`**, with the current
   `version` in `detail`. Nothing is written.
3. **No precondition** means last-writer-wins. Servers MAY refuse such a
   write with 428 Precondition Required on resources whose Part says so.
4. On success the response carries the new `version` and SHOULD carry
   `ETag: "<version>"`.

**Idempotent replay.** Where an operation takes `Idempotency-Key`, a
replay of the same key with the same body returns the resource's
**current** representation with the original status code's success class
(200 for a replayed create), not a snapshot taken when it was first
created. The same key with a different body is 409 `idempotency-conflict`.
Keys are scoped to the credential and the operation, and servers MUST
retain them for at least 24 hours.

## 4.4 Datatype conventions

APX reuses APDS datatypes by `$ref`: `DateTime` (RFC 3339), `Duration`
(ISO 8601), `MultilingualString`, `AmountInCurrency` (ISO 4217),
`Reference`, `VersionedReference`, `DeletedReference`, `Url`. APX MUST NOT
introduce parallel datatypes for concepts APDS already models.

---

<a id="05-data"></a>

# APX Part 5 — Data Profile (over APDS native routes)

The `apx-data` conformance class. APX defines **no parallel data routes**:
pull and push use APDS 4.1's own endpoints (`/places`, `/observations`,
`/contacts`, `/rights/specs`, `/rights/assigned`, `/rates`, `/sessions`,
`/quotes`, and the by-id routes APDS defines under them, such as
`/observations/{id}` and `/quotes/{id}`), mounted verbatim. This Part defines the additive behavior APDS
leaves open.

## 5.1 Update modes (generalizing APDS Use Case C.1.1.6)

APDS defines "Full" vs "Change" updates for Place information. APX
generalizes this to **all** the entity classes above, in both directions:

- **Full** — the payload is the complete current state of the object.
  Fields absent from the payload have no defined value.
- **Change** — the payload carries identity (`id`, `version`) plus only the
  changed fields. **Explicit `null` clears a field; an absent field is
  unchanged.** This is the APDS null-out sentinel rule, made normative for
  every class.

Updates (`PUT` on native routes) declare their mode with the request
header `APX-Update-Mode: full|change` (default `full`, preserving stock
APDS behavior). A create (`POST`) has no prior state to merge into, so it
always carries the complete state; a server receiving
`APX-Update-Mode: change` on a `POST` treats the body as full. Servers
MUST reject a change-mode write targeting a stale `version` with problem
`version-conflict`.

## 5.1a Validating writes, and the error dialect (normative)

The native request schemas describe complete objects, so a change-mode
body cannot validate against them. The data overlay therefore declares
each native `PUT` body as `anyOf [<native schema>, <Class>ChangePayload]`,
where `<Class>ChangePayload` (for example `RateTableChangePayload`) is
derived from the class at build time:

- With `APX-Update-Mode: full` (or no header) the body MUST validate
  against the native schema. A validator that knows the header MUST
  apply that branch alone: a body that is a valid change payload but an
  incomplete object (an `AssignedRight` without `rightSpecification`) is
  valid only in change mode.
- With `APX-Update-Mode: change` the body MUST validate against the
  class's change payload: `id` present, `version` optional (the
  precondition when present, Part 4 §4.2a), and every other member
  optional and MAY be `null` ("clear"). Each scalar member present is
  checked against the class's own property schema, `extensions` against
  the APX `Extensions` container (Part 4 §4.3), and a member the class
  does not define is refused with 400. Nested objects, and array items
  that are objects, are left open in the schema because those that carry
  their own `id` (a Session segment) follow the same change rule; the
  server MUST validate them against the class's native property schema.
  The generic `ChangePayload` is the shape of every `ChangeFeedPage` item,
  so what a writer sends and what a reader receives are the same thing.
  (Until 0.12.2 the native `PUT`s used the generic `ChangePayload`, which
  requires only `id`, so a full-mode body carrying an `id` was never
  checked.)

Native routes keep their APDS error shape (`ResponseStatus`) by default.
The overlay adds `application/problem+json` (`Problem`) as a second
media type on every native 400, 404, and 409; a client that sends
`Accept: application/problem+json` MUST receive the Problem, with the
registered type: `invalid-request` for a 400, `target-not-found` for a
404, `id-collision` for a 409 on a create and `version-conflict` for a
409 on an update. The overlay also declares the shared 401, 403, and 429
(`unauthenticated`, `insufficient-scope`/`insufficient-grant`,
`rate-limited`) on every native operation; these have no APDS
equivalent and are always Problems. A cursor refusal (§5.2) is always a
Problem too, because the cursor is an APX parameter.

## 5.2 Change feed (pull deltas)

APDS natively provides `modified_since` on list routes, returning changed
entities plus `deletedReferences` tombstones in the `PaginatedList` shape —
APX designates that as the **coarse fallback**. For gapless incremental
sync, APX adds the parameters `mode` and `cursor`:

- `mode=full` (or absent) — stock APDS list behavior, byte-compatible.
- `mode=change` — on a native route, returns a `ChangeFeedPage`: changed
  entities (change-mode payloads), `deleted[]` tombstones (APDS
  `DeletedReference`), and an opaque `cursor` resuming strictly after this
  page. On an APX list route the same members ride in the route's own
  list envelope (rule 8).

Rules:

1. The feed is **ordered and gapless** per class: replaying from any issued
   cursor yields every change after it exactly once.
2. Cursors are opaque; clients MUST NOT parse them. Servers MUST retain
   enough history to honor cursors at least 7 days old; older cursors get
   404 with problem `target-not-found` (always a Problem, §5.1a) and the
   client re-syncs with `mode=full`.
3. When no cursor is held, clients use the NATIVE `modified_since`
   parameter (stock APDS) and then switch to cursors. Note the type:
   APDS declares `modified_since` as a **Unix epoch integer** (seconds),
   not an RFC 3339 string like every APX time — `modified_since=1785542400`,
   not `modified_since=2026-08-01T00:00:00Z`.
4. Tombstones MUST be emitted for deletes and retained for the same window.
5. **Cursor scope.** A cursor is scoped to (entity class, credential,
   filter set). Presenting a cursor with a different filter set or from a
   different credential yields problem `target-not-found`; the client
   re-syncs. Cursors are server-local: they never survive a move to a
   different implementation (Part 18).
6. **Filtered feeds.** `mode=change` composes with the native APDS filters
   (e.g. `place_ids`): the resulting feed MUST be ordered and gapless
   *within that filter set* — every change to an entity matching the
   filter appears exactly once. Grants compose the same way: the feed a
   credential sees is gapless within its `apx_places` world.
7. **Grant expansion.** When a credential's `apx_places` grant later gains
   a subtree, existing cursors DO NOT retroactively include the new
   subtree's history. Servers MUST signal this by including the new
   subtree roots in `ChangeFeedPage.grantAdditions` on the first page
   served after the change; the client then runs `mode=full` for those
   subtrees before relying on the feed for them, selecting each with the
   native `place` filter (subtree-inclusive; the data overlay adds it to
   `/places`, which APDS 4.1 omits).
8. **APX resources.** A module that runs apart from the implementation
   it reads from — a pairing engine, an enforcement back office, a site
   server that replicates to an aggregator — catches up after an outage
   by cursor. Webhook retries end after 24 hours and an SSE stream keeps
   as little as 1000 events or 15 minutes (Part 8 §8.3–8.4), so without a
   feed a link loss longer than that loses violations, alerts, or tickets
   for good. Every APX list route of a stateful resource in the table
   below therefore MUST accept `mode=change&cursor=` (`ModeParam` and
   `CursorParam` on the operation). The obligation belongs to the class
   that owns the route, not to `apx-data`. Rules 1, 2 and 4–7 apply
   unchanged: ordered and gapless per (route, credential, filter set),
   cursors opaque and honoured for at least 7 days, a refused cursor is
   404 `target-not-found` (always a Problem), tombstones retained for the
   same window, grant additions signalled. The differences:
   1. **Envelope.** The change page is the route's own list envelope, not
      a `ChangeFeedPage`: `data` holds the changed resources, and the
      optional members `updateMode`, `cursor`, `deleted`,
      `grantAdditions`, and `next` carry what the `ChangeFeedPage`
      members of the same name carry. A change page MUST carry
      `updateMode: change` and `cursor`, and an ordinary (`mode=full`)
      page MUST NOT carry `updateMode`, so a client tells the two kinds
      of page apart by `updateMode`. `meta` is absent on a change page.
      (Swapping the response for a `ChangeFeedPage` would re-type a
      response that already exists, which Part 3 §3.2 forbids within a
      major version.)
   2. **Items are whole.** Each item in `data` is the resource's complete
      current representation, never a change payload: APX resources
      change by domain operations, not by change-mode writes, and each
      keeps its own history (`statusHistory[]`, `amountHistory[]`). A
      server MAY deliver only the latest state of a resource that changed
      several times after the cursor; it MUST NOT deliver an older state
      after a newer one. Consumers key on `id` and keep the highest
      `version` where the resource has one.
   3. **Starting the feed.** These routes have no `modified_since`
      (rule 3). `mode=change` without a `cursor` returns a page with no
      items whose `cursor` is the feed's current head. A client starting
      from nothing takes the head cursor first, then lists with
      `mode=full`, then follows the cursor; a change that lands between
      the two is seen twice, never missed, and difference 2 resolves it.
   4. **Filters.** `mode=change` composes with the route's own filters
      (rule 6). A change belongs to a filtered feed when the resource
      matched the filter set before the change **or** after it, so a
      consumer filtering on `status` sees the change that takes a
      resource out of its view. The `page` parameter is not used with
      `mode=change`; a page ends where the server chooses, and `next`
      (or the `cursor`) continues it.
   5. **Tombstones.** APX resources are rarely deleted (a void is a
      state). `deleted[]` carries the resources a retention purge removed
      (Part 9 §9.6), those released with a moved place (Part 18 §18.4),
      and entries removed from a plate list (Part 24 §24.2 rule 4), with the
      resource's schema name (`Violation`, `ValetTicket`, …) as
      `className`.
   6. **Payments.** `GET /v1/payments` serves keyed lookups only (Part 13
      §13.2). Its feed is the catch-up path for the same records
      `apx.accounts.payment.recorded.v1` already delivers to the
      credential (Part 13 §13.4), so `mode=change` needs no key and has no
      8-hour window, and follows that topic's rules: a hold appears at
      capture, never before, and a voided hold never appears. The
      truncated keys `ticketLast4` and `cardLast4` are refused with
      `mode=change` (400 `invalid-request`), so the feed is never a way
      round the §13.2 window.

| Route | Resource | Required by |
|---|---|---|
| `GET /v1/commands` | Command | `apx-control` |
| `GET /v1/alerts` | Alert | `apx-alerts` |
| `GET /v1/accounts` | Account | `apx-accounts` |
| `GET /v1/payments` | PaymentRecord | `apx-payment-history` |
| `GET /v1/tolling/transactions` | TollTransaction | `apx-tolling` |
| `GET /v1/violations` | Violation | `apx-violations` |
| `GET /v1/enforcement/policies` | EnforcementPolicy | `apx-violations` |
| `GET /v1/enforcement/signage` | Signage | `apx-violations` |
| `GET /v1/validations/programs` | ValidationProgram | `apx-validations` |
| `GET /v1/validations/programs/{id}/issuances` | ValidationIssuance | `apx-validations` |
| `GET /v1/validations/redemptions` | ValidationRedemption | `apx-validations` |
| `GET /v1/validations/programs/{id}/statements` | ValidationStatement | `apx-validations` |
| `GET /v1/credentials` | CredentialRecord | `apx-credentials` |
| `GET /v1/access-events` | AccessEvent | `apx-credentials` |
| `GET /v1/valet/tickets` | ValetTicket | `apx-valet` |
| `GET /v1/charging/sessions` | ChargingSession | `apx-charging` |
| `GET /v1/plate-lists` | PlateList | `apx-plate-lists` |
| `GET /v1/plate-lists/{id}/entries` | PlateListEntry | `apx-plate-lists` |

**Not feeds (informative).** Routes that compute a view rather than list
stored resources take no cursor: `/v1/valet/queue`,
`/v1/enforcement/policies/effective`, `/v1/enforcement/signage/effective`,
`/v1/reservations/recent`, `/v1/validations/providers`,
`/v1/devices` (a status snapshot), `/v1/lpr/reads` and
`/v1/lpr/candidates` (projections of Observations, whose feed is
`/observations`), and `/v1/resolution/contexts/{id}/allowed-actions`.
`GET /v1/credentials/{id}/access-events` is a slice of
`/v1/access-events`; a client feeds the latter with `credential=`.
`/v1/support/interactions` stays a lookup (Part 17 §17.6) in this edition.

**Rate deck mirroring (informative).** A third-party system that needs the
operator's rate deck — a call platform, a reservation channel, a revenue
system — mirrors it exactly this way: `GET /rates?place_ids=…&mode=full`
once, then `mode=change` with the cursor. APDS `RateTable` is the rate
model; APX adds none. What APX does add rides *inside* each table: the
`apds-ext:apx:ratepolicy@1.0` decoration (Part 6 §6.6) says which tables
an agent may offer as a negotiated rate, so the mirror carries that flag
without a second query.

**Machine-readability (normative).** The `mode`/`cursor` parameters and the
`ChangeFeedPage` response alternate are declared by the APX data-profile
overlay (`spec/openapi/overlays/apx-data-overlay.yaml`, OpenAPI Overlay
1.0), applied to the bundled document by the build (Part 0 §0.5, Part 3
§3.4). The overlay decorates exactly the five §5.6 feed routes, declares
the §5.1 header and body on the native `PUT`s and the §5.1a responses on
every native operation, and carries the narrow workarounds for the APDS
4.1 errata listed in Part 1 §1.3; the vendored APDS document itself is
never modified. The APX list routes of rule 8 are APX's own, so their
`ModeParam`, `CursorParam`, change-page members (`ChangeFeedUpdateMode`,
`ChangeFeedCursor`, `ChangeFeedTombstones`, `ChangeFeedGrantAdditions`,
`ChangeFeedNext`), and `CursorRefused` 404 are declared directly in
`spec/openapi/apx.yaml`.

**Design note (informative) — why the feed rides the native routes.** A
separate APX route family (`/v1/changes?class=…`) would have been fully
expressible in plain OpenAPI, but it would duplicate every native list
route's filter surface and create exactly the "parallel construct" Part 10
§10.2 forbids. Keeping the feed on APDS's own routes means cursors,
filters, and grants compose per-collection, and a stock APDS client on the
same route sees stock behavior — at the cost of needing the overlay to
express the additions, which is the trade this standard accepts.

## 5.3 Push (client→server ingest)

Data flows INTO an APX implementation through the same native `POST`/`PUT`
routes under `apx.data:write`, with §5.1 semantics. Occupancy data flows
through the Place hierarchy (APDS model); high-frequency demand publishing
SHOULD use change-mode writes.

## 5.4 Push (server→client)

Change events are delivered over the fabric (Part 8) using APDS's own
`EventTypeEnum` topics (`SessionCreated`, `PlaceUpdated`, …). The event
`data` is the APDS `EventData` shape; `subject` references the entity.
Both schemas are in the bundle through the `apx-native-event` webhook
entry (Part 8 §8.7), so a validator can check an envelope against them. A
subscriber holding a cursor MAY treat events as wake-ups and pull via
`mode=change` (recommended for exactly-once processing).

## 5.5 Occupancy snapshot (convenience read)

Occupancy natively lives inside the Place hierarchy (APDS `Supply` +
`DemandTable`), which makes "how full is this garage right now" an
expensive question — pull the hierarchy, walk to the element, read the
demand table. APX adds one convenience read:

- `GET /v1/places/{id}/occupancy` (scope `apx.data:read`; the `apx_places`
  grant must cover the element) returns an `OccupancySnapshot`: the
  element Reference, `computedAt`, the verbatim APDS `Supply` and latest
  `DemandType` record, and a derived `available` count
  (`supplyQuantity − count`, clamped at 0, `null` when either side is
  unknown).
- The snapshot is a **read-model, not a resource**: it has no id/version,
  is never written directly, and MUST be derivable from the Place
  hierarchy — the hierarchy stays the source of truth (§5.3). Fields the
  implementation does not know are absent, never guessed.
- Material occupancy changes (threshold crossings or publisher-defined
  deltas) publish `apx.data.occupancy.v1` (data: OccupancySnapshot) over
  the Part 8 fabric. Where `apx-alerts` is implemented, a configured
  threshold crossing SHOULD also raise an `occupancyThresholdExceeded`
  alert (Part 7).
- Part 23 §23.7: the snapshot MAY carry an `evCharging`
  block — charging points total, available, charging, occupied but not
  charging, blocked, inoperative — derived from `ChargingPointStatus`, so
  "is there a free charger" is the same read as "is there a free space".

## 5.5a Occupancy history

`GET /v1/places/{id}/occupancy/history?from=&to=&interval=` (scope
`apx.data:read`; the grant must cover the element) returns an
`OccupancyHistoryPage`: one bucket per `interval` (ISO 8601 duration,
default `PT1H`) between `from` and `to`, each with `maxCount`, the
time-weighted `avgCount`, the `supplyQuantity` in force, and how many
demand records it is computed from.

- Buckets align to interval boundaries in the place's time zone.
- Like §5.5, the history is a read-model derived from the APDS
  `DemandTable` records the implementation holds; it is never written
  directly. A bucket with no record carries no counts — never a guess.
- An element the implementation holds no occupancy for is 404
  `target-not-found`. A server MAY refuse an interval shorter than its
  sampling with 400 `invalid-request`.

## 5.6 Conformance

`apx-data` requires: the eight native routes; §5.1 modes on updates and
the §5.1a validation and error-dialect rules; §5.2 change feed on
`/places`, `/sessions`, `/rates`, `/rights/assigned`, and — where the
implementation serves observation ingest — `/observations` (the
exactly-once path Part 13 §13.4 relies on); the stock-APDS compatibility
guarantee (a client sending no APX headers/params observes pure APDS 4.1
behavior). Implementations that hold occupancy data for an element MUST
serve §5.5 for it; implementations with no occupancy data MAY omit the
endpoint entirely (discovery then does not list it); the same holds for
§5.5a. Every `apx-data` implementation accepts and preserves the §5.8
decoration on Sessions (Part 4 §4.3) and refuses a write that breaks
its rules 1, 3, or 6 with the 422 they name; one that also claims `apx-lpr`
enforces its `read-already-bound` rule and answers
`GET /v1/lpr/reads?session=` from it. Every `apx-data` implementation
likewise accepts and preserves the §5.9 decoration, refuses a write that
breaks its rules 1, 2, 4, 5 or 8, and applies its scope rules 7 and 8. Rule 8 of §5.2 is an obligation of
the class that owns each route.

## 5.7 Known APDS 4.1 behaviour on the native routes (informative)

Some native routes differ from their siblings in ways a client written
against one will misread on the next. They are APDS 4.1 as published
(Part 1 §1.3, errata 005, 007, 012); servers MUST NOT "fix" them in a
way that breaks a stock client:

- `POST /rates` answers **200**, not 201, on success (the data overlay
  adds the missing 400 and 409 for parity).
- `PUT /rights/assigned/{id}` declares **201** — a server MAY upsert
  there; one that does not answers 404 (added by the overlay).
- Only `GET /contacts` and `GET /contacts/{contactId}` declare 500.
- `POST /observations` declares no responses in APDS; the overlay
  declares 201/400/409 in the `ResponseStatus` shape of the other native
  creates, and replaces the self-contradictory request wrapper with a
  plain `oneOf [ObservationElement, ObservationSet]` (an element's `type`
  is its `CredentialTypeEnum` value).
- `POST /quotes` declares no request body in APDS; the overlay declares
  an optional `QuoteRightRequest | QuoteSessionExtensionRequest`.
  `GET /quotes` returns the single matching quote object, not a list.
- `PUT /observations/{id}` declares no request body and no 400 in APDS;
  the overlay declares an optional `anyOf [ObservationElement,
  <Class>ChangePayload]` with the §5.1 header, and the 400, as on the
  other native `PUT`s (candidate C-026, not yet filed).

## 5.8 Session detail written by a pairing engine (normative)

APDS describes a Session's money and rights but not how the visit was
assembled: which reads it rests on, why it ended, how sure each match
behind it is, and which visit an area stay belongs to. Where the system
that pairs reads into Sessions is a separate client of the APX server,
it has to write what it decided. It does so with the Level B decoration
`apds-ext:apx:session-detail@1.0` (`SessionDetail`) on the APDS Session,
through the native `POST /sessions` and `PUT /sessions/{id}` (§5.1). A
plain APDS client ignores it. A server that pairs reads itself SHOULD
write the same block on its own Sessions, so consumers see one shape
whoever paired. A write that breaks a rule below is refused with the
problem type the rule names, and nothing is written.

1. **Reads (`reads[]`).** Each entry binds one ObservationElement to the
   Session with a `role`: `entry` and `exit` (the passages that opened
   and closed the visit), `duplicate` (the same passage seen by another
   camera or frame), `activity` (the vehicle seen inside). At most one
   entry is `representative`; two are 422 `request-unprocessable`. A
   reference naming no Observation visible to the caller is 422
   `reference-unknown`. The server derives `LprRead.session` from these
   bindings — naming the Session without a `parent` (the visit) that
   holds the read — and answers `GET /v1/lpr/reads?session=` from them
   (Part 13 §13.3a(6)).
2. **One visit per read.** A read already bound as `entry` or `exit` to
   another Session of the same place MUST be refused with 409
   `read-already-bound`, unless the write names that Session in
   `supersedes` — a re-pairing, which removes the binding from the
   superseded Session in the same write.
3. **Lifecycle and closure.** `lifecycle` is `open` (no `actualEnd`
   yet), `ended` (`actualEnd` set; the pairing may still revise it),
   `final` (no further revision expected), or `disputed`. `lifecycle`
   and `actualEnd` MUST agree: `open` with an `actualEnd`, or any other
   state without one, is 422 `request-unprocessable`. `final` is the
   writer's statement, never a lock: a server MUST NOT refuse a later
   revision on that ground. `closure.endReason` is a definedValue of
   `apx-session-end-reasons` (Part 11) or an implementer code;
   `closure.endInferred` is true when `actualEnd` was set by a rule
   rather than observed, and `closure.rule` names the rule that closed a
   `stale` Session.
4. **Exit-only Sessions.** APDS requires `actualStart`. A Session closed
   from an exit read with no observed entry carries a synthetic
   `actualStart`, which the writer SHOULD set equal to `actualEnd`, and
   `closure.startObserved: false`. Consumers MUST exclude such Sessions
   from dwell statistics and from occupancy derived from Sessions.
5. **Matches (`matches[]`).** Each `MatchEvidence` records how a link
   was established: the `target` (an AssignedRight, a ValetTicket, a
   Violation, a PlateListEntry, an Observation), the `matchType`
   (`right | valet | violation | list | exitPairing | sweep | other`),
   how the plates agreed (`plate.method` `exact | alias | fold | fuzzy |
   lenient | reviewer`, the edit cost, whether the jurisdiction agreed),
   the vehicle evidence as a `level` and a score, whether it was
   `applied` or only a shadow match, and whether a `system` or a
   `person` decided it (`decidedBy`). `target`, `matchType` and
   `applied` are required, and so are `plate.method` and
   `vehicle.level` when those blocks are present. It carries no plate
   characters and no make, model, or colour values (Part 9 §9.6). The
   same object is the decoration `apds-ext:apx:match-evidence@1.0` on a
   Violation (for `relatedSession`, Part 19 §19.13) and a ValetTicket
   (for `session`, Part 22), with the Session as `target`; where both
   ends carry evidence for the same link, the two MUST agree. Evidence
   with `plate.method: lenient` or `applied: false` may excuse a stay
   but never accuse one: a Violation resting on it is not issuable (Part
   19 §19.4 rule 5). `MatchCandidate.matchedBy` (Part 6 §6.7) says how a
   lane inquiry found a candidate; a `matchTicket` that binds one SHOULD
   be recorded with `decidedBy: person`.
6. **Area Sessions.** A Session that is part of a larger visit (a gated
   area inside a garage, a valet stay) names that visit as `parent`. The
   parent's `hierarchyElement` MUST be an ancestor of the child's; a
   `parent` that is not is 422 `request-unprocessable`. An area stay
   shorter than the area's minimum dwell is `passThrough` and is never
   scored or billed. When a visit ends, its children still open SHOULD
   be closed no later than the parent's `actualEnd`, with `endReason`
   `parentExit`. Consumers count visits by Sessions without `parent`.
7. **Events and privacy.** Writes publish the native `SessionCreated` /
   `SessionUpdated` as any Session write does; the decoration adds no
   event topic. It carries references and codes only, never plate
   characters, so it is visible wherever the Session is; the reads it
   names stay behind `apx.lpr:*` (Part 9 §9.6).

```json
"extensions": {
  "apds-ext:apx:session-detail@1.0": {
    "reads": [
      { "observation": { "id": "f3000000-0000-4000-8000-000000000101", "className": "ObservationElement" }, "role": "entry", "representative": true },
      { "observation": { "id": "f3000000-0000-4000-8000-000000000102", "className": "ObservationElement" }, "role": "duplicate" },
      { "observation": { "id": "f3000000-0000-4000-8000-000000000140", "className": "ObservationElement" }, "role": "exit" }
    ],
    "lifecycle": "ended",
    "closure": { "endReason": "exitRead" },
    "matches": [
      {
        "target": { "id": "4b1c0000-0000-4000-8000-000000000042", "className": "AssignedRight" },
        "matchType": "right",
        "plate": { "method": "fold", "editCost": null, "jurisdiction": "agrees" },
        "vehicle": { "level": "good", "llr": 2.31, "model": "evidence-weights@2026-10-01" },
        "applied": true,
        "decidedBy": "system",
        "decidedTime": "2026-10-08T14:02:11Z"
      }
    ]
  }
}
```

The same evidence on a Violation that cites this stay, as the decoration
`apds-ext:apx:match-evidence@1.0`, names the Session as its `target`:

```json
"extensions": {
  "apds-ext:apx:match-evidence@1.0": {
    "target": { "id": "c4000000-0000-4000-8000-000000000077", "className": "Session" },
    "matchType": "violation",
    "plate": { "method": "exact", "jurisdiction": "agrees" },
    "vehicle": { "level": "strong" },
    "applied": true,
    "decidedBy": "person"
  }
}
```

## 5.9 Stay compliance written by a compliance engine (normative)

A compliance engine scores every stay: paid, inside the grace, an
overstay, waiting for review. Other systems need the same answer —
reporting, a city, a call-centre console — without running the engine's
rules again. The engine writes its result with the Level B decoration
`apds-ext:apx:stay-compliance@1.0` (`StayCompliance`) on the APDS
Session, through the native `POST /sessions` and `PUT /sessions/{id}`
(§5.1), as §5.8 does for the pairing. A server that scores stays itself
SHOULD write the same block on its own Sessions. APX carries the
outcome and the policy it was measured against; the rules that turn one
into the other stay in the engine. A write that breaks a rule below is
refused with the problem type the rule names, and nothing is written.

1. **One status.** `status` (REQUIRED) is a definedValue of
   `apx-stay-compliance-statuses` (Part 11) or an implementer code.
   `family` (REQUIRED) is `compliant`, `violation`, `withheld` (not
   scored, or held back for review) or `other` (an outcome that is
   neither, such as `paidLate`). For a registry status, `family` MUST be
   the family the registry gives it; a disagreement is 422
   `request-unprocessable`.
2. **Held for review.** `pendingStatus` names the status the stay would
   have once a person confirms it, and is present only with `status:
   reviewRequired`; a `pendingStatus` with any other status is 422
   `request-unprocessable`.
3. **Flags.** `flags[]` records facts behind the status
   (`paidAfterExit`, `matchedFuzzy`, `endInferred`, `onList`, `paid`): a
   definedValue of `apx-stay-compliance-flags` or an implementer code.
4. **Re-evaluation.** A stay is scored again as reads and payments
   arrive. `evaluatedTime` (REQUIRED) says when, and `sequence`
   (REQUIRED, an integer of at least 1) orders the evaluations of one
   Session. A write whose `sequence` is lower than the one the Session
   holds is 409 `evaluation-out-of-order`; an equal one replaces it.
   `final: true` is the writer's statement that no further evaluation is
   expected, never a lock: a server MUST NOT refuse a later evaluation
   on that ground (as `lifecycle: final` in §5.8).
5. **The policy measured against.** `policy` is a VersionedReference to
   the EnforcementPolicy (Part 19 §19.10) the evaluation used, so its
   `stayGrace` explains the result. One naming no policy visible to the
   caller is 422 `reference-unknown`. It is OPTIONAL: a server without
   `apx-violations` holds no policy.
6. **A status is not a Violation.** A `violation`-family status issues
   nothing and starts no enforcement clock. A Violation is issued only
   under Part 19, with its own evidence and policy rules; it MAY name the
   Session as `relatedSession`, whose status then explains it.
7. **Reading needs `apx.compliance:read`.** Per-stay compliance may go
   to parties beyond the operator, so it has its own scope. A server
   MUST leave the decoration out of every Session it returns to a token
   without `apx.compliance:read` — the native reads, the §5.2 change
   feed and the native `SessionCreated` / `SessionUpdated` deliveries to
   a subscription created without it. The rest of the Session is
   unchanged.
8. **Writing needs `apx.compliance:write`**, beside the scope the
   Session write itself needs. A write that carries the decoration
   without it is 403 `insufficient-scope`, and nothing is written. A
   write that omits the decoration leaves the stored block as it was,
   so a client that cannot see the block cannot erase it. There is no
   removal: an engine that withdraws its score writes `status:
   unscored`.
9. **Events and privacy.** Writes publish the native Session events; the
   decoration adds no topic. It carries codes and references only, never
   plate characters, so it is personal data only through the Session it
   decorates (Part 9 §9.6).

```json
"extensions": {
  "apds-ext:apx:stay-compliance@1.0": {
    "status": "overstay",
    "family": "violation",
    "flags": ["paidAfterExit"],
    "evaluatedTime": "2026-10-21T14:05:00Z",
    "sequence": 3,
    "final": false,
    "policy": { "id": "d1900000-0000-4000-8000-000000000010", "className": "EnforcementPolicy", "version": 4 }
  }
}
```

---

<a id="06-control"></a>

# APX Part 6 — Control (command plane, lanes, validations, devices)

The `apx-control` conformance class. APDS inventories devices
(`SupplementalEquipment`) and marks access-controlled places, but defines no
actuation. This Part adds it, referencing APDS entities throughout.

## 6.1 Commands

`POST /v1/commands` (scope `apx.control:execute`):

- **`Idempotency-Key` header REQUIRED** — same key + same body returns the
  command as it currently stands (`200`, Part 4 §4.2a), not a snapshot of
  the first response; same key + different body is
  `409 idempotency-conflict`.
- A request that names an unknown `commandType`, omits `target`, or
  carries a parameter that fails its definition below is refused with
  `400 invalid-request` (Part 12 §12.4).
- `commandType` values are OPEN (registry `apx-command-types`): `vendGate`,
  `holdGateOpen`, `closeLane`, `lostTicket`, `pushRate`, `applyValidation`,
  `setDeviceState`, `displayMessage`, `restartDevice`; the Part 17
  passback and courtesy entries (registry v2); `pushNegotiatedRate` and
  `matchTicket` (registry v3, §6.6–6.7); the Part 23 charger commands
  (registry v5); and, from registry v6, `ejectTicket` (return the ticket
  or card the machine holds), `cancelTransaction` (abandon the
  transaction in progress at the device and return its media) and
  `reprintReceipt` (print the last receipt at the device), all targeting
  a device. They are the three actions agents take on a stuck machine
  that no earlier command covered; a device that cannot perform one
  reports it `failed` in `statusHistory`. Re-sending a receipt to a
  customer, as opposed to printing it at the device, is a Part 13 §13.1a
  domain operation.
- Normative parameters per type: `lostTicket.method` (string, operator code
  list); `pushRate.rateTable` (VersionedReference to RateTable);
  `applyValidation.ticket` + `applyValidation.provider` (Reference);
  `displayMessage.message` (MultilingualString), and optionally
  `displayMessage.template` (a Reference or implementer code),
  `displayMessage.data` (the template's fields), `holdSeconds`, and
  `wordingVersion` for per-vehicle content rendered from a template;
  `setDeviceState.state`
  (apx-device-states value); `pushNegotiatedRate.rateTable`
  (VersionedReference to a RateTable flagged negotiable, §6.6) or
  `pushNegotiatedRate.amount` (AmountInCurrency, the negotiatedAmounts
  feature, §6.6), exactly one of the two;
  `matchTicket.session` (Reference to an open Session) and
  `matchTicket.evidence` (Reference, §6.7); `resetPassback.credential`,
  `forceIn.credential`, and `forceOut.credential` (Reference to a
  Credential); `courtesyExit.holder` (Reference to the RightHolder the
  courtesy is counted against). Part 17 §17.4 defines when those four are
  used; their parameters are the ones named here.
- `agent` / `agentType` (optional on every command; REQUIRED on
  `pushNegotiatedRate` and `matchTicket`) name the human or AI principal
  who initiated the command — distinct from `requestedBy` (the
  organisation) and `approval.approvedBy` (the approver). A command that
  requires `agent` and lacks it is refused with `400 agent-required`.
- `target` is a Reference to a SupplementalEquipment (device) or a
  HierarchyElement (lane/place). Legacy "location id + lane number"
  addressing used by existing PARCS integrations maps to the Place UUID +
  VehicularAccess UUID.
- Commands are **perishable**: a command whose `expiryTime` passes before
  dispatch transitions to `expired` and MUST NOT fire (a gate vend requested
  10 minutes ago must not open the gate now).
- The response is `202` with the Command in state `received`/`accepted` —
  richer than a bare success/failure boolean because execution is
  asynchronous and audited. Refusals the server can decide from its own
  state at POST time (an unknown provider, a missing lost-ticket fee, a
  table not flagged negotiable, a session that is not open) are
  synchronous `4xx` responses and nothing is dispatched; outcomes only the
  device can report arrive as `failed` in `statusHistory`.
- **Hold-open release (normative).** A `holdGateOpen` stays `executing`
  while the gate is held. It is released by its own `expiryTime` (the
  instant the hold ends, as well as the dispatch deadline), by a later
  `closeLane` at the same lane, or by a `setDeviceState` on the held gate
  device; on release it transitions to `succeeded` with the releasing
  cause in `detail`. A hold is never cancelled once dispatched
  (`409 command-not-cancellable`, §6.1); a hold with no `expiryTime` holds
  until one of the two releasing commands.
- `confirmationLevel` distinguishes how far success is physically
  confirmed: `accepted` (command taken), `deviceAcknowledged` (device
  acked), `physicallyConfirmed` (outcome verified, e.g. gate-state
  sensor). Consumers MUST NOT report an outcome stronger than this level —
  an agent may say "the gate is open" only at `physicallyConfirmed`
  (Part 17 §17.4).

**Lifecycle:** `received → accepted | rejected → dispatched → executing →
succeeded | failed | expired | cancelled`. Every transition appends to the
immutable `statusHistory[]` (state, time, actor, detail) — every control
transaction is tracked end to end. Transitions
publish `apx.control.command.status.v1`.

- `GET /v1/commands/{id}` — poll state (scope `apx.control:read`).
- `GET /v1/commands` — the audit query (§6.1b).
- `POST /v1/commands/{id}/cancel` — allowed until `dispatched`;
  afterwards `409 command-not-cancellable`.

**Authorization:** scope `apx.control:execute` AND the token's `apx_places`
grant must cover the target (else `403 insufficient-grant`).

**Lost-ticket fee semantics (normative):** the lost-ticket fee is **part of
the rate deck** — a flat RateLine identified by `description:
"lostTicketFee"` in the place's applicable RateTable, queryable like any
rate via the native `/rates` lookup and updated like any rate (including
via `pushRate`). A successful `lostTicket` command issues a new lost ticket
AT the target lane whose `amountDue` is that fee (in the rate line
collection's currency). A rate deck with no lostTicketFee line makes the
command fail rather than guess: the deck is server-side state, so the
server MUST refuse the `POST` synchronously with
`422 lost-ticket-fee-undefined` and MUST NOT create or dispatch a command.
The command's `result` (§6.1a) names the issued ticket, its session, and
the fee; the lane inquiry (§6.2) then shows it as the current ticket, and
the normal flow applies: take a payment (Part 13), apply a validation
(§6.3), or vend (§6.1). The fee is never silently waived — reducing it is
an explicit validation or payment event on the audit record.

### 6.1a Command result

A command that reaches `succeeded` carries a `result` object
(`CommandResult`) naming what it produced, so a console can continue
without parsing `statusHistory[].detail` or making a second call. The
members are normative per command type:

| commandType | `result` members |
|---|---|
| `lostTicket` | `ticketNumber`, `session` (Reference), `amountDue` (the fee) |
| `matchTicket` | `session` (the matched Session), `amountDue` (priced from its true entry) |
| `pushNegotiatedRate` | `rateTable` (VersionedReference, the version applied), `amountDue` |
| `displayMessage` | `display` — what the device confirmed it showed: `shownTime`, `contentHash`, `wordingVersion`, an access-controlled `screenshot` link, and `acknowledgedBy` (`player`, `operator`, `none`); written when the device confirms, and the proof of display for a refused-service sign (Part 24 §24.4). OPTIONAL: present where the display reports it. The same shape is the Part 26 §26.9 `DisplayRecord`, which a sign driven without a command records, and which names the command when one drove it |

Other command types leave `result` absent. `result` is server-assigned and
absent before `succeeded`; it restates, and never contradicts, what the
lane inquiry shows.

### 6.1b Listing commands (the audit query)

`GET /v1/commands` (scope `apx.control:read`) returns the commands the
caller may see, newest first, in the APDS `{meta, data}` envelope, each with
its full `statusHistory`. Filters, all optional and combined with AND:
`target` (the device, lane, or place id), `place` (comma-separated,
subtree-inclusive), `commandType`, `status`, `agent`, and `since` / `until`
(the instant the command was received). Only commands whose target lies
inside the token's `apx_places` grant are returned; naming a `place`
outside it is `403 insufficient-grant` (Part 9 §9.3). This is the route for
"what was vended at lane 2 between 18:00 and 19:00, and by whom" — the same
records `apx.control.command.status.v1` publishes, readable after the fact.

## 6.2 Lane inquiry (screen-pop)

`GET /v1/lanes/{id}/current` (scope `apx.control:read`) returns
`LaneStatus`: the ticket currently in the machine (issued time, amount due,
applied validations, paid-in-full), the latest LPR read (plate, confidence,
screenshot link — an APDS Observation), and monthly-credential context
(access granted/denial reason, last activity, last 10 events).

## 6.3 Validations

- `GET /v1/validations/providers?place={uuid}` — the venues allowed to
  validate tickets at that place (`ValidationProvider[]`).
- Applying a validation = `applyValidation` command with
  `parameters.ticket` and `parameters.provider`. On success the lane's
  `currentTicket.validations[]` gains an entry and `amountDue` is
  recalculated.
- **Enforcement (normative):** servers MUST reject an `applyValidation`
  whose `provider` is not on the place's provider list with
  `422 validation-provider-unknown` — the list is authoritative, not
  advisory.
- **Per-ticket price adjustment (design rule):** validations and discounts
  ARE the sanctioned mechanism for adjusting one ticket's amount due —
  auditable, provider-bound, and policy-gated. Outside them, APX defines
  exactly one single-ticket price change: a negotiated rate (§6.6), either
  a table selected from the deck or, where the host offers the
  `negotiatedAmounts` feature, an amount the agent states. Both are
  commands with a REQUIRED `agent`. The rate deck stays authoritative
  (corrected, when wrong, via `pushRate`), and every change on a specific
  ticket is attributable on the audit record to a named validation
  provider, discount, deck table, or agent with a reason. Revenue integrity by construction, not by
  operator discipline.
- **APDS alignment (informative):** APDS 4.1 records that a validation
  *happened* (`PaymentTypeEnum: validation`, `Segment.validationType/
  validationId`, `RateTable.validation`) but defines no provider registry —
  that registry is exactly what this section adds. Implementations SHOULD
  record an applied validation in APDS-native terms: a Payment of type
  `validation` and the segment's `validationId`, so downstream APDS
  consumers see it without speaking APX.
- **Program management (Part 20):** the merchant side — enrolment,
  instrument stock, the redemption ledger, and billing — is the optional
  class `apx-validations`. When claimed, the provider list above is
  derived from active `ValidationProgram`s (each row carries the additive
  `program` reference) and every `applyValidation` materializes a
  `ValidationRedemption` (Part 20 §20.1–20.3).

## 6.4 Device status

- `GET /v1/devices` / `GET /v1/devices/{id}` (scope
  `apx.control:read`) — `DeviceStatus` overlay keyed by Reference to the
  APDS SupplementalEquipment. APDS 4.1 has no general equipment status
  list: its `RefillPointStatusEnum` (Data Dictionary A.3.53) covers refill
  points only and is absent from `apds-api-4.1.yaml`. States come from
  registry `apx-device-states` (`available, occupied, inoperative,
  outOfService, fault, unknown`), which follows that enumeration's style;
  `available`, `occupied`, `inoperative` and `unknown` are its own values.
- The list takes two optional filters: `place` (comma-separated
  HierarchyElement ids, subtree-inclusive, as the APDS `place` filter) and
  `deviceState` (one `apx-device-states` value, e.g. `fault`). A `place`
  outside the token's grant is `403 insufficient-grant`.
- State changes publish `apx.control.device.state.v1`. A transition to
  `fault` SHOULD auto-raise a `deviceFault` alert (Part 7).
- **What is wrong, and what is running out.** A `DeviceStatus` in
  `fault` SHOULD carry `fault`: the device's own `code`, its
  `displayText` as the device or vendor shows it, and `since`. A device
  that holds consumables carries `consumables[]`: `kind` (`tickets`,
  `receiptPaper`, `cash`, `coins`) and `level` (`ok`, `low`, `empty`,
  `unknown`); a level reaching `low` or `empty` SHOULD raise a
  `consumableLow` alert (Part 7 §7.7). `maintenanceMode` marks a device
  under test or service, so its events and the calls about it can be
  kept out of service figures. "When was it last seen" is the existing
  `lastCommunication`; no second member is added for it.
- The same object may decorate APDS payloads as
  `apds-ext:apx:devicestatus@1.0` (Level B). What the device *is* (its
  role, make, model, versions, where it stands) is the Part 26 §26.1
  equipment detail on the SupplementalEquipment, never repeated here.

## 6.5 Conformance

`apx-control` requires: §6.1 command plane with vendGate, lostTicket,
pushRate, applyValidation; §6.2 lane inquiry; §6.3 provider query; §6.4
device status; the grant rule; and command/device event publication.

Negotiated rates (§6.6) and ticket matching (§6.7) are **optional
features** of the class, named `negotiatedRates` and `ticketMatching`.
Negotiated amounts (§6.6) are a further optional feature,
`negotiatedAmounts`, offered only together with `negotiatedRates`. An
implementation that offers either MUST list it in the `features` member of
`/.well-known/apx-configuration` (Part 16 §16.1) and of every discovery
document whose client holds an `apx.control` scope (Part 16 §16.2), and MUST
meet the corresponding section in full (Annex A rows APX-CTL-09 through
12, and APX-CTL-16 for negotiated amounts). `commandTypes` in a discovery document remains the permission list:
`pushNegotiatedRate` or `matchTicket` appears there only for a client that
may execute it, and never for a host that does not offer the feature.

## 6.6 Negotiated rates (optional feature)

APDS owns the rate deck: `RateTable`, its collections and lines, served
and mirrored through the native `/rates` route (Part 5 §5.2). APX adds no
rate model. What the deck cannot say is which of its tables an agent may
offer a driver on the phone, and who offered it. This section adds
exactly that.

- **The flag.** A RateTable that MAY be offered as a negotiated rate
  carries the Level B decoration `apds-ext:apx:ratepolicy@1.0`
  (`RatePolicy`: `negotiable`, optional `displayName`, `note`) in its
  `extensions` container (Part 4 §4.3). Because the decoration is inside
  the table, every consumer that syncs `/rates` receives it with the
  deck. The set of negotiable tables at a place is therefore the set of
  tables that apply there and carry `negotiable: true` — no second
  registry, no second query.
- **The command.** `pushNegotiatedRate` with `parameters.rateTable`
  (VersionedReference) and a lane target applies that table to the
  **current ticket at the lane only**. It never changes the lane's deck;
  the next car prices normally. (`pushRate` remains the deck-level
  correction, with its supervisor-grade consequences.) Servers MUST
  refuse a table that is not flagged negotiable for the target's place,
  or that does not apply there, with `422 rate-not-negotiable`, and MUST
  refuse the command at a lane with no transaction in progress with
  `409 lane-no-current-transaction`.
- **Who chose it.** `agent` is REQUIRED (`400 agent-required` otherwise),
  `agentType` SHOULD be given, and `reason` SHOULD be. The lane inquiry
  then shows `currentTicket.negotiatedRate` — the table at the exact
  version applied, the command, and the agent — so a later context at
  the lane, a dispute, or a revenue report sees who chose what, from the
  same audit record every other command has.
- **Any amount (the `negotiatedAmounts` feature).** Instead of a table,
  `parameters.amount` (AmountInCurrency) states the amount the current
  ticket owes. It replaces the ticket's `amountDue`; it is not a
  discount. APX sets no floor and no ceiling: zero, less than the deck
  price, and more than it are all valid. Exactly one of `rateTable` and
  `amount` MUST be given. With `amount`, `reason` is REQUIRED as
  well as `agent`, because there is no deck table to explain the
  figure. Servers MUST refuse:
  - both members, neither, a negative `currencyValue`, a missing
    `reason`, or a `currencyType` other than the ticket's, with
    `400 invalid-request`;
  - `amount` on a host that does not offer `negotiatedAmounts`, with
    `422 rate-not-negotiable`;
  - an empty lane with `409 lane-no-current-transaction`, as for a table.

  The lane inquiry then shows `currentTicket.negotiatedAmount` with the
  `amount`, `reason`, `agent`, and the command, and the command's
  `result.amountDue` restates it (`result.rateTable` is absent). A stated
  amount is never reported through `currentTicket.negotiatedRate`, whose
  `rateTable` stays required: a ticket shows at most one of the two
  members, the one the latest succeeded `pushNegotiatedRate` for it set.
  The next car prices from the deck as usual.
- **Attribution, not restriction (design rule).** Every negotiated rate
  names who set it and, for a stated amount, why. That keeps §6.3's rule
  intact: every change on a ticket is attributable on the audit record.
  A host that wants agents limited to the deck simply does not offer
  `negotiatedAmounts`. One that wants approval above or below some
  figure gates the command with Part 17 §17.3's policy layer
  (`requiresApproval`), which APX already defines.
- **No selection rules, deliberately.** APX does not say when a negotiable
  table may be chosen, or what amount may be stated: no length-of-stay bands, no time windows, no caller
  categories. Such rules multiply fast and differ by operator; the system
  presenting the deck to an agent or a third party applies its own
  guardrails, and Part 17 §17.3's policy layer can gate the action per
  context. The server enforces only the flag and the rules above.
- **APDS alignment (informative).** The negotiated table is an ordinary
  APDS RateTable, so the resulting Session segment references it exactly
  as any other rate; a stock APDS consumer sees a session priced by a
  named table, which is the truth. A stated amount has no table: the
  stock consumer sees the Session's amount, and the command, found by
  the Session's lane and time, says who set it and why.

## 6.7 Ticket matching (optional feature)

A driver at the exit lane without a ticket is the garage's most common
revenue leak: the choice today is a lost-ticket fee the driver resents or
a courtesy vend the operator absorbs. The entry usually *was* recorded —
by the camera, by a credential, by a reservation — and this section lets
the agent find it and bind the exit to it, so the open ticket closes at
the real fare.

- **Candidates.** When the lane has no ticket in the machine, or when the
  caller passes `plate`, `phone`, or `credential` on
  `GET /v1/lanes/{id}/current`, the server returns `matchCandidates[]`:
  open sessions at the place that plausibly belong to the vehicle in the
  lane, best first. Sources are the lane's own LPR read against entry
  reads (`plateRead`), a credential presented at the lane (`credential`),
  the account registry by plate or phone (`account`, Part 13 §13.1), and
  a checked-in reservation carrying the plate (`reservation`). Each
  candidate names the session, its entry time and lane, how it was
  matched, the evidence record, a confidence, and SHOULD carry
  `amountDueIfMatched` so the agent can quote the fare before matching.
  Candidates are **advisory**: the server MUST NOT bind one without a
  command.
- **Phone is an account key, not a call identifier.** The `phone`
  parameter resolves a permit holder through the account registry exactly
  as `GET /v1/accounts?phone=` does. It is not a telephony identifier and
  Part 17 §17.1's layering rule is unchanged: the call platform still
  maps call → lane before calling APX.
- **The command.** `matchTicket` with `parameters.session` (Reference to
  the chosen open Session), `parameters.evidence` (Reference, SHOULD be
  given — the Observation, Credential, RightHolder, or AssignedRight that
  justified the match), and `agent` REQUIRED (`400 agent-required`) binds
  the current transaction at the target lane to that session. The exit
  then prices from the session's true entry time under the rate that
  applied to it; `lostTicket` is not involved. Servers MUST refuse a
  session that is closed, already bound to an exit, or at another place
  with `422 session-not-open`, and an empty lane with
  `409 lane-no-current-transaction`.
- **Materialization (normative).** On vend the matched Session MUST close
  in APDS terms — the exit segment, end time, and the exit Observation
  where one exists — and MUST publish `SessionUpdated`, so a plain APDS
  client reading `/sessions/{id}` sees one complete stay. The lane's
  `currentTicket` carries `matchedCommand` until the vehicle leaves. APX
  keeps no parallel matching store: the match *is* the session's exit.
- **Fallback stays explicit.** When no candidate is right, `lostTicket`
  applies as §6.1 defines it. Both paths close the open ticket and both
  are attributable; the difference is the fare, and the audit shows which
  path was taken and by whom.
- **No matching rules, deliberately.** APX does not define how confident
  a candidate must be, whether the driver must read the plate back, or
  what a supervisor must approve. As with §6.6 those are operator policy,
  presented through Part 17 §17.3's allowed actions when a context is in
  play, and applied by the presenting system otherwise.

## 6.8 Charging commands (Part 23)

`startCharging`, `stopCharging`, and `unlockConnector` (registry
`apx-command-types` v4) target an APDS `ElectricChargingEquipment`.
Parameters: `connectorIndex` (1-based position in the static
`connectors[]`) and `chargingSession` (Reference), or for
`startCharging` an `authorization` object (the
`ChargingSession.authorization` shape) in place of the session. The
operator's charging-network bridge translates them to the charger
protocol; `confirmationLevel` applies as in §6.1. Part 23 §23.9 defines
their semantics; they are offered only by an implementation that claims
`apx-charging`, and `commandTypes` in a discovery document lists them
only for a client that may execute them.

---

<a id="07-alerts"></a>

# APX Part 7 — Alerts

The `apx-alerts` conformance class. APDS has no alert, event, or
notification classes — this domain is net-new, built on APX common
conventions and delivered over the Part 8 fabric.

## 7.1 The Alert resource

See the `Alert` schema. Key rules:

- `alertType` is OPEN — values come from the `apx-alert-types` registry or
  an implementer's own UserDefinedCodeList (Part 11). `severity` is CLOSED
  (`info < warning < minor < major < critical`).
- `occurrenceTime` (when it happened) vs `detectionTime` (when the system
  learned of it) — both RFC 3339; detectionTime is required.
- `source.place` carries a HierarchyElement UUID; filtering by place is
  **subtree-inclusive** (an alert on a lane matches its parent Place).
- `statusHistory[]` is the immutable audit trail; every transition appends.
- `id` is server-assigned when the raise carries none. A client MAY supply
  its own UUID on raise (Part 4 §4.1; the schema's `readOnly` marks the
  server's authority over the value, not a prohibition on this case); an id
  that already exists is `409 id-collision`, distinct from
  `idempotency-conflict`.
- **Alerts with no place binding (normative).** An alert whose subject has
  no place (a `webhookDeliveryFailed` alert about a Subscription) carries
  no `source.place` and is bound to the organisation that owns its
  subject, or that raised it (`apx_org`). It is visible to every token of
  that organisation holding `apx.alerts:read`, whatever its `apx_places`,
  is excluded from any `place`-filtered list, and may be raised by a token
  of that organisation with `apx.alerts:write` (Part 9 §9.3a rule 3). Tokens
  of other organisations never see it.
- **Evidence other than the subject.** When what an operator acts on and
  the evidence for the alert are different entities, `relatedEntity` is
  the entity acted on (for example a Session) and the evidence rides the
  alert's `extensions` under `apds-ext:apx:alert-evidence@1.0` as
  `{ "observations": [Reference, …] }` (for example the LPR reads behind
  it).
- **The call behind the alert.** An alert raised from a customer contact
  (a receipt or refund request, a citation inquiry, reported damage, a
  callback promised, §7.7) carries `supportInteraction`, a Reference to
  the Part 17 §17.6 record of that call, so the site case and the call
  record point at each other and the §17.7 correlation chain reaches the
  alert.

## 7.2 Endpoints

- `POST /v1/alerts` — raise. **`Idempotency-Key` header REQUIRED**
  (device retry storms must not duplicate alerts): replaying the same key
  with the same body returns the alert as it currently stands (`200`, Part 4
  §4.2a) — a replay after an acknowledgement shows `acknowledged`, since the
  point of the replay is deduplication; the same key with a different body
  is `409 idempotency-conflict`. A body that fails the schema is
  `400 invalid-request`. Scope `apx.alerts:write`.
- `GET /v1/alerts` — filters: `status`, `severityFloor`, `type`,
  `place` (subtree), `device` (matches `source.device.id`),
  `relatedEntity` (matches `relatedEntity.id`), `since` (detectionTime ≥).
  Scope `apx.alerts:read`.
- `GET /v1/alerts/{id}`.
- `POST /v1/alerts/{id}/acknowledge` and `/resolve` — lifecycle
  transitions (scope `apx.alerts:write`). Illegal transitions (e.g.
  resolving a resolved alert) are `409 alert-transition-illegal`. Both take
  an optional `AlertTransition` body: `detail` is copied into the history
  entry the transition appends, and `agent` (the human or AI principal
  acting through the client, as `Command.agent`) is recorded as that
  entry's `actor` in place of the token's principal. A request with no body
  behaves exactly as before.

## 7.3 Lifecycle

`raised → acknowledged → resolved` (normal path); `raised → resolved`
(direct) is permitted; `expired` is a server-side terminal state for alerts
with an `expiryTime`. A client MAY set `expiryTime` on raise; when it does
not, the server MAY apply a default by policy per `alertType` and returns
the value applied, so every open alert shows when it will lapse. An alert
still `raised` or `acknowledged` at its `expiryTime` transitions to
`expired` and publishes `apx.alert.status.v1`; an alert without one never
expires. Terminal states (`resolved`, `expired`) never transition again.

## 7.4 Eventing

- `apx.alert.raised.v1` on creation; `apx.alert.status.v1` on every
  transition. Event `data` is the Alert; `subject` references it.
- Subscription `filters.severityFloor` applies to alert topics (Part 8).
- Self-referential rule: a `webhookDeliveryFailed` alert MUST NOT itself
  generate webhook deliveries to the failed subscription.

## 7.5 Charging-bay alert types (Part 23)

Registered in `apx-alert-types` v4: `evBayNotCharging` (a vehicle has occupied a
charging bay without connecting beyond the place's grace — the EV
counterpart of the seeded `iceInEvSpace`) and `evBayBlocked` (the bay is
obstructed by something other than a parked vehicle). Both are derived
from the Part 23 §23.3 bay-presence fusion and carry the camera
Observation as evidence.

## 7.6 List-arrival alert types (Part 24)

Registered in `apx-alert-types` v5:

- `watchListArrival` — a credential on a `watch` plate list was read at a
  place where the list has `alertOnArrival`;
- `scofflawArrival` — the same for a `scofflaw` list;
- `refusedServiceArrival` — a listed credential arrived at a lane where
  the list has `refuseService`, and service was refused.

Each carries the `PlateListEntry` as `relatedEntity` and the read as
evidence (`apds-ext:apx:alert-evidence@1.0`, §7.1); a refused-service
alert SHOULD also reference the `displayMessage` command whose
`result.display` proves what the lane showed (Part 6 §6.1a). Subscribers
that want only these use `filters.alertTypes` (Part 8 §8.5a).

## 7.7 Service-request and consumable alert types (Parts 6, 17)

Registered in `apx-alert-types` v6:

- `consumableLow` — a device reports a consumable (`tickets`,
  `receiptPaper`, `cash`, `coins`) at `low` or `empty` (Part 6 §6.4);
  `source.device` is the device and `description` names the consumable;
- `receiptRequested` — a customer asked for a receipt that the agent
  could not send during the call (Part 13 §13.1a);
- `refundRequested` — a customer asked for a refund the agent could not
  grant during the call; the refund, when made, is the Part 13 §13.1a
  action, and its `reasonCode` comes from `apx-refund-reasons`;
- `citationInquiry` — a customer asked about a citation or notice (Part
  19) that the call center does not handle;
- `damageReported` — a customer reported damage to, or a break-in of, a
  vehicle on site;
- `customerCallbackRequested` — the customer is owed a call back.

Each is a site case opened from a call: `relatedEntity` is what the site
acts on (the payment, the violation, the session) and `supportInteraction`
(§7.1) is the call it came from. They are the alerts a call center hands
to the operator, so the operator's queue and the call record agree on
what is still open.

---

<a id="08-events"></a>

# APX Part 8 — Delivery Fabric (webhooks + SSE)

The `apx-events` (webhooks) and `apx-events-sse` (SSE) conformance classes.
APDS 4.1 defines `/webhooks` (subscribe/unsubscribe) but leaves the delivery
contract per-project. APX completes it — **as a superset**: a stock APDS
`EventSubscription {endpoint, topics[]}` works unmodified.

## 8.1 Subscriptions (at APDS's own `/webhooks` route)

- `POST /webhooks` — create. Body: `ApxEventSubscription`. Stock APDS bodies
  are valid (transport defaults to `webhook`). Requires
  `apx.subscriptions:manage`.
  **Response negotiation (normative):** the stock APDS 4.1 response
  contract is preserved. A request WITHOUT `Prefer: return=representation`
  (RFC 7240) MUST receive APDS's documented `200`/`202` with
  `ResponseStatus` — a plain APDS client observes pure APDS behavior. An
  APX client sends `Prefer: return=representation` and MUST receive `201`
  with the full subscription including the signing `secret` when
  `transport` is `webhook`, returned exactly once, in that response only.
  An SSE subscription has nothing to sign and gets no secret. The stock
  `200`/`202` `ResponseStatus` MUST carry the new subscription's id as
  the single entry of `ids[]`, so a stock client can later revoke it.
  **Idempotency.** `POST /webhooks` takes the optional `Idempotency-Key`
  (Part 4 §4.2a). A replay with the same body returns the same status
  with the subscription as it now stands — never the secret a second
  time (rotate it if the first response was lost); the same key with a
  different body is 409 `idempotency-conflict`.
  **Refusals at creation and update.** A topic the credential could not
  read synchronously MUST be refused with 403 `insufficient-scope`,
  naming the topic and the missing scope in `detail` (Part 9 §9.6(4)); the
  read scope of a topic is the scope of the read route of its data
  schema (e.g. `apx.lpr:read` for `apx.data.observation.created.v1` and
  `.updated.v1`). A
  `filters.places` entry outside the token's `apx_places` grant MUST be
  refused with 403 `insufficient-grant`, and a token with no `apx_places`
  claim cannot create a place-filtered subscription. A webhook-transport
  subscription without `endpoint`, an empty `topics[]`, a secret under 32
  bytes of entropy, or a malformed `extensions` key is 400
  `invalid-request` with an `errors[]` pointer; an unregistered topic is
  400 `unknown-topic`.
- `GET /webhooks/{id}` — read one subscription (APX additive operation).
  Secrets are never returned.
- `PATCH /webhooks/{id}` — update (APX additive operation). The body is a
  JSON Merge Patch (RFC 7396) over the subscription
  (`ApxEventSubscriptionPatch`): members present replace the stored
  value, absent members are unchanged, so `{"status": "active"}` alone
  resumes a subscription. The precondition is `If-Match` or the body
  `version`; a stale one is 409 `version-conflict` (Part 4 §4.2a).
- `DELETE /webhooks/{id}` — revoke (native APDS operation).
- `GET /webhooks` — list (APX additive operation).
- `GET /webhooks/{id}/deliveries` — the delivery ledger (§8.3), filterable
  by `status`, `eventId`, and `since`.
- `topics[]` mixes APDS `EventTypeEnum` values and APX topics freely.

**Subscription states.** `active` delivers. `paused` (client-set) holds
matching events for up to 24 hours and delivers them in order on resume;
an event older than 24 hours when the subscription resumes is dropped,
and a held event has no ledger record until its first attempt. `failed`
is server-assigned when the retry schedule is exhausted (§8.3); a client
PATCH setting `status: failed` is 400 `invalid-request`. While `failed`,
the subscription MUST accept a PATCH only when the body sets `status:
active` (which also resets the retry state); every other PATCH, and the
SSE stream, MUST answer 410 `subscription-failed`. `GET` of the
subscription, its ledger, and `DELETE` remain available in every state.

**Key rotation (Part 9 §9.4).** A PATCH carrying `secret` or `secretRef`
starts a rotation: the new key joins `activeKeyIds` and signs every
delivery from then on, and deliveries carry `APX-Key-Id`. The client MAY
name the new key with `keyId` (else the server assigns one). The overlap
ends when the client PATCHes `retireKeyIds` naming the old key, or
automatically 24 hours after the rotation. Retiring the last active key
is 400 `invalid-request`.

## 8.2 The envelope

Every delivery carries exactly one `EventEnvelope` (Part 4 conventions;
CloudEvents-aligned). For APDS EventTypeEnum topics, `data` is the APDS
`EventData` shape and `subject` references the changed entity.

## 8.3 Webhook delivery (normative)

- HTTP POST of the envelope JSON to the subscription endpoint.
- Headers: `APX-Signature: v1=<hex HMAC-SHA256(secret, timestamp + "." + body)>`,
  `APX-Timestamp` (RFC 3339), `APX-Delivery-Id` (UUID, new per attempt),
  `APX-Key-Id` (REQUIRED during secret-rotation overlap windows, else
  optional — Part 9 §9.4), `Content-Type: application/json`.
- **Every attempt is signed independently.** `APX-Timestamp` is the time
  of that attempt and `APX-Signature` is computed over it and the
  unchanged body. The body — and with it the envelope `id` and `time` —
  is byte-identical across attempts. (A retry that reused the first
  attempt's timestamp would fall outside the receiver's window after five
  minutes, and the subscription would fail for a reason that has nothing
  to do with the endpoint.)
- Receivers MUST verify the signature and reject deliveries outside a
  ±5 minute timestamp window.
- Success = any 2xx response. Anything else triggers the retry schedule:
  **0s, 30s, 2m, 10m, 1h, then hourly up to 24h total.** The values are
  *delays between consecutive attempts*: the first attempt is sent at
  once, the second 30 s after the first fails, the third 2 min after the
  second, the fourth 10 min after that, the fifth 1 h after that, and
  then one attempt an hour for as long as the next attempt would fall
  within 24 hours of the first — 27 attempts, the last 23 h 12 min 30 s
  after the first. When the last attempt fails the subscription
  transitions to `failed`, an `apx.subscription.failed.v1` event (data:
  `SubscriptionFailure`) is published to the organisation's other
  subscriptions that list the topic — never to the failed one — and
  (where `apx-alerts` is implemented) a `webhookDeliveryFailed` alert is
  raised.
- The event `id` is stable across retries (receiver-side dedup key);
  `APX-Delivery-Id` is unique per attempt.
- **The ledger records attempts.** `GET /webhooks/{id}/deliveries` holds
  one `DeliveryRecord` per (event, subscription): the attempt count, the
  status, the latest attempt's `deliveryId`/`lastCode`/`time`, and
  `attemptHistory[]` — every attempt's `APX-Delivery-Id`, time (which is
  its `APX-Timestamp`), and HTTP code, so a receiver and a publisher
  disputing a retry share one record.
- Ordering is best-effort per subscription; receivers MUST NOT assume
  cross-topic ordering. The data-profile cursor (Part 5) is the
  exactly-once path.

## 8.4 SSE (optional class `apx-events-sse`)

- `GET /v1/events/stream?subscription={id}` with `Accept:
  text/event-stream`, authenticated like any APX call. The subscription
  MUST have `transport: sse`.
- **Frame grammar (normative).** One event per frame: an `id:` line
  carrying a monotonically increasing per-subscription sequence (a
  decimal integer), exactly one `data:` line carrying the EventEnvelope
  JSON on a single line, and a blank line. Servers MAY send `retry:`
  lines and comment lines (`: keep-alive`); they carry no events.
- Reconnection with `Last-Event-ID` resumes strictly after that sequence;
  servers MUST buffer at least 1000 events or 15 minutes per subscription.
  When `Last-Event-ID` is older than the oldest buffered frame the server
  MUST answer 410 `stream-position-expired` rather than resume with a
  silent gap; the client re-syncs through the Part 5 change feed and
  reconnects without `Last-Event-ID`.
- Checks run in this order: the subscription exists and is visible (else
  404 `target-not-found`); its `transport` is `sse` (else 404
  `target-not-found` — there is no stream for it); it is not `failed`
  (else 410 `subscription-failed`); then the `Last-Event-ID` position.
- Exists for consumers that cannot expose an inbound endpoint (NAT'd PARCS,
  kiosks).

## 8.5 Place binding and filtering (normative)

Subscription `filters.places` and per-site analytics both require every
event to be attributable to a location. The binding rule, per topic:

1. An event is **bound to a place** by, in order of precedence: (a) a
   `place` field in `data` (e.g. OccupancySnapshot, PaymentRecord);
   (b) a `source.place` field in `data` (Alerts); (c) `subject` when it
   references a HierarchyElement; (d) the place that inventories the
   subject device (`SupplementalEquipment`) or contains the subject
   entity (e.g. a Session's `hierarchyElement`).
2. Publishers MUST populate at least one of these bindings on every event
   whose topic concerns a physical location. In particular:
   `apx.accounts.payment.recorded.v1` data carries the (required)
   `PaymentRecord.place`; `apx.data.observation.created.v1` and
   `apx.data.observation.updated.v1` publishers MUST populate the
   `ObservationElement`'s `elementIds` with the observing lane or place.
3. `filters.places` matches an event when its bound place is inside any
   granted subtree. An event with NO resolvable place binding matches only
   subscriptions without a `places` filter, and MUST NOT be delivered to a
   subscriber whose `apx_places` grant would not include it — when in
   doubt, drop rather than leak (Part 9 §9.3).
4. A subscription whose `filters.places` names an element outside the
   token's `apx_places` grant is refused at creation or update with 403
   `insufficient-grant` (§8.1); the filter can narrow the grant, never
   widen it.

## 8.5a Alert-type filter (normative)

`filters.alertTypes[]` applies to the alert topics (`apx.alert.raised.v1`,
`apx.alert.status.v1`): only alerts whose `alertType` is listed are
delivered. It combines with `severityFloor` (both must hold) and with
`places`. Values are not validated against `apx-alert-types`, because
`alertType` is open (Part 7 §7.1); a value no alert carries simply
matches nothing. On other topics the filter is ignored.

## 8.6 Publishing obligations

Implementations claiming `apx-events` MUST publish the APDS EventTypeEnum
topics for every entity class they serve writes for, and the APX topics of
every other conformance class they claim (registry `apx-topics`).

## 8.7 Topic data schemas (normative)

Every topic names the schema its `data` validates against; publishers MAY
set the envelope's `dataschema` to it. Most APX topics carry the
resource of their own domain (registry `apx-topics`). The ones whose
data lives elsewhere, and the APDS topics, are also declared as typed
entries of the OpenAPI `webhooks` object, so the bundle carries their
schemas:

| Topic | `data` | `subject` |
|---|---|---|
| APDS `EventTypeEnum` values (`SessionCreated`, …) | APDS `EventData` (the entity; for `*Deleted`, its last state) | the entity |
| `apx.subscription.failed.v1` | `SubscriptionFailure` (subscription Reference with `className` `ApxEventSubscription`, `endpoint`, `failedTime`, `firstFailedEventId`, `attempts`, `lastCode`) | the subscription |
| `apx.data.observation.created.v1` | APDS `ObservationElement` — one event per element, so an ingested `ObservationSet` yields one event per read | the element (`className` `ObservationElement`) |
| `apx.data.observation.updated.v1` | APDS `ObservationElement` as it stands after a change (e.g. an `accessEvent` revised through `PUT /v1/lpr/reads/{observation}/access-event`, Part 13 §13.3a(4), or a plate review decision through `PUT /v1/lpr/reads/{observation}/plate`, §13.3a(4a)); consumers keep the highest `version` | the element (`className` `ObservationElement`) |
| `apx.reservation.noshow.v1` | `ReservationSummary` | the AssignedRight |
| `apx.data.occupancy.v1` | `OccupancySnapshot` | the HierarchyElement |

---

<a id="09-security"></a>

# APX Part 9 — Security Profile

APDS leaves security to the Distributing Party. APX narrows that to an
interoperable profile without prescribing an identity provider.

## 9.1 Authentication

- Implementations MUST support **OAuth 2.0 client credentials** (RFC 6749).
- The token endpoint is advertised in `/.well-known/apx-configuration`.
- Access tokens SHOULD be JWTs; introspection-based opaque tokens MAY be
  used if the resource server enforces the same claims.
- All APX traffic MUST use HTTPS. The `apx-mtls` conformance class adds
  mutual TLS.

## 9.2 Scopes

Scopes follow `apx.<domain>:<verb>` (full list in the OpenAPI
`securitySchemes`). A request whose token lacks the operation's scope MUST
receive HTTP 403 with problem type
`https://apx-standard.org/problems/insufficient-scope`.

## 9.3 Grants (normative claims)

Two token claims bound a client's world:

- `apx_org` — object `{id, className}` (APDS Reference, className
  `Organisation`): the client's organisation. Implementations MUST attribute
  writes (`recordInfo.creator`, command `requestedBy` defaults) to it.
- `apx_places` — array of HierarchyElement id strings. A grant on an
  element includes its entire subtree (Campus → … → Space).
  **Fail-closed (normative):** a token WITHOUT the claim has NO place
  grant — every place-targeting request fails `insufficient-grant`. An
  all-places grant is EXPLICIT: the single wildcard entry `"*"`. (For a
  control plane that can open gates, misconfiguration must fail toward
  nothing, not everything.) A request targeting a place/device outside the
  grant MUST receive 403 with problem type
  `https://apx-standard.org/problems/insufficient-grant` — even when the
  scope check passes.

Discovery (`GET /v1/discovery`) MUST reflect scopes and grants exactly:
a client can call everything its discovery document lists, and nothing more.

## 9.3a Visibility outside the grant (normative)

Three rules settle what a caller learns about records it may not see:

1. **Place grant.** A request that names a place, lane, device, or a
   resource addressed by id whose owning place lies outside `apx_places`
   receives 403 `insufficient-grant` (§9.3). This is unchanged.
2. **Narrower ownership scopes.** Where a Part confines a credential to
   its own records inside a granted place (a merchant's own validation
   programs, a valet customer's own ticket), a record outside that scope
   is reported as 404 `target-not-found`, exactly as a record that does
   not exist. The answer never confirms another party's record.
3. **Lookups that name no place.** A list or lookup that names no place
   (by plate, phone, card digits, ticket) returns only records at granted
   places. Under an absent or empty grant it returns `200` with an empty
   `data` array, never 403: the request targeted nothing outside the grant,
   and a 403 would reveal that matching records exist. Resources that
   belong to no place (a subscription, a subscription-failure alert) are
   scoped to the caller's `apx_org` instead.

An APX endpoint of a conformance class the server does not claim is 404
`target-not-found`; an endpoint the server does implement but the token's
scopes do not cover is 403 `insufficient-scope` (§9.2). Discovery lists
neither.

## 9.4 Webhook authenticity

- Every webhook delivery MUST be signed: `APX-Signature: v1=<hex>` where
  `<hex>` = HMAC-SHA256(secret, `<APX-Timestamp>` + "." + raw body). The
  secret is used as raw UTF-8 key material and MUST contain at least 32
  bytes of entropy.
- `APX-Timestamp` is RFC 3339; receivers MUST reject deliveries older/newer
  than 5 minutes (replay window).
- Subscription secrets are server-generated (returned exactly once at
  creation) or supplied by reference via the subscription's `secretRef`
  (an identifier into an out-of-band exchange — never the secret value
  itself). Rotation uses a dual-key overlap window (both keys valid until
  the old one is retired); **during the overlap, every delivery MUST carry
  `APX-Key-Id` naming the signing key** so receivers verify against the
  right key instead of trying both. Outside a rotation window the header
  is OPTIONAL. The overlap ends when the subscriber retires the old key
  (`PATCH` with `retireKeyIds`) or 24 hours after the rotation, whichever
  comes first (Part 8 §8.1).

## 9.5 Token issuance

APX does not define a token issuer. Implementations bring their own IdP;
any OAuth2 client-credentials-capable issuer satisfies this Part.

## 9.6 Privacy and data protection

License-plate numbers, plate imagery, and payment records are personal
data in many jurisdictions (e.g. GDPR). APX surfaces that carry them —
Observations/LPR reads (Part 13 §13.3), the
`apx.data.observation.created.v1` stream, plate-keyed lookups, and
payment history — are subject to this clause:

1. **Minimization (normative).** Responses and event payloads MUST NOT
   carry more personal data than the requesting scope needs: full PANs are
   never carried (Part 0 §0.4); plate values appear only under `apx.lpr:*`,
   `apx.tolling:*`, `apx.violations:*`, `apx.accounts:*`, or
   `apx.charging:read`/`:manage` (Part 23) scopes;
   imagery is carried as links, never inline. Passage facts of a read
   (`accessEvent`, `movement`, `plateFace` and the others Part 13
   §13.3a(7) lists) are not personal data and are served under
   `apx.data:read`. `PUT /v1/sessions/{id}/plate` and
   `PUT /v1/lpr/reads/{observation}/plate` (scope `apx.data:write`) echo
   only the plate values the caller supplied and disclose no other
   plate-bearing data (Part 17 §17.5, Part 13 §13.3a(4a)); the read route
   returns the whole read only to a caller that also holds
   `apx.lpr:read`.
2. **Access-controlled imagery (normative).** `imageLink` and any other
   media URL MUST require the same authentication and place grant as the
   API call that produced it. Unauthenticated, long-lived image URLs do
   not conform.
3. **Retention (normative floor and ceiling).** The Part 5 change feed
   requires at least 7 days of history. Implementations MUST define and
   publish (in operator documentation) a retention period for plate reads,
   imagery, and truncated-card payment records, and MUST be able to purge
   them on that schedule. This standard does not set a single ceiling —
   jurisdictions differ — but indefinite retention of plate imagery does
   not conform.
4. **Purpose limitation (normative).** Subscriptions to plate-bearing
   topics require the corresponding read scope; a subscription MUST NOT
   deliver a topic the credential could not read synchronously.
   Implementations MUST refuse such a subscription at creation or update
   with 403 `insufficient-scope`, naming the topic and the missing scope
   in `detail` (Part 8 §8.1).
5. **Truncated-key lookups.** The 8-hour window on `ticketLast4`/
   `cardLast4` lookups (Part 13 §13.2) is a privacy control, not a
   convenience limit; implementations MUST NOT widen it by configuration.
6. **Credential digests (optional).** A server MAY publish, on an
   Observation, Session, AssignedRight, Violation, or PlateListEntry, the
   Level B decoration `apds-ext:apx:credential-digest@1.0`
   (`CredentialDigest`): base64url(HMAC-SHA-256(key, normalized
   identification)) under a key named by `keyId`, visible under
   `apx.data:read`. A consumer that only needs "is this the same plate?"
   — an arrival screen, a list matcher — then matches by digest without
   receiving plates. Keys are distributed by `GET /v1/digest-keys` (scope
   `apx.digest:use`) to clients the operator authorizes; each key states
   the normalization applied before hashing; rotation is by `keyId`,
   with both keys served during an overlap. Keys are scoped to the
   operator organisation: a digest computed by one operator does not
   match another's. A server that publishes digests MUST NOT publish the
   key material under any other scope.
7. **Keys in bodies, not URLs.** Query strings are kept by access logs,
   proxies, gateways, and tracing, outside this clause's minimization.
   Every plate-, card-, or phone-keyed lookup therefore has a form that
   carries the keys in a request body: `POST /v1/lpr/reads:search`,
   `POST /v1/accounts:search`, `POST /v1/payments:search`,
   `POST /v1/violations:search`, and
   `POST /v1/enforcement/eligibility:check`. They answer exactly as the
   corresponding `GET`. Implementations MUST serve the body form of every
   lookup they serve, and a deployment whose calls cross a network
   boundary SHOULD use it.
8. **Per-stay outcomes (normative).** A stay's compliance status (Part
   5 §5.9) and the messages a sign showed for it (Part 26 §26.9) carry
   no plate, but they describe one person's visit. The status is served
   only under `apx.compliance:read` and written only under
   `apx.compliance:write`; a display record never stores the message
   text or the plate, even when the sign showed the plate, and its
   `screenshot` is an access-controlled link (rule 2) that MUST follow
   the retention published for plate imagery (rule 3).

Regulatory compliance (lawful basis, data-subject rights, cross-border
transfer) remains the Distributing Party's responsibility; this clause
defines the interoperable floor the API contract itself enforces.

## 9.7 Integration secrets (normative)

A review of ten remote-assistance platforms found PARCS keys and camera
passwords stored on location and lane records and returned by ordinary
reads. APX closes that door at the contract:

1. No APX response, request body, event payload, change-feed page, or
   decoration MAY carry an integration secret: a PARCS or vendor API key,
   client secret or password; a camera, recorder or device user name and
   password; a SIP password; a webhook signing secret after creation
   (§9.4); a launch signing key or handshake secret (Part 25); or a
   stream address that embeds credentials. The rule applies to every
   member and to any JSON or text nested inside a member.
2. Where a consumer must reach a protected resource, APX names the
   secret, never carries it: a subscription's `secretRef` (§9.4), a
   camera's `credentialRef` (Part 26 §26.3), a key's `keyId` (§9.6 rule
   6). The secret itself is exchanged once, outside APX, at onboarding,
   and the receiver keeps it in its own vault.
3. Every secret APX names is rotatable with an overlap window, as §9.4,
   and the rotation never requires the secret to pass through APX.
4. An implementation that finds a secret in a client's write MUST refuse
   the write with 422 `personal-data-not-permitted` (the problem type
   covers data the request must not carry) and MUST NOT persist or log
   the value.

## 9.8 Launch material and view links (normative)

Two features hand a browser something that works without a token: a
Part 25 launch `url` or signed token, and a Part 26 camera view link.
Both are bearer material and follow the same rules:

1. Single-use, bound to one agent and one subject, and short-lived: a
   launch at most 60 seconds from issue to first use, a view link at
   most 5 minutes. A later use is refused.
2. HTTPS only; never in a query string, a referrer, or a log line. A
   launch token travels in a form POST or the URL fragment; a launch
   `url` and a view link identify their record by at least 128 bits of
   entropy and carry no personal data.
3. Every issue and every use is logged against the agent and the
   `correlationId` of the interaction it served (Part 17 §17.7); a
   consumer MUST NOT reuse or share the link outside that interaction.
4. The session a launch opens ends when the launch closes or expires
   (Part 25 §25.6); a view link stops working at `expiresAt`.
5. Signed launch tokens are JWS with `ES256` or `RS256`; `none` and
   symmetric algorithms are refused; `aud` names the implementation and
   `jti` is unique for at least the token's lifetime.

---

<a id="10-extensibility"></a>

# APX Part 10 — Extensibility

APX is built to be extended the same way it extends APDS.

## 10.1 The three sanctioned mechanisms

1. **Extensions container** (Part 4 §4.3) — arbitrary structured additions to
   any APX resource or APDS entity, namespaced and versioned
   (`apds-ext:<ns>:<class>@<M.m>`).
2. **UserDefinedCodeLists** (Part 11) — open vocabularies. Where an APX field
   is typed `CodeListValue`, implementers MAY add values by publishing their
   own list; they MUST NOT overload APX-registered values with different
   semantics.
3. **New domains** — a new spec module + conformance class + scope family +
   topic prefix, added without touching existing files (this is how
   reservations, permits, and tolling were added to APX itself).

## 10.2 What extension MUST NOT do

- Change the semantics of an existing field, path, topic, or code-list entry.
- Add required fields to existing schemas.
- Introduce parallel constructs for things APDS or APX already models
  (identity, timestamps, money, references, place hierarchy…).
- Squat the `apx` namespace or `apx.` topic prefix (reserved; see Part 3
  §3.3 for the full vendor rules and the adoption path into `apx`).

## 10.3 Level B / Level C posture (informative)

Relative to the DATEX II extension taxonomy the APDS Information Model uses:
APX in-payload decorations via the extensions container are **Level B**
(backward compatible — plain APDS consumers ignore them). The APX companion
API layer (commands, alerts, subscriptions-superset, discovery…) is honestly
**Level C-adjacent**: new interaction roots that APDS consumers never see,
referencing APDS entities by Reference. APX declares
`extensionName: apx` and `extensionVersion` in the OpenAPI `info` block per
the Information Model's extension declaration rule.

---

<a id="11-registries"></a>

# APX Part 11 — Registries

APX open vocabularies are published as **APDS UserDefinedCodeList documents**
(PkCommon/CodeLists) in `spec/registries/`, validated in CI against
`registry.schema.json`.

| Registry | Purpose | Referenced from |
|---|---|---|
| `apx-command-types` | Command plane verbs | `Command.commandType` |
| `apx-alert-types` | Alert taxonomies (seeded from APDS Use Case C.2.2 exceptions/space statuses) | `Alert.alertType` |
| `apx-device-states` | Device state values (in the style of APDS `RefillPointStatusEnum`, which covers refill points only) | `DeviceStatus.deviceState` |
| `apx-topics` | APX event topics | `EventEnvelope.type`, subscriptions |
| `apx-conformance-classes` | Conformance classes | `.well-known/apx-configuration` |
| `apx-issue-types` | Resolution issue classification | `ResolutionIssue.code` (Part 17) |
| `apx-violation-types` | Enforcement violation classification (open; jurisdictional vocabularies extend it) | `Violation.violationType`, `EligibilityResult.suggestedViolationType` (Part 19) |
| `apx-toll-dispute-reasons` | Toll dispute reasons (open) | `TollTransaction.dispute.reason` (Part 15) |
| `apx-toll-dispute-resolutions` | Toll dispute resolutions (open) | `TollTransaction.dispute.resolution` (Part 15) |
| `apx-access-denial-reasons` | Why a credential was refused at a lane (open) | `CredentialAccessEvent.denialReason`, `AccessEvent.denialReason` (Part 21), lane inquiry `monthlyCredential.denialReason` (Part 6) |
| `apx-features` | Optional features of a conformance class | `features` in `/.well-known/apx-configuration` and discovery documents (Part 6 §6.5, Part 16) |
| `apx-session-end-reasons` | Why and how a Session ended (open) | `SessionDetail.closure.endReason` (Part 5 §5.8) |
| `apx-read-review-reasons` | Why a read was sent to human review (open) | `LprReadDetail.review.reasons` (Part 13 §13.3a(4a)) |
| `apx-payment-line-codes` | The lines a payment total is made of (open) | APDS `PaymentLine.idCode` in `PaymentDetail.lines[]` (Part 13 §13.6a) |
| `apx-plate-list-categories` | Plate list categories (open) | `PlateList.category` (Part 24) |
| `apx-ui-surfaces` | The screens an implementation may offer to staff, named `<module>.<view>`, each naming the subject class it opens (open: modules add surfaces) | `UiSurface.surface`, `UiLaunch.surface`, `AllowedAction.execution.surface` (Part 25) |
| `apx-interaction-outcomes` | How a support interaction ended (open) | `SupportInteraction.resolution.code` (Part 17 §17.6) |
| `apx-refund-reasons` | Why a payment was refunded (open) | Refund request `reasonCode` (Part 13 §13.1a) |
| `apx-equipment-roles` | What a piece of hardware does on a lane or in a place (fixed: implementations use the codes, only the maintainers add) | `EquipmentDetail.role` (Part 26 §26.1) |
| `apx-area-types` | Where an item stands in a location (fixed) | `EquipmentDetail.area` (Part 26 §26.2) |
| `apx-comms-point-types` | How a parker reaches help at a point (fixed) | `CommsPoint.type` (Part 26 §26.4) |
| `apx-escalation-roles` | Whom an agent escalates to, as roles (fixed) | `PlaceOperations.escalationContacts[].role`, `ApprovalRequest.role` (Part 26 §26.5, Part 17 §17.3a) |
| `apx-display-message-kinds` | Which message a sign showed (open: a location's own messages are implementer codes) | `DisplayRecord.messageKind` (Part 26 §26.9) |
| `apx-stay-compliance-statuses` | The outcome of scoring a stay, each with its family (open: other engines add their own) | `StayCompliance.status`, `StayCompliance.pendingStatus` (Part 5 §5.9) |
| `apx-stay-compliance-flags` | Facts behind a stay's compliance status (open) | `StayCompliance.flags[]` (Part 5 §5.9) |

Registries marked **fixed** still follow §11.1: entries are added, never
removed or re-meant. They differ from the open ones in that an
implementer does not extend them with a list of its own; a value that is
missing is requested per §11.3 and used as `other` meanwhile.

## 11.1 Rules

1. Registry entries are **add-only**; a definedValue is never removed or
   re-meant. Corrections happen via new entries + deprecation notes.
2. Every addition increments the list `version` (APDS PkCommon rule).
3. `entryIndex` is stable and unique within a list; `definedValue` is unique
   within a list.
4. Implementers extend vocabularies by publishing their OWN lists (with
   their own `creator` and `locator`) and referencing entries via
   `ReferencedCodeListEntry` / `CodeListValue.codeListId`. Implementers MUST
   NOT modify APX registry files.
5. Where severity/priority semantics matter (alerts), the closed enum in the
   schema governs; registries only carry open taxonomies.

## 11.1a Choosing the form of a vocabulary (normative)

Every coded value APX defines takes exactly one of four forms, chosen in
this order:

1. **APDS already defines it** → the APDS enum or code list, by `$ref`
   (Part 0 §0.2, Part 4 §4.4). Example: `PaymentRecord.meansOfPayment` is
   APDS `MeansOfPaymentEnum`.
2. **A closed set APX fully owns**, changing only between editions → an
   `enum` in the schema (an `other` value is the escape where one is
   needed). Examples: `Alert.severity`, `PaymentRecord.channel`, validation
   `method`.
3. **An open set that more than one party must interpret** — implementers
   or vendors add values, and clients, dashboards, or agents act on them →
   a registry in this Part, extended per §11.1(4). Examples: alert types,
   access denial reasons, features.
4. **A value meaningful only inside one implementation** → a plain string,
   with no values seeded by APX.

A field in form 3 is typed `string` and names its registry in its
description; the registry, not the schema, lists the values.

## 11.2 Publication

Each registry declares a `locator` naming its permanent home under
`apx-standard.org`. That domain is **not yet delegated**, so until it is,
the authoritative published copies are served from the specification site:

- <https://rneubauer.github.io/apx/registries/> — an index, the registries
  as JSON, and `registry.schema.json`.

The files there are byte-identical to `spec/registries/` in the repository
and are republished by CI on every push to `main`, so the served copy cannot
drift from the copy CI validates. When the canonical domain is delegated the
`locator` values become live and these become mirrors.

An implementation MUST serve or link the registry versions it validates
against, and MUST advertise those URLs in
`/.well-known/apx-configuration.registries`. Advertising a URL that does not
resolve does not conform.

## 11.3 Registration authority (normative)

1. **Who may request an entry:** anyone — implementer, vendor, or
   operator — by opening an issue against the specification repository
   naming the registry, the proposed `definedValue`, and the use case. No
   membership is required.
2. **Decision:** the maintainers (the working group, once chartered —
   CONTRIBUTING.md) accept, reject with reasons, or defer within **60
   days**. Acceptance appends the entry at the next `entryIndex` and bumps
   the registry version; it never requires a new standard edition.
3. **Interim path:** a requester never has to wait — Part 3 §3.3 lets any
   vendor publish its own UserDefinedCodeList immediately and reference it
   via `CodeListValue.codeListId`; an accepted request migrates the value
   into the APX registry per §3.3(7).
4. **Appeal:** a rejected request may be re-raised with new evidence after
   one edition; should APDS adopt APX, appeals escalate to the APDS change
   process, and any registry value APDS later standardizes is reconciled
   per Part 3 §3.3(8).

---

<a id="12-errors"></a>

# APX Part 12 — Error Model

## 12.1 Problem details

APX endpoints MUST return errors as RFC 9457 `application/problem+json`
using the `Problem` schema. APDS-native routes keep their APDS-documented
error shapes (`ResponseStatus`); implementations SHOULD additionally accept
`Accept: application/problem+json` there. On those routes a client that
sends it receives the registered Problem on 400, 404, and 409, as the data
overlay declares (Part 5 §5.1a); cursor refusals and the shared 401, 403,
and 429 are always Problems.

## 12.2 APX problem-type registry

Base URI: `https://apx-standard.org/problems/`

A `type` URI is a **stable identifier, not a location**. Per RFC 9457 §3.1.1
it need not dereference, and clients MUST match on the exact string rather
than fetching it. The base URI above is therefore fixed even though the
domain is not yet delegated (Part 11 §11.2), and it will not change when the
domain goes live. The human-readable documentation for every type is the
table below.

| Slug | Status | When |
|---|---|---|
| `insufficient-scope` | 403 | Token lacks the operation's OAuth scope |
| `insufficient-grant` | 403 | Scope OK but target outside `apx_places`/`apx_org` grant |
| `idempotency-key-required` | 400 | Mutating operation sent without `Idempotency-Key` |
| `idempotency-conflict` | 409 | Same `Idempotency-Key` replayed with a different body |
| `id-collision` | 409 | Client-supplied UUID already exists (APDS convention) |
| `version-conflict` | 409 | Update targets a stale object version |
| `command-expired` | 422 | Command `expiryTime` passed before dispatch |
| `command-not-cancellable` | 409 | Cancel requested at `dispatched` or later (Part 6 §6.1) |
| `unknown-topic` | 400 | Subscription references an unregistered topic |
| `subscription-failed` | 410 | Operation on a subscription in `failed` state |
| `target-not-found` | 404 | Referenced APDS/APX entity does not exist |
| `validation-provider-unknown` | 422 | applyValidation names a provider not offered at the place |
| `payment-declined` | 422 | Take-payment attempt declined by the payment layer; carries a `payment` member referencing the recorded declined PaymentRecord (Part 13 §13.1) |
| `dispute-closed` | 409 | Toll dispute operation on a closed dispute |
| `pool-exhausted` | 409 | Permit issuance against an exhausted RightPool (Part 14 §14.2) |
| `right-not-linkable` | 409 | AssignedRight already consumed, outside validity, or wrong place (Part 14 §14.1b) |
| `action-not-allowed` | 403 | Execution of an action the current resolution context evaluated as not allowed, or did not offer (Part 17 §17.3) |
| `approval-required` | 403 | Requires-approval action executed without approval evidence (Part 17 §17.3) |
| `rate-limited` | 429 | Throttled; response carries `Retry-After` (§12.3) |
| `violation-transition-illegal` | 409 | Violation transition requested from a state that does not allow it (Part 19 §19.1) |
| `violation-not-issuable` | 409 | `issue` on an unreviewed `guided`/`manual` detection, an `automated` one where policy forbids unreviewed issuance, one whose Session link rests on `lenient` or unapplied match evidence, or one with an evidence read whose plate review is `pending` or `rejected` (Part 19 §19.4) |
| `appeal-closed` | 409 | Appeal operation on a violation with no open appeal, a second appeal, or a closed/voided violation (Part 19 §19.1) |
| `program-not-active` | 422 | Issuance or redemption against a validation program that is not `active`, or a transition out of `ended` (Part 20 §20.1, §20.3) |
| `instrument-invalid` | 422 | Validation code unknown, void, expired, or already redeemed; or an instrument required and absent (Part 20 §20.3) |
| `redemption-limit-exceeded` | 422 | A program rule (maxPerTicket, maxPerDay, stackable, applicableRateTables) refuses the redemption (Part 20 §20.3) |
| `redemption-reversed` | 409 | Reverse requested on an already-reversed redemption (Part 20 §20.4) |
| `statement-closed` | 409 | Reserved. No longer returned by `reverse` since 0.11.0: a reversal in a closed period is credited on the next statement (Part 20 §20.4, §20.6) |
| `statement-overlap` | 409 | Closing a period that overlaps an existing closed statement (Part 20 §20.6) |
| `delivery-method-not-permitted` | 422 | `issue` with a notice delivery method, or without the minimum evidence, the enforcement policy in force does not permit for the detection mode (Part 19 §19.10) |
| `notice-deadline-passed` | 422 | `issue` after the policy's notice deadline for the delivery method has elapsed since detection (Part 19 §19.10) |
| `penalty-exceeds-cap` | 422 | `issue` amount above the policy's penalty cap with `onExceed: refuse` (Part 19 §19.10) |
| `signage-required` | 422 | `issue` at a place whose policy requires posted signage and none was in force at detection (Part 19 §19.10–19.11) |
| `signage-referenced` | 422 | `PUT` altering the text of a Signage record an issued violation references (Part 19 §19.11) |
| `credential-identification-in-use` | 409 | Issuing or replacing with an identification already held by a non-terminal record of the same type (Part 21 §21.1), or issuing a permit whose identification is active on another AssignedRight at the place (Part 14 §14.2a) |
| `credential-transition-illegal` | 409 | Credential transition requested from a state that does not allow it (Part 21 §21.1) |
| `credential-not-replaceable` | 409 | `replace` on a revoked, expired, or already-replaced credential (Part 21 §21.1) |
| `valet-transition-illegal` | 409 | Valet ticket transition requested from a state that does not allow it (Part 22 §22.1) |
| `valet-vehicle-not-located` | 409 | `retrieve` on a ticket still in `dropped` — no parked position recorded yet (Part 22 §22.1) |
| `valet-verification-failed` | 403 | Handback claimant verification failed; the attempt is recorded in `statusHistory` (Part 22 §22.4) |
| `agent-required` | 400 | `pushNegotiatedRate` or `matchTicket` without `agent` — the selecting or matching principal is mandatory (Part 6 §6.6–6.7) |
| `rate-not-negotiable` | 422 | `pushNegotiatedRate` names a RateTable not flagged negotiable for the target's place, or one that does not apply there, or states an `amount` on a host that does not offer the `negotiatedAmounts` feature (Part 6 §6.6) |
| `lane-no-current-transaction` | 409 | `pushNegotiatedRate` or `matchTicket` at a lane with no vehicle transaction in progress (Part 6 §6.6–6.7) |
| `session-not-open` | 422 | `matchTicket` names a Session that is closed, already bound to an exit, or at a different place (Part 6 §6.7); or a plate correction or right unlink on a closed Session outside the operator's dispute window (Part 14 §14.1b, Part 17 §17.5) |
| `unauthenticated` | 401 | Access token missing, malformed, expired, or revoked (Part 9 §9.1) |
| `invalid-request` | 400 | Body or parameter fails the operation's schema or a normative field rule; carries `errors[]` (§12.4) |
| `reference-unknown` | 422 | A Reference or registered code value in the body names nothing that exists or is visible to the caller |
| `request-unprocessable` | 422 | Well-formed body that fails a documented consistency rule of the resource (e.g. an inverted validity window, an ambiguous benefit); `detail` names the rule |
| `personal-data-not-permitted` | 422 | The request carries personal data the surface must never accept, e.g. a full card number or a raw phone number where a handle is required (Part 9 §9.6, Part 0 §0.4) |
| `lost-ticket-fee-undefined` | 422 | `lostTicket` at a place whose applicable rate deck has no `lostTicketFee` line (Part 6 §6.1) |
| `alert-transition-illegal` | 409 | Alert transition requested from a state that does not allow it (Part 7) |
| `toll-transition-illegal` | 409 | TollTransaction or dispute operation from a state that does not allow it (Part 15) |
| `payment-state-illegal` | 409 | Refund, void, capture, or posting on a payment, or cancel on a payment link, whose state does not allow it (Part 13 §13.1a) |
| `reservation-transition-illegal` | 409 | Reservation transition (amend, check-in, cancel, no-show) from a state that does not allow it (Part 14 §14.1) |
| `stream-position-expired` | 410 | SSE `Last-Event-ID` is older than the server's buffer; re-sync via the Part 5 change feed and reconnect without it (Part 8 §8.4) |
| `charging-transition-illegal` | 409 | A ChargingEvent illegal from the session's current state, or out of time order (Part 23 §23.2) |
| `charging-point-unavailable` | 409 | Opening a session on a point/connector that is inoperative, out of order, reserved for another right, or already serving an active session (Part 23 §23.2) |
| `charging-settlement-conflict` | 409 | `…/payment` on a `parkingSession`-settled session, on a `directPayment` session not yet `unplugged`, or on an already `closed` session (Part 23 §23.5) |
| `read-already-bound` | 409 | A `session-detail` write binds a read as `entry` or `exit` that another Session of the same place already holds, and does not name that Session in `supersedes` (Part 5 §5.8) |
| `not-authoritative` | 409 | A write to an entity class for which, at that place, the other replicated implementation is authoritative (Part 18 §18.6) |
| `ui-surface-unknown` | 422 | A UI launch names a surface this implementation does not offer, or not at that place (Part 25 §25.2) |
| `ui-display-mode-unsupported` | 422 | A UI launch asks for a display mode the surface does not support (Part 25 §25.2) |
| `ui-launch-expired` | 409 | An action recorded against a launch whose url or token expired unused (Part 25 §25.3) |
| `ui-launch-closed` | 409 | An action or close against a launch already closed (Part 25 §25.3) |
| `approval-closed` | 409 | A decision on an approval request already decided or expired (Part 17 §17.8) |
| `receipt-channel-unsupported` | 422 | A receipt re-send by a channel this implementation or device cannot serve (Part 13 §13.1b) |
| `comms-point-unknown` | 422 | A resolve request names a comms point, caller id, dialled number or comms device id the implementation does not hold (Part 17 §17.1, Part 26 §26.4) |
| `camera-view-unavailable` | 409 | A camera view cannot be issued now: the camera is offline, or has no recorder for playback (Part 26 §26.3) |
| `playback-range-unavailable` | 422 | A playback view whose `from`/`to` is missing or outside the camera's retention (Part 26 §26.3) |
| `evaluation-out-of-order` | 409 | A `stay-compliance` write whose `sequence` is lower than the one the Session holds (Part 5 §5.9 rule 4) |

Problem responses SHOULD include `detail` and MAY carry additional members
(RFC 9457 extension members), including an `extensions` container.

## 12.3 General HTTP conventions

- 429 with `Retry-After` for throttling (APDS convention).
- 202 for accepted-but-asynchronous work (commands).
- Pagination, where APX defines list endpoints, follows the APDS
  `PaginatedList` metadata shape.
- **Every secured APX operation declares 401, 403, and 429; every
  operation addressed by a path id declares 404; every operation that
  takes a body declares 400** (the shared `Unauthorized`, `Forbidden`,
  `TooManyRequests`, `NotFound`, and `BadRequest` responses). The Spectral
  ruleset enforces this on the bundle, so a new operation cannot ship
  without them.

## 12.4 Choosing a type

A server picks the most specific registered type that applies, in this
order: a domain slug (`violation-transition-illegal`,
`validation-provider-unknown`, …); then `reference-unknown` or
`request-unprocessable` for a well-formed body the server cannot act on;
then `invalid-request` for a body or parameter that fails its schema.

`invalid-request` SHOULD carry an `errors` extension member: an array of
`{ "pointer": "<RFC 6901 JSON Pointer into the request>", "detail":
"<human-readable reason>" }`, one per offending location, so a console
can mark the field rather than parse `detail`.

---

<a id="13-accounts-payments-lpr"></a>

# APX Part 13 — Accounts, Payments, LPR (optional classes)

Three optional conformance classes for call-center and back-office
integration over live PARCS state.

## 13.1 `apx-accounts`

- `GET /v1/accounts?name=|phone=|email=|card=|plate=` — look up accounts
  by any combination. Returns Account[] with balances and
  status. Scope `apx.accounts:read`. `email` (like `phone`) is personal
  data under Part 9 §9.6 and is returned only under this scope.
- `GET /v1/accounts/{id}` — full account info.
- `POST /v1/payments` — take a payment. Body:
  account Reference (or `ticketNumber`), the (required) `place` binding,
  `amount`, `meansOfPayment` (APDS `MeansOfPaymentEnum`), and `channel`
  (`channel: autoAttendant` = a PCI-compliant IVR captures the card out of
  band; APX never carries PANs). **Idempotency-Key REQUIRED.** Returns a
  PaymentRecord with `transactionID`. A body naming neither `account` nor
  `ticketNumber` is `400 invalid-request`. Declines are `422
  payment-declined`; the declined attempt is still recorded (and
  published, §13.4), and the problem SHOULD carry a `payment` extension
  member — a Reference to that declined PaymentRecord — so the caller can
  cite it without a second lookup. `captureLater: true` asks for an
  authorization only — a hold (§13.1a). Captured account payments reduce
  the account balance; a hold does not until it is captured. The body
  MAY carry `apds-ext:apx:payment-detail@1.0` (the itemization and the
  rate sold, §13.6a). Scope `apx.payments:write`.
- `POST /v1/payments/{id}/postings` — accounting write-back
  (PARIS-style): posts account/card/amount/transaction
  ID to the AR system and returns `{confirmationNumber, accountUpdated,
  newBalance}`. Only an `approved`, captured payment can be posted.
- **No card data (normative).** No APX request carries more than the
  truncated last four card digits, in a body member or a query parameter.
  A server that receives more (a PAN in `cardLast4`, an undeclared
  `cardNumber` member, a PAN pasted into a search) MUST refuse it with
  `422 personal-data-not-permitted` before the request is persisted or
  logged, and MUST NOT echo the digits in the problem.
- **Lookup in a body.** `POST /v1/accounts:search` takes the same keys as
  `GET /v1/accounts` in an `AccountSearch` body and answers exactly as
  the `GET` does, so names, phones, emails, and plates never enter a URL
  (Part 9 §9.6). Implementations claiming `apx-accounts` MUST serve it;
  a deployment that crosses a network boundary SHOULD use it.

## 13.1a Payment lifecycle and payment links (financial actions)

Customer-service financial actions are domain operations here — never
control commands (Part 17 §17.4):

- `POST /v1/payment-links` — send a hosted payment link (sms/email) for an
  account, ticket, or session; returns the `PaymentLink` lifecycle
  resource (`sent → opened → paid | expired | cancelled`). The action of
  first resort when policy blocks a gate override. APX never carries PANs;
  `sentTo` is masked. A body naming nothing to settle is `400
  invalid-request`; a `channel` the server does not deliver on is `422
  request-unprocessable`.
- `GET /v1/payment-links/{id}` — the link's current state (scope
  `apx.accounts:read` or `apx.payments:write`), grant-checked on its
  `place`; once `paid`, `payment` references the PaymentRecord.
- `POST /v1/payment-links/{id}/cancel` — `sent | opened → cancelled`; the
  hosted page MUST stop accepting payment from that instant. A link that
  is `paid`, `expired`, or `cancelled` is `409 payment-state-illegal`.
- `POST /v1/payments/{id}/refund` — full, or partial per `amount`.
  Refunds SHOULD require approval by default operator policy; approval
  evidence rides the request when the resolution context demanded it.
  `reasonCode` (a definedValue of `apx-refund-reasons`:
  `changeNotDispensed`, `doubleCharge`, `overcharge`, `serviceFailure`,
  `courtesy`, or an implementer code) says why, so refunds can be
  counted by cause across operators; `reason` stays the free text.
- `POST /v1/payments/{id}/receipt` — send the receipt of a settled
  payment again: `channel` (`email`, `sms`, `print`, `link`) and
  `destination` (the address or number, in the body, never the URL, Part
  9 §9.6 rule 7), with `agent` and `correlationId`. The answer is `202`
  with a `ReceiptDelivery` (`status` `queued`, `sent` or `failed`; a
  `link` for the `link` channel). A channel the implementation does not
  deliver on is `422 receipt-channel-unsupported`; a payment that is not
  settled (a hold, a decline) is `409 payment-state-illegal`. `print`
  prints at the device the payment was taken on, which is the Part 6
  `reprintReceipt` command behind a payment-keyed façade. Where the
  receipt cannot be sent during the call, the agent raises a
  `receiptRequested` alert (Part 7 §7.7).
- `POST /v1/payments/{id}/void` / `POST /v1/payments/{id}/capture` —
  release or settle a hold, where the implementation models an
  authorization lifecycle.

The four payment actions take a REQUIRED `Idempotency-Key`; link cancel
takes an optional one (Part 4 §4.2a). Completion of a link-initiated
payment publishes `apx.accounts.payment.recorded.v1` like any other.

**Payment states and transitions (normative).** `paymentStatus` is one of
`approved`, `declined`, `reversed`. An approved payment also carries
`captureStatus`: `captured` (funds collected; absent means the same) or
`authorized` — a **hold**, where the payment layer approved an
authorization-only request, funds are held, and nothing is collected:

| From (`paymentStatus` / `captureStatus`) | Action | To | Notes |
|---|---|---|---|
| (create, sale) | `POST /v1/payments` | `approved` / `captured` (or absent), or `declined` | the default |
| (create, `captureLater: true`) | `POST /v1/payments` | `approved` / `authorized` (a hold), or `declined` | a server without an authorization lifecycle refuses `captureLater` with `422 request-unprocessable` |
| `approved` / `authorized` | capture | `approved` / `captured` | `amount` MAY be lowered to the captured amount; more than was authorized is `422 request-unprocessable` |
| `approved` / `authorized` | void | `reversed` / `authorized` | hold released, nothing collected |
| `approved` / `captured` | refund | `approved` or `reversed` | `refundedAmount` grows by the refund; the record becomes `reversed` when `refundedAmount` reaches `amount`. A refund above what remains is `422 request-unprocessable` |
| `approved` / `captured` | posting | unchanged | the AR write-back |

Every other combination — refunding or posting a hold or a `declined` or
`reversed` payment, capturing or voiding anything that is not a hold — is
`409 payment-state-illegal`, and nothing changes. `declined` and
`reversed` are terminal.

**Holds stay out of sight of consumers that predate `captureStatus`
(normative).** A hold is returned to the caller that created it (the 201,
its idempotent replay, and capture/void responses), but:

1. `GET /v1/payments` MUST NOT return a hold unless the query carries
   `captureStatus=authorized` (§13.2);
2. `apx.accounts.payment.recorded.v1` MUST NOT be published for a hold; it
   is first published at capture, with the captured record. Voiding a
   hold publishes nothing, since no consumer was told of it and no money
   moved;
3. a hold is never materialized as an APDS `Payment` (§13.6) and does not
   change an account balance until it is captured.

A hold's record therefore reads `paymentStatus: approved` only to its own
creator and to callers who ask for holds.

## 13.2 `apx-payment-history`

- `GET /v1/payments?ticketLast4=&cardLast4=&ticketNumber=&account=&date=&place=`
  — payments made on a ticket or an account. `cardLast4` (truncated PAN,
  PCI-permitted) is
  the **transient-parker lookup of last resort**: at locations without LPR,
  a caller who cannot read their ticket usually has nothing else.
  **Keys (normative):** a query MUST carry at least one key —
  `ticketLast4`, `cardLast4`, `ticketNumber` (exact), or `account` (the
  account's statement view); `date` and `place` only narrow a keyed query.
  A query with no key is `400 invalid-request`: the operation never serves
  the whole ledger as a lookup. The one exception is the change feed below.
  **Privacy rule (normative):** truncated-key lookups (`ticketLast4` or
  `cardLast4`) without a `date` are constrained to the last 8 hours; older
  records require the full ticket number (`ticketNumber`) or an
  account-scoped query (`account`), which are exempt from the window. All
  results remain constrained to the caller's grant (§13.5).
  A `cardLast4` (or any other parameter) carrying more than four card
  digits is `422 personal-data-not-permitted`, refused before the request
  is logged (§13.1).
  **Holds (normative):** results exclude holds (`captureStatus:
  authorized`, §13.1a) unless the query carries `captureStatus=authorized`,
  which returns only holds; `captureStatus=captured` returns only
  collected payments. `captureStatus` narrows a keyed query and is never a
  key on its own.
  **Change feed (normative).** With `mode=change` the route is a feed,
  not a lookup: no key is needed and the 8-hour window does not apply,
  because it serves exactly what `apx.accounts.payment.recorded.v1`
  delivers to the same credential (§13.4) — a hold appears at capture,
  never before, and a voided hold never appears. `ticketLast4` and
  `cardLast4` are refused with it (400 `invalid-request`). Part 5 §5.2
  rule 8, difference 6.
- `POST /v1/payments:search` takes the same keys in a `PaymentSearch`
  body, with the same keys rule, 8-hour window, hold rules, grant rules,
  and response as the `GET` (Part 9 §9.6). Implementations claiming
  `apx-payment-history` MUST serve it.

## 13.3 `apx-lpr`

- Ingest is NATIVE: LPR vendors `POST /observations` (APDS route) with
  Confidence and Image — nothing new to implement.
- `GET /v1/lpr/reads?plate=|ticket=|observation=|session=` — the
  bidirectional cross-lookup: plate → ticket/session (+ accuracy +
  screenshot), ticket → plate, Observation id → read (the read-back route
  for one ingested Observation, which APDS 4.1 lacks), and Session → the
  reads behind it (§13.3a(6)). Scope `apx.lpr:read`. **At least one of
  `plate`, `ticket`, `observation`, or `session` is REQUIRED**; a call
  with none is 400 `invalid-request`, never a bulk export (Part 9 §9.6).
  Keys given together intersect; the optional `place` narrows below the
  grant (§13.5(3)). Every read carries its (required) `place` binding
  (§13.5) and, where the Observation references one, its `lane` and
  `cameraId`; its `detail.accessEvent` (§13.3a(4)) says whether it was an
  entry or an exit.
- **Extensions on the read.** `LprRead.extensions` and
  `PlateCandidate.extensions` project the underlying Observation's
  `extensions` container minus the `apds-ext:apx:lpr-read@1.0` block
  (which is `detail`), so a vendor key preserved on ingest is visible
  through the APX surface (Part 4 §4.3).
- **Read-back by path.** `GET /v1/lpr/reads/{observation}` returns the one
  `LprRead` for an ingested Observation — the same projection as
  `?observation=`, addressed like the revision routes. Unknown or purged
  (§13.3b) is 404 `target-not-found`; a read at a place outside the grant
  is 403 `insufficient-grant`.
- **Lookup in a body.** `POST /v1/lpr/reads:search` takes the `GET`'s
  keys in an `LprReadSearch` body, with the same key rule, grant rules,
  and response, so a plate never enters a URL that proxies, gateways, and
  access logs keep (Part 9 §9.6). The `GET` keeps working; a deployment
  that crosses a network boundary SHOULD use the `POST`.

### 13.3a Read detail, access events, and lane cameras (normative)

APDS's Observation carries the plate, the vehicle's `country`,
`stateProvince`, `make`, `model`, and `color`, one overall `Confidence`,
and per-character confidence — but no confidence per attribute, no
alternate candidates, and nothing about how the vehicle moved. Modern LPR
engines produce all of that, and gateless sites depend on it. APX adds it
as the Level B decoration `apds-ext:apx:lpr-read@1.0` (`LprReadDetail`) in
the Observation's `extensions` container (Part 4 §4.3), so a camera vendor
still ingests through native `POST /observations` and a plain APDS
consumer still sees a valid Observation.

1. **Per-attribute reads.** `detail.plate`, `.country`, `.stateProvince`,
   `.plateCategory`, `.make`, `.model`, `.color`, `.bodyType` are each an
   `AttributeRead` (`value` + `confidence` 0–1). The winning values MUST
   also appear in the APDS-native fields (`observedCredentialId`,
   `vehicleAncillaryIdentification`) so APDS-only readers see them,
   whenever the APDS object can be formed. APDS 4.1 requires `country`
   in `vehicleAncillaryIdentification`, so for a read whose plate country
   the engine did not report, `make`, `model`, and `color` appear in
   `detail` only and `vehicleAncillaryIdentification` is omitted — the
   attributes are kept, not dropped;
   `Confidence.overallConfidence` remains the overall score. Every
   attribute is optional — an engine that does not classify colour omits
   `color` rather than guessing. `plateCategory` is the plate's category
   where the issuing country uses one (for example the prefix on some
   Gulf-state plates). `bodyType` is the engine's own vehicle class label
   (SUV, sedan, pickup, bus, truck…), carried as the engine reports it
   and not mapped to a standard list.
2. **Alternate reads.** `detail.alternateReads[]` keeps the candidate
   plate strings the engine rejected, best first, each with confidence.
   Part 17 §17.5 plate correction SHOULD offer them as `PlateCandidate`s.
3. **Passage geometry.** These are camera facts; they say nothing about
   the facility by themselves.
   - `detail.movement` (`toward | away | stopped | unknown`) is the
     vehicle's motion relative to the camera. `approaching` and
     `receding` are deprecated synonyms of `toward` and `away`: servers
     MUST accept them until 1.0, and writers SHOULD send the new values.
   - `detail.frameReads` is how many frames read this plate as the
     vehicle passed. Engines need at least two to call `movement`, so a
     camera that sees only one of the vehicle's plates still reports it.
   - `detail.platesRead` is how many distinct plates of the vehicle were
     read in the passage (1, or 2 for front and rear).
   - `detail.plateFace` (`front | rear | unknown`) is which plate was
     read, for engines that can tell.
   - `detail.plateBox` is the plate's position in the camera frame as
     fractions 0–1 (`top`, `left`, `bottom`, `right`), so it survives
     image resizing.
   - `detail.speed` is the vehicle's speed in km/h when the engine
     measures it, and absent otherwise — never a sentinel such as `-1`.
4. **Access event.** `detail.accessEvent` (`entry | exit | unknown`) says
   whether the read was a vehicle entering or leaving the facility. The
   LPR system reports it; how it decides is implementation-defined, and
   the APX server MUST NOT infer it from `movement` or `plateFace`.
   Absent means `unknown`. The LPR system MAY revise it — for example
   after pairing the reads of one `captureGroup`, or an entry read with
   an exit read — through `PUT /v1/lpr/reads/{observation}/access-event`
   (APDS 4.1 has no route that replaces one Observation). The server
   writes the value into the Observation's decoration, increments its
   `version`, and publishes `apx.data.observation.updated.v1` (§13.4);
   `If-Match` guards against a stale revision (409 `version-conflict`),
   and resending the current value changes nothing.
4a. **Read review.** A read the place sends to human review — low
   confidence, no confidence, reads of one passage that disagree, or a
   jurisdiction that requires a person to confirm a plate before
   enforcement — carries `detail.review` with `state: pending` and its
   `reasons` (definedValues of `apx-read-review-reasons`, Part 11). A
   reviewer decides through `PUT /v1/lpr/reads/{observation}/plate`
   (`PlateCorrection`, scope `apx.data:write`): `confirmed` keeps the
   engine's values; `corrected` writes the given `plate`, `country`, and
   `stateProvince` into `observedCredentialId`,
   `vehicleAncillaryIdentification` (item 1) and `detail`; `rejected`
   marks the read unusable for pairing and enforcement. `corrected`
   without a `plate`, or `plate`, `country`, or `stateProvince` sent
   with any other decision, is 422 `request-unprocessable`. On the first
   correction the server keeps the engine's values, as `AttributeRead`s
   with their confidences, in `detail.review.original`, and MUST NOT
   change them afterwards — a later correction replaces the current
   values, never the original. Every decision that changes the read sets
   `review.state`, `decidedTime`, and `decidedBy`, increments the
   Observation `version`, and publishes
   `apx.data.observation.updated.v1`; resending the decision and values
   the read already has is a no-op (200, no new version, no event).
   `If-Match` guards against a stale decision (409 `version-conflict`).
   The correction is per read: it happens before any Session exists, it
   changes no Session, and every Session the read feeds sees it (a
   Session's own plate is corrected through `PUT /v1/sessions/{id}/plate`,
   Part 17 §17.5). Without `apx.lpr:read` the 200 echoes only the plate
   values the caller supplied (Part 9 §9.6). A read with no review is
   `notRequired` or carries no `review`. The review queue itself
   (assignment, ordering, reviewers) stays implementation-private. Part
   19 §19.4 rule 6 refuses to issue a Violation while any of its evidence
   reads is `pending` or `rejected`, whatever the detection mode.
5. **Lane cameras.** A lane's LPR cameras are described on its APDS
   `VehicularAccess` by the Level B decoration
   `apds-ext:apx:lane-cameras@1.0` (`LaneCameras`). Per camera:
   `cameraId`, which MUST be unique within the Place and MUST equal
   `Image.cameraID` on that camera's reads (a write that would give two
   lanes of one Place the same `cameraId` is 422
   `request-unprocessable`); and `faces`: `inward`
   (looking into the facility, the way entering traffic travels) or
   `outward` (looking out of it, the way exiting traffic travels). Facing
   is relative to the facility, not to the lane's mode, so it holds on
   `reversible` lanes. Informative: a vehicle moving `away` from a camera
   travels the way the camera faces, and `toward` it the opposite way.

   | `faces` | `movement` | Vehicle is | Plate seen |
   |---|---|---|---|
   | `inward` | `away` | entering | rear |
   | `inward` | `toward` | exiting | front |
   | `outward` | `away` | exiting | rear |
   | `outward` | `toward` | entering | front |

   Most deployments read rear plates: entry-lane cameras face `inward`,
   exit-lane cameras `outward`. A reversible lane typically carries one
   camera each way, so a rear plate is read in either mode. The table
   says what the fields mean; it is not a rule APX applies.
6. **Reads, passages, and Sessions.** Each read is one event: a plate
   seen by one camera, with its `accessEvent`. The lane a vehicle used
   does not change the event — where entry and exit share an unseparated
   driveway, an entry on the exit lane is still an entry. The reads of
   one vehicle's passage, from one camera or several (for example one
   reading the front plate and one the rear, or several frames), share a
   `detail.captureGroup` and SHOULD carry the same `accessEvent`; which of
   them the platform prefers is implementation-defined. `LprRead` returns
   one row per Observation, and a consumer counting vehicles collapses
   them by `captureGroup`. A Session (APDS) is one complete visit, opened
   by an entry and closed by an exit, with its times taken from the
   reads' capture times (item 8); `LprRead.session` links each read to it,
   and `GET /v1/lpr/reads?session=` lists the reads behind a Session. How
   reads are paired into Sessions is implementation-defined. APX defines
   no notion of wrong-way travel: `VehicularAccess.accessType` says what
   a lane is for, and APX does not judge reads against it.
7. **Privacy.** `detail` splits in two. The **plate-bearing** members —
   `plate`, `alternateReads`, `plateCategory`, `plateBox`, `review`,
   and the vehicle attributes `make`, `model`, `color`, `bodyType`,
   `country`, `stateProvince` — are personal data under Part 9 §9.6 and
   appear only under `apx.lpr:*` (and the Part 17 candidate route); make,
   model, and colour are attributes of the read, not a vehicle registry.
   The **passage facts** — `accessEvent`, `movement`, `plateFace`,
   `frameReads`, `platesRead`, `captureGroup`, `speed`, `engine` —
   identify nobody and MUST also be served under `apx.data:read`,
   including on `apx.data.observation.created.v1` and `.updated.v1`
   delivered to a subscriber without `apx.lpr:read`, so a counting
   display or an arrival screen can tell an entry from an exit without
   learning the plate.
8. **Ingest mapping.** Engines report in their own formats; the LPR
   system maps them onto the Observation as follows.
   - **Identity.** The Observation `id` SHOULD be the engine's own read
     id when it is a UUID, so it is stable across re-sends; a re-send
     then receives `409 id-collision` (Part 12) and creates no second
     read, and the LPR system treats that as delivered.
   - **Time.** `observationStartTime` is the moment of capture. Engines
     buffer and re-send, so reads can arrive hours late; a Session's
     start and end MUST follow capture time, never arrival time.
   - **Location.** Without GPS, `location.observerLocation` is the
     camera's or lane's configured position — never a placeholder such
     as 0,0.
   - **Confidence.** Every confidence is on the 0–1 scale; an engine
     that reports 0–100 is scaled.

### 13.3b Retention and purge of reads (normative)

Part 9 §9.6 requires a published retention period for plate reads and
imagery and purge on schedule. On the APX surface a purge looks like
this, so "purged" and "never captured" stay distinguishable:

1. **Imagery purge.** When a read's imagery passes its retention, the
   server removes `imageLink` (and `PlateCandidate.plateImage` /
   `vehicleImage`) from the read and sets `purgedImagery: true`. The read
   itself survives until its own retention. A previously issued image
   link MAY answer 404 or 410; it MUST NOT return the image.
2. **Read purge.** When the read passes its retention, it disappears from
   every APX lookup (`/v1/lpr/reads`, `/v1/lpr/candidates`, resolution
   contexts) and the server emits the APDS entity's deletion on the Part 5
   change feed, so downstream copies can follow. A purged read is not
   tombstoned on the APX surface.

### 13.3c Images of a read (normative)

An engine may keep several stills of one read: the plate crop, a wider
crop around the plate, the overview, and the scene around the vehicle.
APDS `Observation.images[]` can carry them all, but APDS `ImageTypeEnum`
names only `overview` and `plate`.

1. `LprRead.images[]` lists every unpurged image of the read, each with
   its `id`, `imageType`, access-controlled `imageLink`, and `cameraId`.
   `imageLink` on the read stays the preferred still.
2. `imageType` is an APDS `ImageTypeEnum` value or one of `plateExtended`
   (a wider crop around the plate) and `context` (the scene around the
   vehicle, wider than the overview), carried as strings until APDS adds
   them (requested in `docs/apds-change-requests.md`). On the
   Observation, such images are listed with no `imageType`.
3. Servers SHOULD list every unpurged image on both the Observation and
   the `LprRead`. Access and purge follow Part 9 §9.6(2) and §13.3b; a
   purged image leaves the list.

## 13.4 Eventing — the analytics feed

APDS's native `EventTypeEnum` publishes entity lifecycle events for
Sessions, Places, Rates, Rights, and Organisations — but **not** for
payments or observations, the two highest-value streams for financial and
LPR analytics. APX closes both gaps (registry `apx-topics`):

- `apx.accounts.payment.recorded.v1` — published for every recorded
  payment, whether taken via `POST /v1/payments` or ingested from a lane
  device. Event `data` is the PaymentRecord. Implementations claiming
  `apx-accounts` MUST publish it. It is published on creation (declined
  attempts included) **and again on every change** to `paymentStatus`,
  `amount`, or `refundedAmount` (refund), always carrying the full current
  record; consumers key on `data.id` and keep the latest by event `time`,
  so a finance feed learns of every reversal without double-counting. A
  hold (§13.1a) is not published at creation: its first event is at
  capture, and voiding a hold publishes nothing.
- `apx.data.observation.created.v1` — published for every ingested
  Observation (LPR read, RFID hit, sensor event). Event `data` is the
  APDS Observation; `subject` references it. Implementations claiming
  `apx-lpr` MUST publish it; implementations serving `POST /observations`
  writes SHOULD publish it regardless.
- `apx.data.observation.updated.v1` — published whenever an ingested
  Observation changes — today, when the LPR system revises `accessEvent`
  through `PUT /v1/lpr/reads/{observation}/access-event` (§13.3a(4)),
  when a reviewer decides its plate through
  `PUT /v1/lpr/reads/{observation}/plate` (§13.3a(4a)), and on any other
  replacement an implementation supports, including native
  `PUT /observations/{id}` (Part 5). Event `data`
  is the APDS Observation as it now stands; `subject` and place binding
  are those of `observation.created.v1`. Implementations claiming
  `apx-lpr` MUST publish it; consumers key on the Observation id and keep
  the highest `version`.

**The full-fidelity export recipe (informative).** An analytics platform
that wants *everything* about a location combines three mechanisms, all
already normative: (1) bulk/exactly-once history via the Part 5 change
feed (`mode=change&cursor=…`) on every native route — sessions, rates,
rights, observations — and on the APX resource lists of Part 5 §5.2
rule 8 (payments, commands, alerts, violations, and the rest); (2)
real-time push via one Part 8 subscription mixing APDS EventTypeEnum
topics with the APX topics above plus
`apx.data.occupancy.v1`, alert, and command topics; (3) point-in-time
convenience reads (occupancy §5.5, lane inquiry §6.2). Nothing about a
place that APX models is unreachable by feed.

## 13.5 Site binding on aggregating implementations (normative)

Payments, accounts, and LPR reads must remain attributable and isolated
per location when one endpoint fronts many places (Part 8 §8.5, Part 9
§9.3):

1. `PaymentRecord.place` and `LprRead.place` are REQUIRED — every payment
   and plate read names the HierarchyElement it belongs to, in API
   responses and in event payloads alike. `Account.places` SHOULD be
   populated where accounts are place-scoped.
2. **Grant enforcement on place-less lookups.** `GET /v1/accounts`,
   `GET /v1/payments`, and `GET /v1/lpr/reads` need no place parameter,
   but their results MUST be constrained to records whose place binding
   (or, for accounts, any of whose `places`) falls inside the caller's
   `apx_places` grant. A credential granted one garage searching by
   name, plate, or truncated card MUST NOT see records from any other
   location. The scope check alone is NOT sufficient authorization for
   these routes. Under an absent or empty grant these lookups return
   `200` with an empty `data` array, never 403 (Part 9 §9.3a(3)); a
   lookup by id (`GET /v1/accounts/{id}`, `GET /v1/payment-links/{id}`)
   of a record at an ungranted place is `403 insufficient-grant`
   (§9.3a(1)).
3. `GET /v1/accounts` and `GET /v1/payments` declare an optional `place`
   query parameter (comma-separated HierarchyElement ids,
   subtree-inclusive) that narrows results below the grant, the pattern
   established by `/v1/reservations/recent` (Part 14 §14.1a). A `place`
   outside the grant is `403 insufficient-grant`. Implementations that do
   not implement narrowing MAY ignore it.
4. **LPR lookups outside the grant** (Part 9 §9.3a). `GET /v1/lpr/reads`
   declares the item 3 `place` parameter (the HierarchyElement and its
   subtree). A lookup keyed by a value — `plate`, `ticket`, or an
   `observation` id — returns only in-grant reads and is never 403 on
   the key's account: under an empty grant it is an empty 200, so the
   answer never confirms that a plate was seen somewhere the caller may
   not look. A lookup that names an entity outside the grant —
   `place=` or `session=` on the reads, `lane=` or `session=` on
   `/v1/lpr/candidates` — is 403 `insufficient-grant`.

## 13.6 PaymentRecord ↔ APDS Payment mapping (normative)

APDS 4.1 defines `Payment` — a settled-payment *record* embedded in the
rights/session model, requiring `serviceProvider` and `paymentLines[]`.
APX's `PaymentRecord` is not a parallel definition of that concept but the
*action record* of taking a payment: addressable, idempotent, and able to
represent outcomes APDS's Payment cannot (declined, reversed). The two
relate field-by-field:

| APX `PaymentRecord` | APDS `Payment` | Note |
|---|---|---|
| `id` / `version` | `VersionedIdentity` (allOf) | same identity shape |
| `transactionID` | `transactionID` | identical meaning |
| `dateCollected` | `dateCollected` | identical (`dateAuthorised` has no APX field; authorization time is the record's creation) |
| `amount` | `paymentLines[].value` summed (`paymentType: payment` lines) | APX carries the total; the itemization, when the implementation records one, rides `apds-ext:apx:payment-detail@1.0` `lines` (§13.6a) as APDS `PaymentLine`s |
| `extensions["apds-ext:apx:payment-detail@1.0"].rateSold` | `extensions["apds-ext:apx:payment-detail@1.0"].rateSold` | the same decoration on both; on `Payment` it never carries `lines` (§13.6a) |
| `meansOfPayment` | — | APDS vocabulary (`MeansOfPaymentEnum`, which APDS uses for the means a place accepts), APX-only member: APDS `Payment` has no per-payment means |
| `channel` | — | APX-only (where the payment was captured; PCI-safe) |
| `paymentStatus` | — | APX-only; APDS Payment records only collected payments — `approved` with `captureStatus` `captured` (or absent) is the only state that maps |
| `captureStatus`, `captureLater` | — | APX-only (§13.1a); a hold (`captureStatus: authorized`) is not materialized until it is captured |
| `refundedAmount` | — | APX-only lifecycle member (§13.1a); APDS `PaymentTypeEnum` has no refund value, so a partial refund is visible only on the APX surface |
| `account` | `idCode` / RightHolder linkage | correlation, not identity |
| `place` | — | APX-only site binding (§13.5) |
| `ticketNumber`, `cardLast4`, `postings` | — | APX-only call-center/AR surface |
| — | `serviceProvider` | APDS-required; populated by the implementation when materializing |

**Materialization rule:** an implementation that persists APDS `Payment`
entities MUST materialize every `approved`, captured PaymentRecord (a hold
only once it is captured) as (or bind it
to) a native `Payment` with a `paymentLines` entry of `paymentType:
payment` and `value` = `amount`, so plain APDS consumers see the money
without speaking APX. A record itemized with
`apds-ext:apx:payment-detail@1.0` `lines` (§13.6a) is materialized with
those lines as its `paymentLines`, unchanged (same `id`s), in place of the
single line; its `rateSold` is copied to the same decoration on the native
`Payment`, which carries no `lines`. Declined and reversed records exist only on the APX
surface — APDS has no vocabulary for them, which is precisely the gap
`PaymentRecord` fills.

## 13.6a Payment detail (normative)

`PaymentRecord.amount` is the total. Revenue reports, the rate-deck price
check, and fee reconciliation need what the total is made of and what
was sold. APDS already has the itemization shape — `Payment.paymentLines[]`,
each a `PaymentLine` with a `paymentType` (`payment | validation`), a
`value`, and an `idCode` that APDS leaves as a bilateral cost code — but no
shared vocabulary for `idCode` and no member for the rate sold. The Level
B decoration `apds-ext:apx:payment-detail@1.0` (`PaymentDetail`) adds
both, on a `PaymentRecord` and on the materialized APDS `Payment`, without
re-shaping anything APDS defines:

1. `lines[]` — the itemization, as APDS `PaymentLine`s (by `$ref`; each
   has the `id` and `version` of `VersionedIdentity`). A line's code is
   its `idCode`: a definedValue of `apx-payment-line-codes` (Part 11:
   `base`, `convenienceFee`, `serviceFee`, `tax`, `discount`,
   `validation`, `idle`, `energy`) or an implementer code (Part 11
   §11.1). The code fixes the APDS `paymentType`:
   - a `validation` line is `paymentType: validation`; its `value` is the
     amount the validation offset (a positive value), which is not money
     collected. It MAY carry the validation's identifier (Part 20) in
     `identifierId`;
   - every other line is `paymentType: payment`. A `discount` is a
     reduction in the price and carries a negative `value`. When a
     validation covers part of the charge, `base` is the part paid in
     money and the `validation` line carries the rest.
   A line coded `validation` with `paymentType: payment`, or a line of
   another registry code with `paymentType: validation`, is `422
   request-unprocessable`.
2. `rateSold` — the rate the seller sold, as the seller names it (`id`,
   `name`), the APDS `RateTable` at the version applied when the deck is
   the source, the `product` (`standard | pass | other`), and the
   `class` (`standard | validation | discount | zeroed | other`).
3. **Lines sum to the total.** On a `PaymentRecord`, the `currencyValue`s
   of the `paymentType: payment` lines MUST sum to `amount`, in the same
   `currencyType`. A `POST /v1/payments` whose lines do not is `422
   request-unprocessable`, checked before the payment layer is asked:
   nothing is charged and no record is created.
4. **One store.** `lines` appears only on a `PaymentRecord`. On a native
   `Payment` the itemization is the Payment's own `paymentLines`, and the
   decoration there carries `rateSold` and MUST NOT carry `lines`; a
   materialized record's lines become those `paymentLines` unchanged
   (§13.6). The same lines are never stored twice (Part 0 §0.2).
5. **The lines describe the sale.** Refunds, captures, and voids do not
   rewrite them: `refundedAmount` (§13.1a) records what was given back. A
   server that accepts the decoration on `POST /v1/payments` returns it
   unchanged on the record, in `GET /v1/payments` and
   `POST /v1/payments:search`, and in `apx.accounts.payment.recorded.v1`
   events.
6. **No inferred lines.** Implementations SHOULD decorate every
   `PaymentRecord` they can itemize, and MUST NOT invent lines they do
   not know. An undecorated record, or one without a fee line, means the
   itemization is unknown, not that there is none: a client MUST NOT
   infer a fee of zero from an absent line, and a server MUST NOT add a
   zero-value line it was not given.

```json
"extensions": {
  "apds-ext:apx:payment-detail@1.0": {
    "rateSold": { "id": "evening-flat", "name": "Evening Flat Rate", "product": "standard", "class": "validation" },
    "lines": [
      { "id": "e1000000-0000-4000-8000-000000000101", "version": 1, "paymentType": "payment", "idCode": "base",
        "value": { "currencyType": "USD", "currencyValue": 9.00 } },
      { "id": "e1000000-0000-4000-8000-000000000102", "version": 1, "paymentType": "payment", "idCode": "convenienceFee",
        "value": { "currencyType": "USD", "currencyValue": 0.75 } },
      { "id": "e1000000-0000-4000-8000-000000000103", "version": 1, "paymentType": "validation", "idCode": "validation",
        "value": { "currencyType": "USD", "currencyValue": 3.00 } }
    ]
  }
}
```

Here `amount` is USD 9.75: the $12.00 evening rate, $3.00 of it validated,
plus the $0.75 app fee.

APDS impact: none. The registry gives a shared meaning to a member APDS
leaves bilateral, and the decoration sits in the extension container plain
APDS consumers ignore (declared on `Payment` by the data overlay, APX
erratum 010). A shared code list for `PaymentLine.idCode` and a rate-sold
member on `Payment` are offered to APDS as suggestions
(`docs/apds-change-requests.md`, Part B); if APDS adopts them, the APDS
form wins (Part 3 §3.3(8)).

---

<a id="14-reservations-permits"></a>

# APX Part 14 — Reservations & Permits (optional classes)

Both classes are **thin profiles over APDS native machinery** — no parallel
booking or permit entities exist.

## 14.1 `apx-reservations`

The reservation lifecycle rides entirely on native routes:

1. **Quote** — native `POST /quotes` (QuoteRightRequest/Response).
2. **Book** — native `POST /rights/assigned`: an AssignedRight carrying the
   `apds-ext:apx:reservation@1.0` extension (`reservationState: confirmed`,
   `plannedStart/plannedEnd` — the APDS PlannedUse concept). The plate on
   file is a `CredentialAssigned` of `type: licensePlate` under
   `rightHolder.credentials[]` (its `identifier` a Reference), never a
   top-level member. The native create answers 201 `ResponseStatus`
   naming the id; a client that wants the stored right (with the
   server-set `noShowAfter`) reads it back with `GET /rights/assigned/{id}`.
3. **Amend** — native `PUT /rights/assigned/{id}` (change mode);
   `reservationState: amended`. Amendments made **after** check-in retain
   `reservationState: checkedIn` — `checkInSession` remains the normative
   linkage and only the planned times change. The body's `version` is the
   version the client last read (Part 4 §4.2a); a stale one is refused
   with `version-conflict`. Until a change-mode schema exists for native
   entities (Part 5 §5.1), an amend body MUST still carry the
   AssignedRight's required members (`id`, `version`,
   `rightSpecification`, `rightHolder`) so it validates against the
   published schema.
4. **Cancel** — native `DELETE` or state `cancelled`. The server sets
   `plannedUses[0].cancelTime`.
5. **Check-in** — creating a native Session whose segment references the
   AssignedRight transitions the reservation to `checkedIn` and sets
   `checkInSession`. This is the normative linkage.
6. **No-show** — a reservation whose `plannedStart` + grace period passes
   with no check-in transitions to `noShow`, sets
   `plannedUses[0].expiryTime` to that instant, and publishes
   `apx.reservation.noshow.v1` with a `ReservationSummary` as `data`. The
   grace period is operator policy; the server exposes its effect as the
   read-only `noShowAfter` on the extension (and on `ReservationSummary`),
   so a platform can tell the customer when the booking lapses.
7. **Issuer and amount paid.** `ReservationSummary` carries `issuer` (the
   seller the code came from, as an implementer code or the seller's
   organisation name) and `amountPaid` (APDS `AmountInCurrency`, what the
   customer already paid the seller), because reservation codes from
   several sellers collide and because the first question at the lane is
   whether the stay is already paid. A Part 17 §17.1 resolve request that
   carries `reservationCode` MAY carry `reservationIssuer` to say which
   seller's code it is.

**Planned times.** `plannedUses[0].startTime`/`endTime` are authoritative;
the extension's `plannedStart`/`plannedEnd` MUST mirror them on every
write. A server receiving a write where the two disagree applies
`plannedUses[0]` and rewrites the extension to match, so a plain APDS
client editing `plannedUses[0]` still moves the reservation.

**Transitions (normative).**

| From | Legal transitions |
|---|---|
| `confirmed` | `amended`, `checkedIn`, `cancelled`, `noShow` |
| `amended` | `amended` (a further amendment), `checkedIn`, `cancelled`, `noShow` |
| `checkedIn` | changes to the planned times only (the state stays `checkedIn`); back to its pre-check-in state only through the §14.1b unlink |
| `cancelled`, `noShow` | none — terminal |

Any other transition (amending or cancelling a `cancelled` or `noShow`
right, cancelling after check-in, reverting to `confirmed` by a write) is refused
with 409. On the native routes the refusal is the APDS `ResponseStatus`
409, or the problem `reservation-transition-illegal` where the client
negotiates `application/problem+json` (Part 12 §12.1).

Events: native APDS `AssignedRightCreated/Updated/Deleted` topics carry the
reservation payloads; only no-show adds an APX topic.

## 14.1a Customer identity is per-system (normative)

**APX assumes NO central catalog of users.** In real deployments each
location may run a different PARCS, each with its own user/permit database;
an APX endpoint speaks only for the system behind it. Therefore:

1. RightHolder identifiers are LOCAL to the issuing implementation. Clients
   MUST NOT assume a holder id from one APX endpoint resolves at another.
2. Cross-location/cross-system customer correlation is done by the CALLER
   using the **strongest credential available for the parker segment**:
   permits and reservations carry a plate/credential on file; transient
   parkers at LPR-equipped locations are correlated by camera reads; and
   transient parkers WITHOUT LPR are findable only by ticket number or
   truncated payment-card digits (Part 13 §13.2) — often nothing at all
   until they present the ticket at a lane (Part 6 lane inquiry).
3. History/lookup endpoints return only what the queried system knows, and
   accept an optional `place` parameter so aggregating implementations
   (one endpoint fronting many locations) can scope results per location.
4. **The recent lookup.** `GET /v1/reservations/recent` requires `plate`
   or `holder` (400 `invalid-request` otherwise); both together
   intersect. A bare plate string is ambiguous across jurisdictions, so
   the optional `country` and `stateProvince` (APDS
   `VehicleAncillaryIdentification` vocabulary, as in Part 17 §17.5)
   qualify it; absent, the plate string alone is matched. Optional
   `from`/`to` (applied to `plannedStart`; `from` after `to` is 400) and
   `state` reach past the ten most recent for disputes. An unknown
   `place` is 404 `target-not-found`; a place outside the grant is 403
   `insufficient-grant` (Part 9 §9.3a).

## 14.1b Linking a reservation to a session (normative)

At LPR-equipped facilities the plate IS the link: correcting the session's
plate (Part 17 §17.5) lets the implementation's own matching bind the
prepaid AssignedRight. Where no plate can carry the association —
barcode-only reservations, unreadable plates — the explicit link exists:

- `PUT /v1/sessions/{id}/assigned-right` (scope `apx.reservations:manage`)
  binds the session to the AssignedRight, optionally recording the
  presented `reservationCode` as audit evidence. Naturally idempotent.
- **Materialization:** the link MUST land in the underlying APDS Session
  (`segments[].assignedRight`), MUST publish `SessionUpdated`, and MUST be
  visible to plain APDS clients — a façade over APDS-modeled state.
- An AssignedRight already consumed by another session, outside its
  validity window, for another place, or in `reservationState`
  `cancelled` or `noShow` is `409` (problem `right-not-linkable`);
  pricing consequences follow from the link via the implementation's
  normal rating.
- **Re-pointing.** Re-sending the same link is a no-op 200. A link naming
  a *different* right on a session already linked to one is `409`
  `right-not-linkable` ("session already linked") — a link is never
  silently replaced.
- **Unlinking.** `POST /v1/sessions/{id}/assigned-right/unlink` (scope
  `apx.reservations:manage`, `reason` REQUIRED) is the audited undo: it
  reverts the segment to the drive-up right, returns the reservation to
  its pre-check-in state (clearing `checkInSession`), materializes in the
  APDS Session, and publishes `SessionUpdated`. Unlinking a session with
  nothing linked is a no-op 200; a closed Session is 422
  `session-not-open`. Re-point = unlink, then link.
- **Concurrency and approval.** Both writes take `If-Match` with the APDS
  Session `version` the client read (409 `version-conflict` when stale,
  Part 4 §4.2a) and an optional `approval` for actions a resolution
  context gated (Part 17 §17.3).

## 14.2 `apx-permits`

Permits = pooled RightSpecifications:

- **Pool availability** — `GET /v1/permits/pools/{rightSpecId}/availability`
  → `{capacity, issued, available}` (profile over APDS RightPool) for one
  pool. APDS keeps one `RightPool` per period in
  `RightSpecification.rightPools[]`; the optional `pool` (a RightPool id)
  or `at` (an instant) query parameter selects it, and with neither the
  server uses the pool whose `validity` contains now, else the earliest
  future pool. The response names the selected `pool` and its `validity`.
  `issued` = `distributedAssignedRights`, `available` =
  `availableAssignedRights`, `capacity` = their sum; an operator that
  sells above the physical space count MAY report that count as `spaces`.
- **Issue** — `POST /v1/permits/issue` creates a native AssignedRight
  with multiple vehicle `credentials[]` (APDS annual-permit pattern: one
  right, many vehicles). Pool exhaustion is `409` with problem type
  `https://apx-standard.org/problems/pool-exhausted`. A client that may
  retry SHOULD send `Idempotency-Key` (Part 4 §4.2a): a replay returns the
  AssignedRight the key issued (200) and consumes no second slot, while a
  retry without a key issues a second permit.
- **Renewal** — re-issue against the same holder with a new validity window;
  implementations SHOULD link renewals with the `apds-ext:apx:permit@1.0`
  extension (`PermitExtension`): the renewal's issue request carries
  `extensions["apds-ext:apx:permit@1.0"].renews` (a Reference to the
  AssignedRight renewed), and the server MAY set `renewedBy` on the old
  one. APDS 4.1's `AssignedRight` declares no `extensions` container of
  its own; implementations carry the key there regardless (Part 4 §4.3),
  an item for APDS reconciliation (Part 3 §3.3(8)).
- **Waitlist** — OPTIONAL and not interoperable: on exhaustion an
  implementation MAY record a waitlist entry and MAY describe it in a
  vendor-namespaced extension member of the `pool-exhausted` problem. APX
  v1 standardizes no waitlist shape, route, or processing rule, and a
  client MUST NOT rely on one.

### 14.2a Issue refusals (normative)

In the order a server checks them:

1. A body that fails the schema, or a `credentialType` that is not an APDS
   `CredentialTypeEnum` value, is `400 invalid-request`.
2. A `rightSpecification` or `holder` that names nothing that exists, or
   nothing visible to the caller, is `422 reference-unknown`.
3. A stale `rightSpecification.version` is `409 version-conflict`.
4. A RightSpecification with no `rightPools` (an event or quote-priced
   spec) is not a permit: `422 request-unprocessable`, `detail` "not a
   pooled RightSpecification".
5. A `credentialType` absent from the RightSpecification's `credentials`
   allow-list is `422 request-unprocessable`, `detail` naming the type.
6. An identification already carried by another active AssignedRight
   issued from a RightSpecification at the same place MAY be refused with
   `409 credential-identification-in-use` whether or not the server claims
   `apx-credentials`; whether one plate may hold two permits is operator
   policy, and a server that allows it MUST say so in its ICS.
7. An exhausted pool is `409 pool-exhausted`.

### 14.2b Materialization onto the AssignedRight (normative)

The issued permit is an ordinary APDS 4.1 `AssignedRight`, so a stock
APDS lane or enforcement client needs nothing from APX to honour it. The
issue request maps onto it as follows:

1. `rightSpecification` → `AssignedRight.rightSpecification`
   (VersionedReference, as sent).
2. `holder` → one entry of `rightHolder.credentials[]` that is a
   `CustomerCredential`: `credentialAssignedType: customer`, `identifier`
   = the holder Reference (`className: RightHolder`), and `type:
   permit`.
3. Each `credentials[]` entry → one `VehicleCredential` in
   `rightHolder.credentials[]`: `credentialAssignedType: vehicle`, `type`
   = `credentialType`, and `identifier` = a Reference to the
   CredentialRecord when the server claims `apx-credentials` (Part 21
   §21.2), otherwise `{ "id": <credentialIdentification>, "className":
   <credentialType> }` — the identification string itself as the id.
4. `validity` → one `PlannedUse` in `plannedUses[]` with `startTime` =
   `validity.start` and `endTime` = `validity.end`, and `expiry` =
   `validity.end`. With no `validity`, the server applies the selected
   pool's `validity` (or `relativeValidity`) the same way.
5. `issueMethod`, `issuanceTime`, and `assignedRightIssuer` are set by the
   server.

Servers MUST resolve the native `credential_type`/`credential_id` filters
on `GET /rights/assigned` against the identification string the lane
reads (the plate, the tag id), whichever `identifier` form rule 3 chose,
so `GET /rights/assigned?credential_type=licensePlate&credential_id=MBL-7710`
finds the permit.

### 14.2c After issue (normative)

- **Vehicles.** Adding or removing a vehicle is a native
  `PUT /rights/assigned/{id}` of the whole AssignedRight (Part 5), keeping
  the `CustomerCredential` and any extensions. The allow-list rule of
  §14.2a(5) applies to every added vehicle; changing vehicles never
  consumes or returns a pool slot, because the slot is the right, not the
  vehicle.
- **Cancellation.** A permit is cancelled with native
  `DELETE /rights/assigned/{id}`, or ends at `expiry`. Either returns the
  slot to its RightPool (`availableAssignedRights` + 1,
  `distributedAssignedRights` − 1) from the moment it takes effect.
- **Money.** The payment that bought the permit SHOULD be recorded in the
  AssignedRight's native `payments[]`; with `apx-accounts`, that entry's
  `transactionID` equals the PaymentRecord's (Part 13 §13.6), which is how
  a pro-rated refund (`POST /v1/payments/{id}/refund`) finds its payment.
- **Pool changes.** Servers SHOULD publish
  `apx.permits.pool.availability.v1` (registry `apx-topics`), `data` =
  `PoolAvailability` and `subject` = the RightSpecification, whenever a
  pool's `issued` or `capacity` changes, so a portal need not recount
  native AssignedRight events.

Permit consumption (entry/exit) is ordinary APDS Session/Observation data.

### 14.2d Concurrent-vehicle limit (normative)

A multi-vehicle permit (one right, many vehicles, §14.2b) may let its
holder register several vehicles but park only some of them at once.
`apds-ext:apx:permit@1.1` adds two members to the permit decoration;
`@1.0` remains valid and carries `renews`/`renewedBy` only.

1. `maxConcurrentVehicles` — how many of the permit's registered
   vehicles may be parked at the place at once. Absent means all of them.
2. A vehicle that arrives while the limit is already reached is an
   **overage**. The pool is not involved: the slot is still the right
   (§14.2c).
3. `overageBilling.mode` says what an overage costs: `rack` (the place's
   transient deck prices the overage stay), `dayRate` (the flat
   `overageBilling.dayRate`), or `none` (recorded, not billed). An
   operator that cites an overage instead uses the existing
   `permitMisuse` violation type (Part 19).
4. How an overage is detected (which vehicle is the extra one) is
   implementation-defined; the overage stay is an ordinary Session, and
   where an external pairing engine decided it, its `MatchEvidence`
   carries `matchType: right` with the permit as `target` (Part 5 §5.8).

---

<a id="15-tolling"></a>

# APX Part 15 — Tolling (optional class `apx-tolling`)

The one **net-new surface** in APX: APDS has no toll entities. Everything is
still built to APDS conventions — toll points are `SupplementalEquipment`,
reads are native `Observation`s, money is `AmountInCurrency`, settlement
links a Payment reference.

## 15.1 TollTransaction

See schema. Lifecycle: `created → priced → paid`; a dispute moves a
`priced` or `paid` transaction to `disputed`, resolution to `resolved`
(with `dispute.resolution`). `voided` is terminal. `statusHistory[]` is
the immutable audit.

`credential.credentialType` SHOULD be an APDS `CredentialTypeEnum` value
(`licensePlate`, `rfid`, …) and `credential.credentialIdentification`
carries the plate string or tag id as read.

**Transitions (normative).** Every refusal below is `409
toll-transition-illegal` unless stated, and changes nothing:

| Action | Allowed from | To |
|---|---|---|
| create with `pricing` / without | — | `priced` / `created` |
| `…/price` | `created` | `priced` |
| `…/payment` | `priced`; `resolved` (`upheld` or `adjusted`) with no payment attached | `paid` |
| `…/void` | `created`, `priced` | `voided` |
| `…/disputes` | `priced`, `paid` | `disputed` (from `resolved`: `409 dispute-closed`) |
| `…/disputes/resolve` | `disputed` | `resolved` (from `resolved`: `409 dispute-closed`) |

Attaching the PaymentRecord that is already attached to a `paid`
transaction returns 200 unchanged; a different one is refused. A paid
charge is never voided: it is corrected by a dispute resolved `refunded`
(the refund itself is Part 13 §13.1a), and a `refunded` transaction takes
no further payment.

**Disputes.** Disputes are opened by the operator (`apx.tolling:manage`),
on the holder's behalf or on its own initiative; `disputedBy` references
the party on whose behalf the dispute is raised (`RightHolder`,
`Organisation`, or `Contact`) and MAY be absent for an operator-initiated
dispute. APX v1 defines no customer-channel dispute scope. The base codes
are, for `reason`: `wrongVehicle`, `duplicateCharge`, `wrongClass`,
`notLiable`, `other`; for `resolution`: `upheld` (charge stands),
`refunded` (money returned), `adjusted` (new amount owed), `withdrawn`
(the disputing party abandoned it). The base codes are registered as
`apx-toll-dispute-reasons` and `apx-toll-dispute-resolutions`. Senders SHOULD use
them and MAY add implementer codes (Part 11). A resolution of `adjusted` MUST carry
`adjustedPricing`; the server replaces `pricing` with it and keeps the old
amount in `dispute.originalPricing`, so `pricing` changes after `priced`
only through this path.

## 15.2 Endpoints (scope `apx.tolling:manage`)

- `POST /v1/tolling/transactions` — create from Observation refs +
  credential + pricing. **Idempotency-Key REQUIRED** (gantry retry storms).
  A replay returns the transaction the key created in its current
  representation, so `version` and `transactionStatus` may have advanced
  (Part 4 §4.2a). A body failing the schema is `400 invalid-request`.
- `GET /v1/tolling/transactions?plate=&status=&tollPoint=&place=&credentialType=&credentialIdentification=&since=&until=`
  / `GET …/{id}`. Filters combine with AND; `place` is list-valued and
  subtree-inclusive; `since`/`until` bound `statusHistory[0].time`; a
  `status` outside the `transactionStatus` enum is `400 invalid-request`.
- `POST /v1/tolling/transactions/{id}/price` — price a `created`
  transaction (external pricing engines) → `priced`.
- `POST /v1/tolling/transactions/{id}/payment` — attach the settling
  Payment reference → `paid`. Takes an optional `Idempotency-Key`.
- `POST /v1/tolling/transactions/{id}/void` — void a charge that should
  never have been raised (`reason`) → `voided`.
- `POST /v1/tolling/transactions/{id}/disputes` — open a dispute
  (`reason`). Re-opening a resolved dispute is `409 dispute-closed`;
  disputing a `voided` or `created` transaction, or one already
  `disputed`, is `409 toll-transition-illegal`.
- `POST /v1/tolling/transactions/{id}/disputes/resolve` — resolve with
  `resolution` (and `adjustedPricing` for `adjusted`).

`…/price` and `…/void` also take an optional `Idempotency-Key`.

## 15.3 Eventing

`apx.tolling.transaction.created.v1` on creation;
`apx.tolling.transaction.status.v1` on every transition (including price,
void, and dispute open/resolve). Toll reads themselves flow as native Observation data.

---

<a id="16-discovery"></a>

# APX Part 16 — Discovery (optional class `apx-discovery`)

Two layers:

## 16.1 Unauthenticated bootstrap (REQUIRED for all implementations)

`GET /.well-known/apx-configuration` (RFC 8615, served at the host root):
token endpoint, APDS version, supported conformance classes, registry
locations. This is how a client with nothing but a hostname gets started.

- **Where the API lives.** `apiBase` is the URL every path in the bundle is
  relative to, APX and APDS-native routes alike. It is REQUIRED when the API
  is not mounted at the origin root that serves this document (an
  aggregator under `/parking`, a separate API host); absent means that
  origin root.
- **Classes.** `conformanceClasses` MUST list exactly the classes the ICS
  claims (Annex A.20), MUST be closed under the Part 3 §3.1 dependency
  table, and MUST contain only `apx-conformance-classes` registry values or
  vendor classes named per Part 3 §3.1. A client that finds a class claimed
  without a class it requires MUST treat the dependent class as not
  offered, and MUST ignore classes it does not recognise.
- **Features.** `features` lists the optional features the implementation
  offers, as definedValues of the `apx-features` registry (Part 11; Part 6
  §6.5: `negotiatedRates`, `negotiatedAmounts`, `ticketMatching`; Part 26
  §26.3: `cameraViews`, `cameraPlayback`) or vendor features, named
  `<vendor-ns>-<feature>` like vendor classes (Part 3 §3.1). An offered
  feature MUST be listed; an unlisted one is not offered.
- **Registries and edition.** `registries` is keyed by the registry `name`
  from Part 11 (vendor lists by their own name). `registryVersions`, keyed
  the same way, gives the version of each list the implementation validates
  against (Part 11 §11.2). `apxVersion` is the edition implemented (Part 3
  §3.4); when `edition` is also present the two are equal.
- The document carries an optional `extensions` container (Part 4 §4.3),
  as does the discovery document.

## 16.2 Credential-scoped capability document (the optional class)

`GET /v1/discovery` (authenticated, any APX scope; a token with none is
`403 insufficient-scope`):

- MUST reflect the presented token exactly — scopes, `apx_org`,
  `apx_places` — and return only what THIS client can use: conformance
  classes, endpoints (APDS-native and APX), permitted command types,
  offered features, subscribable topics, granted place subtrees, advisory
  rate limits. A token with no `apx_places` claim is reflected as
  `places: []`, never by omitting the member.
- `conformanceClasses` MUST be a subset of the bootstrap document's.
- Two clients with different grants MUST receive different documents.
- **Soundness rule (normative, conformance-tested):** every endpoint and
  command listed MUST be callable by the client for at least one target
  inside its granted places (given valid inputs), so a client whose
  `places` is empty sees no place-targeting endpoint. What an unlisted
  call receives follows Part 9 §9.3a: an endpoint the server implements
  but the token's scopes do not cover is `403 insufficient-scope`; a
  listed endpoint called with a target outside the grant is
  `403 insufficient-grant`; an endpoint of a class the server does not
  claim is `404 target-not-found`. The document is a promise, not
  advertising.
- **Forms.** `endpoints[].path` is the OpenAPI path template exactly as it
  appears in the bundle (`/v1/commands/{id}`), relative to `apiBase`;
  `methods` are upper-case HTTP method names. `topics` lists APX registry
  topics, APDS `EventTypeEnum` values (Part 8), and vendor topics the
  credential may subscribe to. `commandTypes` is the permission list and
  `features` the capability list; a feature is listed for any client
  holding a scope of its class, so a read-only client can see it.
- **Surfaces.** `uiSurfaces[]` lists the Part 25 surfaces this client may
  launch, each with its display and launch modes, the actions it reports,
  and the places where it is offered; it appears for a client holding
  `apx.ui:launch` or `apx.ui:read`, under the soundness rule.
- **What each place can do.** `placeCapabilities[]` says, per granted
  place, which `commandTypes`, `uiSurfaces` and `features` apply there,
  for an implementation whose sites differ (a gated garage and a
  gateless lot under one credential). Each entry is a subset of the
  document's own lists; a place that is not listed has the document's
  lists in full. A console reads it before a call, so an agent is never
  offered a vend at a lot with no gate.
- **Vendor extensions.** Vendor endpoints (`/apx/x/<vendor-ns>/…`), topics
  (`<vendor-ns>.…`), and conformance classes (Part 3 §3.1, §3.3) appear in
  the document under the same soundness rule as APX ones.
- **Rate limits.** `rateLimits` keys are `requestsPerMinute`,
  `commandsPerMinute`, `burst` (most requests in any one second),
  `subscriptionsMax`, and `streamConnectionsMax`; every value is a
  per-credential integer, and other keys are advisory vendor additions. A
  `429 rate-limited` `detail` SHOULD name the key that was exceeded.
- **Replication.** An implementation that shares a place with a
  replicated site or aggregator implementation lists, in `replication[]`,
  which side is authoritative for each entity class at each place
  (Part 18 §18.6). The member is absent otherwise.
- **Lifetime.** The document is computed per request and is valid for the
  lifetime of the token it was computed for. A change to a client's scopes
  or grant MUST take effect through a new token (the old one revoked or
  left to expire), so a client's next discovery with its new token is
  current. `Cache-Control: private` is RECOMMENDED, with a `max-age` no
  longer than the token's remaining lifetime. A client that receives 403 on
  a listed endpoint SHOULD refetch discovery before retrying.

## 16.3 Mutual TLS (`apx-mtls`) and the bootstrap document (normative)

An implementation claiming `apx-mtls` requires TLS 1.2 or later (1.3
RECOMMENDED) and a client certificate on every APX and APDS-native route
except `/.well-known/apx-configuration`, which MUST remain retrievable
without one and MUST list `apx-mtls`, so a prospective client can learn the
requirement. A connection without a client certificate is refused at the
TLS handshake (`certificate_required`); no HTTP response is sent. Access
tokens SHOULD be certificate-bound per RFC 8705 (`cnf.x5t#S256`); a token
whose binding does not match the connection's certificate MUST be refused
with `401 unauthenticated`.

---

<a id="17-resolution"></a>

# APX Part 17 — Customer Service & Resolution (optional class `apx-resolution`)

The `apx-resolution` conformance class. The problem it solves: when a
customer presses the intercom, the agent (human or AI) should not have to
log into another system, find the facility, find the lane, find the
transaction, and reconstruct what happened. One call assembles the context;
the server — not the agent — decides which actions are permitted.

The architecture, each layer grounded in an existing Part:

| Layer | Answers | Where it lives |
|---|---|---|
| DISCOVERY | What exists? | Parts 5, 16 — Place hierarchy, `/v1/discovery` |
| DOMAIN | What is true? | Parts 5, 13–15 — sessions, payments, rights, rates |
| CONTEXT | What is happening? | §17.2 — the ResolutionContext aggregation |
| POLICY | What may be done? | §17.3 — AllowedActions, evaluated server-side |
| ACTIONS | Do it | Part 6 control commands OR domain operations (§17.4) |
| EVENTS | What just happened? | Part 8 — two new topics (§17.6) |

**Implementability floor.** The class itself adds only the three
`/v1/resolution/*` reads plus a handful of small reads/writes owned by the
domains that already exist (passback, plate candidates/correction, payment
lifecycle, support history); the context is composed of schemas other
classes already define. A minimal conforming implementation is: the three
resolution reads plus policy evaluation over the actions it already
supports. Every context section beyond the required core (id, version,
computedAt, status, place, allowedActions) is optional — include what you
know, omit what you don't.

## 17.1 Identifier resolution

Callers present whatever parking-domain identifiers they have
(`ResolutionResolveRequest`): lane, place, device, plate, ticket number,
credential, reservation code, session, holder, and, where the server
holds Part 26 comms points, a comms point or what identifies one (the
caller id, the dialed number, the comms device id). Servers MUST resolve
every relationship they can and MUST NOT fail because some identifiers
are absent. At least one of those identifying members MUST be present;
`interactionId`, `correlationId`, `channel`, `reservationIssuer` and
`guestReference` qualify another member and do not count, so a body
carrying only them is 400 `invalid-request`. Unknown members are ignored
(tolerant reader) and never used for resolution. (The schema states only
`minProperties: 1`; narrowing it to the identifying members would reject
bodies existing clients send, so the rule is enforced by the server.)

Two qualifiers help the lookup: `reservationIssuer` says which seller a
`reservationCode` came from, because codes from several sellers collide
(Part 14 rule 7); `guestReference` (`kind` `hotelRoom`, `company`,
`tenant`, `event` or `other`, and `value`) names a visitor who has no
plate or ticket to give: the hotel guest by room, the vendor by company,
the tenant's employee by tenant, so the context can find the right or
the validation program that covers them.

**Layering rule (normative, amended by Part 26):** telephony values, a
phone number or a caller id, appear in APX only on a Part 26 comms point
and on the three resolve members that name one (`callerId`,
`dialedNumber`, `commsDeviceId`). Whoever runs the calls, the call center
or the voice platform, supplies them; a PARCS implementation never has
to hold, return or understand them. A server that holds no comms points
answers those members with `422 comms-point-unknown` and resolves the
rest of the request as before; a server that holds them resolves a comms
point to its lane when it has one and otherwise to its place only, since
a posted number shared by many lanes can never name the lane by itself
(Part 26 §26.4 rule 6). SIP identifiers are never accepted. (Where an
intercom is inventoried as a `SupplementalEquipment` in the Place
hierarchy, the caller MAY still pass it as `device`.)

## 17.2 Resolution contexts

- `POST /v1/resolution/contexts` (scope `apx.resolution:read`) — resolve
  now; returns 201 with the `ResolutionContext`.
- `GET /v1/resolution/contexts/{id}` — recompute; `version` increments when
  anything material changed. Contexts MAY expire (RECOMMENDED ≥ 1 hour);
  after expiry, re-resolve.
- **Decision audit.** Because a context recomputes, the decisions an
  action was checked against can change minutes later. When a command or
  domain action names a `resolutionContext`, the server SHOULD record in
  that action's audit trail the context `version` its enforcement check
  used, and SHOULD retain the `allowedActions` of every version so used
  for the audit retention period, even after the context expires from
  the read route — "policy allowed it at the time" must stay provable.
- **Partial results (normative):** a server MUST NOT delay the whole
  context because one source is slow. It MAY return `status: partial` with
  `pendingSources[]`; the caller re-reads to refresh. Target latencies
  (informative): P50 < 500 ms from local state; P95 < 2 s when upstream
  calls are required.
- **Scope/grant projection (normative):** a context MUST NOT include any
  section the presenting token could not read via the section's own route
  (an account section requires `apx.accounts:read`; everything is bounded
  by `apx_places`). The context is an aggregation convenience, never a
  privilege escalation.
- **Preloading (informative):** implementations SHOULD create the context
  when the triggering event occurs (intercom call, repeated access denial)
  and publish `apx.resolution.context.created.v1`, so the agent's console
  is populated before the call is answered.

## 17.3 Policy and allowed actions

`allowedActions[]` (and `GET …/allowed-actions` for cheap re-evaluation)
is the policy layer's interoperable surface:

1. Decisions are made **server-side**, against operator policy. The
   consuming agent — explicitly including an AI agent — MUST NOT decide
   permissibility itself; it may only classify, summarize, propose, and
   execute what the server has allowed. (The LLM is never the policy
   engine.)
2. Each `AllowedAction` names a command type, whether it is allowed,
   whether approval is required (and by which role), and — whenever
   `allowed=false` or `requiresApproval=true` — a machine-readable
   `reason` with a display string.
3. **Enforcement (normative):** the decisions are binding on the command
   plane. A `POST /v1/commands` for an action the current context
   evaluates as `allowed=false` MUST be rejected
   (`403 action-not-allowed`); one requiring approval MUST be rejected
   without `approval` evidence (`403 approval-required`). Enforcement
   applies equally to the domain operations an `execution.type: domain`
   descriptor names (`PUT /v1/sessions/{id}/plate`,
   `PUT /v1/sessions/{id}/assigned-right`,
   `POST /v1/sessions/{id}/assigned-right/unlink`, the payment surface);
   their approval evidence rides the request body's `approval` member.
   A command that names a `resolutionContext` whose current
   `allowedActions` do not include its command type MUST be refused with
   `403 action-not-allowed` (not offered is not allowed); a command that
   names no `resolutionContext` is subject only to Part 9. Policy content
   (courtesy limits, thresholds, roles) is operator-defined and out of
   APX scope — only the decision format and its enforcement are normative.
4. `recommendedAction` is advisory, never binding, and MUST be one of the
   allowed actions (a runtime rule; the schema cannot state it).
5. **Descriptor consistency.** The schema enforces the conditional rules
   of `AllowedAction`: `reason` is present whenever `allowed` is false or
   `requiresApproval` is true; `execution.type: control` carries
   `command`, `execution.type: domain` carries `operationId`, and
   `execution.type: ui` carries `surface` (Part 25 §25.5): the action is
   taken on the vendor's own screen, launched for this agent, and what is
   done there comes back as APX data (Part 25 §25.4). The policy decision
   binds a launch exactly as it binds a command.
6. **Unknown execution types.** The set of `execution.type` values grows
   additively (Part 3 §3.2); `ui` was added in Part 25. A client MUST NOT
   fail on an allowed action whose `execution.type` it does not
   recognise: it MUST skip that action (or show it as not executable from
   this client) and process the rest of the context as normal.

## 17.3a Approval requests (normative)

An action that `requiresApproval` needs someone to ask. Today the agent
calls a manager, waits, and types "approved by" into a note.
`ApprovalRequest` makes the asking, the answer and the evidence one
record:

1. `POST /v1/approvals` (scope `apx.support:manage`) opens a request:
   `place`, `action` (the command type or operationId), `target`,
   `requestedBy` (the agent) and `agentType`, `role` (the
   `AllowedAction.approvalRole` it was gated on, a definedValue of
   `apx-escalation-roles` where the place's Part 26 §26.5 escalation
   contacts apply), `reason`, `interactionId`, `correlationId`
   (REQUIRED), and an optional `expiresAt`. It is `pending` on creation.
   `Idempotency-Key` is optional (Part 4 §4.2a).
2. The approver answers with `POST /v1/approvals/{id}/decide` (scope
   `apx.support:manage`): `outcome` `approved` or `denied`, `decidedBy`,
   and a `note`. A decision on a request that is no longer `pending`
   (decided, or `expired` at `expiresAt`) is `409 approval-closed`.
3. The decided request is approval evidence: the executing command's or
   domain operation's `approval` member cites it as `approvalRequest`
   (Reference) beside `approvedBy`, so "who allowed it, and why" is one
   read, and the server MAY verify that the cited request is `approved`
   for that action and target before executing (§17.3 rule 3).
4. `GET /v1/approvals` (filters `place`, `status`, `role`) and
   `GET /v1/approvals/{id}` (scope `apx.resolution:read`) read them; every
   transition publishes `apx.approvals.changed.v1` (data: the request;
   place-bound), which is how an approver's console learns there is
   something to decide.
5. How the approver is reached (a page, a chat, a call) is the operator's;
   APX carries the request and the answer.

## 17.4 Action categories (normative)

Not every action is a control command. Actions split by what they touch,
and each category is owned by the module that already models it:

1. **Operational/physical → Part 6 Control.** Gate and device actuation,
   access state. New `apx-command-types` entries (registry v2):
   `resetPassback`, `forceIn`, `forceOut` (anti-passback correction;
   parameters: `credential`; read side `GET /v1/credentials/{id}/passback`)
   and `courtesyExit` (a gate vend recorded as a tracked courtesy against
   the holder named by `parameters.holder`, Part 6 §6.1 — servers MUST count it toward
   courtesy policy and surface it in `recentOverrides`, where each
   `OverrideRecord` SHOULD copy the Command's `agent`, `agentType`, and
   `reason` so "who granted it, and why" needs no second read). Registry v3 adds
   `pushNegotiatedRate` and `matchTicket` (Part 6 §6.6–6.7): both act on
   the current transaction at a lane, which is why they are commands and
   not domain writes. Control MUST NOT become a dumping ground for
   non-physical writes.
2. **Transactional/business → the owning domain API.** Plate correction is
   `PUT /v1/sessions/{id}/plate` (§17.5); validations are §6.3; rates are
   the native `/rates` machinery.
3. **Financial/customer-service → the payment surface (Part 13 §13.1a).**
   `POST /v1/payment-links`, `POST /v1/payments/{id}/refund|void|capture`,
   and `POST /v1/payments/{id}/receipt` (send the receipt again).
   Refunds SHOULD require approval by default operator policy, and carry
   a `reasonCode` from `apx-refund-reasons`.
4. **Anything else → the vendor's screen (Part 25).** An action the
   implementation has no APX operation for is offered as
   `execution.type: ui` with a surface; what the agent does there comes
   back as the native record where one exists, or as a `UiActionRecord`
   (Part 25 §25.4). Registry v6 adds the three device actions agents
   took in those screens most, `ejectTicket`, `cancelTransaction` and
   `reprintReceipt`, to category 1 (Part 6 §6.1).

The consumer never needs this taxonomy: every `AllowedAction` carries an
`execution` descriptor (`type: control` + command, `type: domain` +
operationId, or `type: ui` + surface), so human and AI agents see one
uniform action list and the right APX surface executes it.

Commands gain four optional fields (additive): `resolutionContext`
(Reference), `correlationId`, `approval` (evidence per §17.3), and
`confirmationLevel` — how far success is physically confirmed (`accepted`
→ `deviceAcknowledged` → `physicallyConfirmed`). Consumers MUST NOT report
an outcome stronger than the confirmation level ("the gate is open" vs
"the open command was accepted") — this distinction is load-bearing for AI
agents speaking to customers.

## 17.5 Plate candidates and correction

`GET /v1/lpr/candidates?session=|lane=` returns `PlateCandidate[]`
(confidence, capture time, access-controlled imagery links per Part 9
§9.6). The agent picks the right one and writes it with
`PUT /v1/sessions/{id}/plate` (a transactional domain action, naturally
idempotent, optionally citing the chosen Observation). This closes the
reservation/plate-mismatch loop: find reservation → find session → correct
plate → link → retry access, every step audited.

**Materialization (normative).** The correction MUST materialize in the
underlying APDS Session — `identifiedCredentials[]` /
`identifiedVehicle` — and MUST publish `SessionUpdated`, so a plain APDS
client reading `/sessions/{id}` sees the corrected plate. The APX route is
a business-action façade (adding audit fields: the cited Observation, the
reason) over APDS-modeled state; it MUST NOT create a parallel plate
store. Plate vocabulary follows APDS `VehicleAncillaryIdentification`
(`country` + `stateProvince`); confidence values are projections of APDS
`Confidence.overallConfidence`. Where the read carried the Part 13 §13.3a
decoration, `PlateCandidate.detail` returns it, and the engine's
`alternateReads` SHOULD be offered as additional candidates — the plate the
engine ranked second is usually the one the agent is looking for.

**Read-level review.** This route corrects the plate of a *Session*. A
plate wrong on a *read* — before any Session exists, or on a read that
feeds several Sessions (a visit and an area stay) or an enforcement
handoff — is corrected on the read itself through
`PUT /v1/lpr/reads/{observation}/plate` (Part 13 §13.3a(4a)); the
Sessions it feeds then see the corrected value. Both are façades over
APDS-modeled state and neither creates a parallel plate store.

**Concurrency (normative).** Two agents can hold the same session open.
`PUT /v1/sessions/{id}/plate` and `PUT /v1/sessions/{id}/assigned-right`
take `If-Match` with the APDS Session `version` the client read; a stale
one is 409 `version-conflict` and nothing is written (Part 4 §4.2a).
Without `If-Match` the write is last-writer-wins, as before. The 200
carries the Session's new `version`.

**Closed sessions (normative).** On a pay-by-plate site the plate is the
billing key, so correcting it after settlement moves a charge between
keepers. A correction on an open Session is always allowed. A correction
on a closed Session (ended, billed, or bound to an exit) is allowed only
inside the operator's dispute window and only with `reason` — it is then
an audited financial adjustment; otherwise it is 422 `session-not-open`.

**Scope of the echo.** The plate write is authorized by `apx.data:write`.
Its 200 echoes only the plate values the caller itself supplied and MUST
NOT disclose any other plate-bearing data (earlier plates, candidates,
imagery); those remain under `apx.lpr:read` (Part 9 §9.6). Where the
resolution context gated the correction, the approval evidence rides the
body's `approval` member (§17.3).

## 17.6 Support interactions and topics

- `POST /v1/support/interactions` / `GET /v1/support/interactions?…`
  (scope `apx.support:manage`) record and query interaction history —
  summaries and command references, never transcripts (Part 9 §9.6).
  Recorded interactions surface in later contexts' `supportHistory`.
- **Retries.** The POST takes an optional `Idempotency-Key`; clients that
  retry SHOULD send it. A replay returns the current record with 200; the
  same key with a different body is 409 `idempotency-conflict` (Part 4
  §4.2a). Without it, a lost 201 retried records the call twice.
- **Open, then complete.** A console MAY record the interaction when the
  call is answered, so the correlation id exists from the first second,
  and complete it with `PUT /v1/support/interactions/{id}` (`endedAt`,
  `resolution`, `summary`, `actions`), which follows Part 4 §4.2a
  (`If-Match` or body `version`; 409 `version-conflict` when stale).
  `GET /v1/support/interactions/{id}` reads one record.
- **Lookup keys.** Besides the subject filters, the list accepts
  `correlationId`, `interactionId`, and `credential`, each sufficient on
  its own, so the §17.7 chain can be walked from either end. A call with
  no filter is 400 `invalid-request`.
- **How it ended.** `resolution.code` is a definedValue of
  `apx-interaction-outcomes` (`resolved`, `vended`, `transferred`,
  `escalated`, `noActionNeeded`, `abandoned`, `callbackScheduled`) or an
  implementer code, with `resolution.display` as the text. The outcome is
  what the call did, not what it was about; the issue is `issue`
  (`apx-issue-types`). "No assistance needed", "manager assisted" and
  "validation applied" are outcomes, and belong here, not in the issue
  list. `actions[]` MAY reference Part 25 `UiLaunch` and
  `UiActionRecord` records beside commands and payment links, so what was
  done on a vendor's screen is listed with what was done by command.
- New topics (registry v4): `apx.resolution.context.created.v1`,
  `apx.support.interaction.recorded.v1`. Place binding per Part 8 §8.5
  (the context's/interaction's `place`). The interaction topic is
  published on create and on every update; `version` tells them apart.

## 17.7 Correlation (normative)

Two identifiers, two owners:

- `interactionId` — OPAQUE, minted and understood only by the calling
  application (it may denote a Teams call, SIP leg, SMS thread, chatbot
  session — APX does not care and MUST NOT interpret it). Echoed on the
  context and recordable on support interactions.
- `correlationId` — a UUID a caller MAY mint at first contact. Every
  resolution request, context, command, payment link, event envelope (as
  `extensions["apds-ext:apx:correlation@1.0"]` or the schema field where
  one exists), and support interaction in the same episode SHOULD carry
  it, so one id reconstructs: interaction → context → policy decision →
  action → gate event → interaction record.

## 17.8 Conformance

`apx-resolution` requires: §17.2 contexts (including partial semantics and
scope projection), §17.3 allowed-actions evaluation AND its command-plane
enforcement, and §17.6 support interactions. Requires `apx-control`.
Passback (§17.4 read + commands) and plate candidates (§17.5) are REQUIRED
where the implementation tracks passback / stores LPR reads respectively,
and otherwise omitted (discovery then does not list them). An
implementation that serves the passback read answers a credential it
knows but does not track (a transient ticket) with 200 and
`state: unknown`; 404 `target-not-found` means the credential does not
exist. The issue
vocabulary is the open `apx-issue-types` registry (v2, 34 entries) —
implementers extend per Part 11. Registry v2 adds the twenty-three
issues that at least five of ten operator call centers share, found in a
six-month sample of 176,860 calls: `noResponse`, `callDropped`,
`audioFault`, `generalInquiry`, `wrongLocation`, `facilityFull`,
`ticketUnreadable`, `ticketNotAccepted`, `noTicketIssued`,
`paidCannotExit`, `cardNotReading`, `paymentMethodUnavailable`,
`mediaJam`, `gateStuckOpen`, `credentialForgotten`, `credentialDisabled`,
`gracePeriod`, `validationExpired`, `authorizedVisitor`,
`receiptRequest`, `refundRequest`, `vehicleIncident` and
`equipmentTest`. `equipmentTest` marks a call that is not a customer's
and is kept out of service figures; `authorizedVisitor` pairs with
`guestReference` (§17.1). Approval requests (§17.3a) are REQUIRED where
any offered action `requiresApproval`; an implementation whose policy
never gates an action omits the route.

---

<a id="18-aggregation"></a>

# APX Part 18 — Aggregation and Onboarding

This Part specifies the lifecycle transition every multi-operator
deployment eventually faces: a
location served by its own standalone APX implementation joins an
**aggregating implementation** — one endpoint fronting many locations from
many operators. It defines what survives the move, what must be re-created,
and in what order. It applies to any implementation hosting places imported
from another implementation; no separate conformance class exists — these
are obligations of whichever classes the aggregator claims.

## 18.1 What an aggregator is

An aggregating implementation is an ordinary APX implementation. Nothing in
this standard distinguishes it on the wire except scale: the same bootstrap
(`/.well-known/apx-configuration`), the same grants, the same discovery
soundness rule. A client onboarding against an aggregator runs exactly the
single-site flow — that uniformity is the design.

## 18.2 Identity across the move (normative)

Per Part 4 §4.1a: HierarchyElement ids SHOULD be preserved on import; on
collision the aggregator MUST mint fresh UUIDs and MUST record each source
id in the element's APDS `operatorDefinedReference`. Consumers migrating
stored keys resolve old → new by querying `/places` and matching
`operatorDefinedReference`. APX resource ids (Commands, Alerts,
subscriptions, contexts) are NEVER migrated — they are historical records
of the source implementation.

## 18.3 The onboarding sequence (normative)

When a place moves from implementation A to aggregator B, the third-party
consumer re-runs onboarding at B. Nothing from A transfers implicitly:

1. **Bootstrap** — `GET /.well-known/apx-configuration` at B's host: token
   endpoint, conformance classes. B's classes MAY differ from A's;
   consumers MUST NOT assume feature parity.
2. **Credential** — a new client registration at B's IdP. B MUST issue the
   grant fail-closed (Part 9 §9.3): `apx_places` naming exactly the
   migrated subtree root(s) — never the wildcard — attributed to the same
   consumer `apx_org`.
3. **Discovery** — `GET /v1/discovery` at B proves the boundary before any
   traffic flows.
4. **Data resync** — cursors are server-local (Part 5 §5.2 rule 5) and DO
   NOT survive the move. The consumer runs `mode=full` per class at B,
   takes fresh cursors, and treats A's history as closed. B is NOT
   required to serve A's pre-import history; where it imports historical
   entities it MUST preserve their `recordInfo` provenance.
5. **Subscriptions** — subscriptions are server-local resources with
   server-held secrets; they MUST be re-created at B (`POST /webhooks`),
   yielding new ids and new signing secrets. A's subscriptions for the
   moved place SHOULD be deleted or filtered by A's operator.
6. **Event source** — `EventEnvelope.source` identifies the publishing
   implementation and therefore CHANGES at cutover. Consumers MUST key
   dedup/attribution on the event's place binding (Part 8 §8.5) plus
   envelope `id`, never on `source` alone.

## 18.4 Cutover (normative where stated)

- There is one moment when A stops being authoritative for the place and B
  starts. The operators of A and B MUST agree on it and SHOULD schedule it
  in a low-traffic window; APX does not coordinate it on the wire.
- After cutover, A MUST stop publishing events bound to the moved place;
  its change feeds emit tombstones for entities it no longer hosts, so
  consumers still cursored at A converge on "this place left".
- In-flight state (open sessions, unsettled payments) is migrated or
  drained by operator agreement — out of APX scope. Whatever B imports, it
  imports as valid APDS entities under §18.2 identity rules.

## 18.5 Isolation obligations recap (informative)

At platform scale the guarantees a consumer relies on are defined
elsewhere and only *matter* here: subtree grants and fail-closed defaults
(Part 9 §9.3), event place binding and filter semantics (Part 8 §8.5),
grant-constrained place-less lookups (Part 13 §13.5), per-filter gapless
feeds and grant-expansion signaling (Part 5 §5.2), and site-bound payments
and plate reads (Part 13). An aggregator conforming to those Parts gives
each tenant exactly the single-site experience — which is this Part's
acceptance test.

## 18.6 Replicated site implementations (normative)

§18.3 moves a place from one implementation to another once. A
replicated pair is different: a site implementation (an edge server at
the garage) and an aggregating implementation (a cloud core) both serve
the same place at the same time, with authority split by entity class —
typically the aggregator for configuration (places, rates, rights,
plate lists, policy) and the site for what it observed and decided
(reads, Sessions, violations, commands, alerts). The site keeps working
when the link is down, and an air-gapped site may never connect.

1. **Authority is declared.** Each side lists, in its discovery document
   (`DiscoveryDocument.replication[]`, Part 16 §16.2), the authoritative
   side per place and entity class, and the peer's `EventEnvelope.source`.
   A client write to a class at a place where the other side is
   authoritative is 409 `not-authoritative`; nothing is written.
2. **Provenance survives replication.** A replicator (a client holding
   `apx.replication:write`) writing the other side's records keeps the
   origin `id`, sets `RecordInfo.origin` to the origin's
   `EventEnvelope.source`, keeps the original `creationTime`, and replays
   lifecycle transitions with their original `statusHistory[].time` and
   `actor`. Without that scope, a write is stamped by the receiver as
   usual, and a write carrying `RecordInfo.origin` is 400
   `invalid-request`.
3. **Idempotency is origin-scoped.** A replicated write carries the
   origin's key as `<origin>:<key>` in `Idempotency-Key`, so a key minted
   at the site never collides with one minted at the aggregator
   (Part 4 §4.2a).
4. **Each side pulls the other's authoritative classes** by the Part 5
   change feed (§5.2, including rule 8 for APX resources), with cursors
   held by the replicator. A conflicting write to a class resolves to the
   authority; the non-authoritative copy is overwritten on the next pull.
5. **Events.** Each side publishes events only for records it is
   authoritative for; a replicated record does not publish a second
   `…created` event at the receiver. Subscribers key on the place binding
   and envelope `id` (§18.3(6)).
6. A replicated pair is not a move: §18.3's re-onboarding and §18.4's
   cutover do not apply, and the grants, cursors, and subscriptions of
   each side are its own.

---

<a id="19-violations"></a>

# APX Part 19 — Violations (optional class `apx-violations`)

Enforcement: what operators variously call tickets, notices, warnings, and
citations, under one resource — **Violation**. A **net-new surface**: APDS
4.1 has no violation entity. It does have the pieces enforcement needs, and
APX builds on them rather than beside them:

- **Eligibility comes from `AssignedRight`.** APDS's own text calls
  assigned rights "the most important source of information for connected
  enforcement systems to check the eligibility of a parked vehicle". APX
  never redefines that; it composes it (§19.3).
- **Evidence is native `Observation` + `Image`** (links, never inline —
  Part 9 §9.6). Location is a `HierarchyElement` reference; the vehicle is
  an APDS `CredentialTypeEnum` credential; money is `AmountInCurrency`;
  settlement links a `Payment` (Part 13).
- **Identity reuses APDS's enforcement role.** The vendored `oAuth` scheme
  already defines the `ep` (Enforcement Provider) role, and `Contact`
  carries an `enforcementSystemProvider` responsibility. An enforcement
  client reads the native routes with `ep` and the APX routes with
  `apx.violations:*`.

Two enforcement styles are first-class, distinguished by
`detection.mode`:

| Mode | Who finds it | Who decides | Typical detector |
|---|---|---|---|
| `automated` | a camera/sensor pipeline | policy (no human) | fixed LPR, bay sensors, mobile LPR vans |
| `guided` | the system surfaces a candidate | an officer, on site | handheld: eligibility check → confirm → issue |
| `manual` | an officer | the officer | handheld / mobile app |

## 19.1 The Violation resource and lifecycle

See schema `Violation`. One resource carries the whole story: `detection`,
`eligibilityCheck` (the §19.3 answer at detection time), `observations[]`
and `evidence[]` (references and links), `notice` (what was issued),
`amount`/`dueTime`/`payment` (settlement), `appeal`, and the immutable
`statusHistory[]` (Part 4 §4.2).

**Lifecycle (normative).**

```
detected ──review──▶ confirmed ──issue──▶ issued ──payment──▶ paid ──▶ closed
   └──review──▶ dismissed

issued ──appeals──▶ appealed ──upheld / reduced──▶ issued
paid   ──appeals──▶ appealed ──upheld / reduced──▶ paid     (pay-then-appeal)
                    appealed ──dismissed─────────▶ closed

every non-terminal state ──void──▶ voided
```

The **terminal** states are `dismissed`, `closed`, and `voided`; every
other state (`detected`, `confirmed`, `issued`, `appealed`, `paid`) is
non-terminal.

1. `review` is valid only from `detected`; `issue` from `confirmed`, or
   from `detected` when `detection.mode` is `automated` AND the policy in
   force permits unreviewed issuance (§19.4 rule 1). A `guided` or
   `manual` detection MUST pass through `review` before `issue` — 409
   `violation-not-issuable` otherwise. A `dismiss` decision MUST carry a
   `reason`; without one it is 400 `invalid-request`.
2. `payment` is valid only from `issued`; `appeals` from `issued`, or
   from `paid` within the policy's `appealWindowDays` (pay-then-appeal,
   §19.6); `appeals/resolve` only from `appealed`. Any other transition
   is 409 `violation-transition-illegal`, except the appeal refusals of
   rule 3. `paid → closed` is a server-side administrative transition
   (reconciliation complete, appeal and refund windows elapsed —
   operator policy) with no API operation.
3. One appeal per violation. The state it was opened from is recorded
   as `appeal.openedFrom`. `upheld` returns to that state (`issued` or
   `paid`); `reduced` does the same with `amount` replaced by
   `adjustedAmount` (REQUIRED; 400 `invalid-request` without it);
   `dismissed` moves to `closed`. **`appeal-closed`** is the refusal for
   the appeal itself: opening a second appeal, opening one on a
   `closed` or `voided` violation or after the appeal window, and
   resolving when no appeal is open, whatever the state. Opening an
   appeal on a violation that was never issued (`detected`,
   `confirmed`, `dismissed`) is `violation-transition-illegal`.
4. `void` is valid from every non-terminal state and never deletes
   anything: the record, evidence links, and `statusHistory[]` remain
   readable. Voiding a `paid` violation SHOULD carry the Part 13
   `refund` reference that returned the money. `void` from a terminal
   state is 409 `violation-transition-illegal`.
5. A detection whose eligibility check finds the vehicle **entitled** is
   still recorded — as `dismissed`, with the basis — so the audit trail
   shows what was checked. Implementations MAY suppress creation under a
   documented policy (e.g. automated pipelines that pre-filter), but MUST
   NOT silently drop a `guided` or `manual` submission.
6. `amount` and `dueTime` are set at `issue` and change only through
   `appeals/resolve` (`reduced`) or a server-applied escalation step of
   the policy in force (§19.10); every change is appended to
   `amountHistory[]`, and `amount` always equals its last entry.
   Collections handoff beyond `closed`/`voided` is out of scope for this
   edition; implementers carry it in `extensions` (Part 3 §3.3).

## 19.2 Endpoints

Scopes: `apx.violations:read` (list, read, eligibility) and
`apx.violations:manage` (create and every transition). Every list is
paginated in the APDS `PaginatedList` shape and constrained to the
caller's place grant (Part 9 §9.3).

- `POST /v1/violations` — record a detection. **Idempotency-Key
  REQUIRED** (cameras and handhelds retry; a retry must not create a
  second violation). The server performs the §19.3 eligibility check and
  records it. Publishes `apx.violations.detected.v1`. An unknown `place`
  or a `violationType` outside the served registries is 422
  `reference-unknown`; a malformed body is 400 `invalid-request`.
- `GET /v1/violations?plate=&noticeNumber=&status=&type=&place=&detectionMode=&since=&until=&sourceSystem=&sourceRecordId=`
  / `GET …/{id}`. `noticeNumber` is the lookup a driver can make with
  what they hold; like `plate`, it names no place, so it returns only
  violations at granted places (Part 9 §9.3a rule 3). `sourceSystem` and
  `sourceRecordId` find a violation imported from another system by its
  source record (§19.13); `sourceRecordId`, too, names no place.
- `POST …/{id}/review` — `confirm` | `dismiss` (guided enforcement). A
  `violationType` in the body corrects the detector's classification.
- `POST …/{id}/issue` — issue the warning/notice/citation: `noticeKind`,
  `amount`, `dueTime`, `deliveryMethod`. Publishes
  `apx.violations.issued.v1`.
- `POST …/{id}/payment` — attach the settling Payment reference → `paid`,
  or a partial payment that leaves it `issued` (§19.6a).
  The payment itself is taken through Part 13 (`POST /v1/payments`, a
  payment link, or an APDS Payment ingested from a pay station).
- `POST …/{id}/appeals` — open the appeal (from `issued`, or from `paid`
  within the appeal window); `POST …/{id}/appeals/resolve` — `upheld` |
  `reduced` (+ `adjustedAmount`) | `dismissed`, with `refund` on a
  pay-then-appeal.
- `POST …/{id}/void` — void with reason (and `refund` when `paid`).
- `POST /v1/violations:search` and `POST /v1/enforcement/eligibility:check`
  — the list lookup and the eligibility check with the plate in a body,
  never a URL; same rules and answers as the `GET`s (Part 9 §9.6 rule 7).
- `POST /v1/enforcement/violation-imports` and `POST …/{id}/session` —
  violations issued by another system, and the Session link set after
  creation (§19.13). The import lives under `/v1/enforcement`, not
  `/v1/violations/…`, for the reason the eligibility check does: no
  literal segment sits beside `/v1/violations/{id}`.
- The versioned `PUT`s on policies and signage take the version last
  read as `If-Match` or body `version` (Part 4 §4.2a).
- `GET /v1/enforcement/eligibility?credential=&credentialType=&place=&at=`
  — §19.3. Lives under `/v1/enforcement` rather than `/v1/violations/…`
  because no violation exists yet when the question is asked (and to
  keep the path unambiguous against `/v1/violations/{id}`).

Every transition appends to `statusHistory[]` and publishes
`apx.violations.status.v1`.

## 19.3 The eligibility check (guided enforcement)

`GET /v1/enforcement/eligibility` answers "is this vehicle entitled to be
here right now, and by what?" — the guided-enforcement screen-pop, the
counterpart of Part 6's lane inquiry. It returns an `EligibilityResult`:

- `entitled` — true when at least one `basis[]` entry is `valid` for the
  place at `checkedTime`.
- `basis[]` — every AssignedRight and Session considered, with a
  `status` explaining why it does or does not cover the stay: `valid`,
  `expired`, `notYetValid`, `wrongPlace`, `consumed`, `suspended`.
- `suggestedViolationType` — when not entitled, the `apx-violation-types`
  value the server's rules suggest. **Advisory**: the officer decides at
  `review`; the server's suggestion is never binding.
- `graceUntil` — when the grace on the stay ends, derived from the
  `stayGrace` of the policy in force (§19.10 rule 7, normative rule 5
  below).
- `lastRead` — the latest plate read of the credential at the place.

**Normative rules.**

1. The answer MUST be derivable from the APDS entities the server holds
   (`/rights/assigned`, `/sessions`, and the Part 14 reservation/permit
   profiles). This is a convenience READ in the spirit of Part 5 §5.5
   (occupancy) and Part 6 (lane inquiry): a client MAY compute the same
   answer from the native routes, and the AssignedRight remains
   authoritative where they disagree.
2. Rights bound to an ancestor of `place` in the hierarchy (a campus
   permit, a lot-wide reservation) MUST count as covering it; rights
   bound to a sibling or a different subtree are `wrongPlace`.
3. `basis[]` MUST NOT include rights the token could not read directly
   via `/rights/assigned` under its grant.
4. Evaluating at a past `at` MUST use the rights and sessions as they
   were at that instant (a backdated payment does not retroactively
   entitle an already-detected violation unless operator policy says so
   at `review`).
5. **`graceUntil` comes from `stayGrace`.** The server derives it from
   the `stayGrace` of the EnforcementPolicy in force at `place` at
   `checkedTime` (§19.10 rule 7). With no right covering the stay, it is
   the stay's start plus `entryMinutes`; once the stay has outlived its
   covering rights, it is the end of the last of them plus
   `exitMinutes`. It is absent when that side has no grace (the member
   absent, or no policy in force), and absent while a right covers the
   stay. The handheld and the sign then agree on the grace, because both
   read the same policy. Which outcome a stay inside or past the grace
   has is the evaluating system's rule, not APX's (Part 5 §5.9).

## 19.4 Automated and guided enforcement (normative)

1. **Automated.** An `automated` detection MUST carry at least one
   `observations[]` reference or one `evidence[]` link, and SHOULD carry
   `detection.confidence` and `detection.rule`. Whether an automated
   detection may proceed `detected → issued` without review is operator
   policy. Where the EnforcementPolicy in force (§19.10) carries
   `unreviewedIssuance`, that is the policy: the server MUST refuse
   `issue` (409 `violation-not-issuable`) unless `permitted` is true,
   the detection's `confidence` is at least `minimumConfidence` (when
   set), and its `violationType` is in `violationTypes` (when set).
   Where the policy is silent, the rule MUST be published in operator
   documentation and enforced the same way. Publishing it on the policy
   lets pipelines read it from `…/policies/effective` in advance.
2. **Guided.** A `guided` detection MUST be reviewed by a human before
   issuance. Implementations SHOULD publish `apx.violations.detected.v1`
   so handhelds can queue candidates; the eligibility check result at
   detection time is recorded so the officer sees what the system saw.
3. **Manual.** A `manual` detection MAY be created and issued by the same
   officer in sequence; the review step still applies (the officer
   confirms their own finding) so that every issued violation has a
   `confirmed` entry with an actor in `statusHistory[]`.
4. `detection.principal` (badge id, pipeline name) and
   `statusHistory[].actor` are opaque strings — no personnel directory is
   implied by this standard.
5. **Match evidence: excuse, never accuse.** How `relatedSession` was
   linked MAY be recorded as the decoration
   `apds-ext:apx:match-evidence@1.0` (Part 5 §5.8 rule 5). Where that
   evidence, or the related Session's `session-detail` `matches[]` entry
   whose `target` is this Violation, has `plate.method: lenient` or
   `applied: false`, the server MUST refuse `issue` with 409
   `violation-not-issuable`: a lenient or shadow match may excuse a stay
   but never accuse one. This holds for every server and every detection
   mode.
6. **Reviewed reads.** Whatever the detection mode, the server MUST
   refuse `issue` (409 `violation-not-issuable`, `detail` naming the
   read) while any LPR read in the Violation's `observations[]` has
   `detail.review.state` `pending` (its plate review is not done) or
   `rejected` (a reviewer found it unusable as plate evidence) — Part 13
   §13.3a(4a). A read with no `review`, or one `notRequired`,
   `confirmed`, or `corrected`, does not block issuance.

## 19.5 Evidence and privacy

Violations carry plate values and imagery and are subject to Part 9 §9.6
in full: plate values appear only under `apx.violations:*` (and the other
scopes §9.6 lists); `evidence[].imageLink` and `lastRead.imageLink` MUST be
access-controlled under the same authentication and place grant as the
violation; retention MUST be published and purgeable. Appeals may carry
`evidenceLinks` supplied by the appellant; implementations MUST treat them
as untrusted media and SHOULD scan or transcode before serving.

## 19.6 Settlement and appeals

Money on a violation is never taken by this Part. `amount` is what is
owed; `payment` is the reference to how it was settled — an APX
`PaymentRecord` (`POST /v1/payments` with the violation's notice number as
the reference, or a `PaymentLink` for pay-by-mail/online) or a native APDS
`Payment`. Attaching it moves the violation to `paid` and, where the
implementation claims `apx-accounts`, the payment's own
`apx.accounts.payment.recorded.v1` event fires as usual.

**Pay then appeal.** A driver may pay first and contest later: an
appeal opened from `paid` within the policy's `appealWindowDays` moves
the violation to `appealed` with `appeal.openedFrom: paid`, and no
escalation applies meanwhile. `upheld` returns it to `paid`. `reduced`
returns it to `paid` with the new `amount`, and the difference is
refunded through Part 13 §13.1a (`/v1/payments/{id}/refund`); `dismissed`
closes it and the whole payment is refunded the same way. The resolve
call carries the refund's reference as `refund`, recorded on
`appeal.refund`. The money moves only through Part 13; this Part records
the link.

Appeal reasons and resolutions are implementer code lists (jurisdictional
vocabulary varies too widely to close); `apx-violation-types` is the only
APX registry this Part defines.

### 19.6a Partial payment (normative)

1. A payment attached with `POST …/{id}/payment` contributes its
   `amount`, or the whole outstanding amount when `amount` is absent.
   Every attached payment is appended to `payments[]` as a
   `ViolationAppliedPayment` (`amount`, `paidTime`, and the Part 13
   `payment` reference) and added to `amountPaid`. A payment an import
   source took itself (§19.13), with no Part 13 record here, carries the
   source's own `reference` instead of `payment`.
2. `paid` means `amountPaid` ≥ `amount`. A payment that leaves an
   outstanding balance keeps the violation `issued`, appends a
   `statusHistory[]` entry noting it, and publishes
   `apx.violations.status.v1`; the escalation clock keeps running on the
   outstanding balance.
3. `payment` is the payment that settled the violation; it is set when
   the violation becomes `paid`.
4. Refunds on a pay-then-appeal (§19.6) and on void reduce `amountPaid`
   by the refunded amount.

## 19.7 Eventing

- `apx.violations.detected.v1` on creation (data: `Violation`).
- `apx.violations.issued.v1` on issuance (data: `Violation`).
- `apx.violations.status.v1` on every other transition — review, payment
  (partial payments included), appeal open/resolve, void — and on a
  change of the Session link or an applied import revision (§19.13)
  (data: `Violation`).

Place binding (Part 8 §8.5) uses `Violation.place`. Subscriptions to these
topics require `apx.violations:read` (Part 9 §9.6 rule 4).

## 19.8 APDS alignment

| Concept | APDS construct reused | APX adds |
|---|---|---|
| Entitlement | `AssignedRight`, `Session`, `/rights/assigned` | the composed `EligibilityResult` read |
| Evidence | `Observation`, `Image`, `Confidence` | references and access-controlled links on the violation |
| Location | `HierarchyElementReference` (`place`, `space`) | subtree semantics for ancestor-bound rights |
| Vehicle | `CredentialTypeEnum` (`licensePlate`, `permit`, `rfid`…) | `jurisdiction`, officer description |
| Issuer | `Contact` / `Organisation` reference; `ep` OAuth role | `notice.issuedBy` |
| Money | `AmountInCurrency`, `Payment` | `amount`, `dueTime`, `payment` reference |
| Lifecycle | — (APDS has none) | the §19.1 state machine and `statusHistory[]` |

Should APDS standardize an enforcement or citation entity natively, Part 3
§3.3(8) applies: the APDS construct wins and this Part migrates to it.

## 19.9 Location of the finding

`Violation.location` carries where the vehicle was and from where it was
observed. It is **modelled on** the APDS `Observation.Location` shape and
reuses its member names and types — `observedLocation` and
`observerLocation` (APDS `PointLocation`, GeoJSON Point) and
`observedLocationTextual` (`MultilingualString`) — with two differences:
`observerLocation` is optional here (APDS requires it; a finding from a
fixed sensor may have none), and APX adds `accuracyMetres`. It is not a
`$ref` to the APDS schema, so an APDS `Location` copied from an
Observation validates here, but not necessarily the reverse.

1. GeoJSON positions are `[longitude, latitude]` **in that order**.
   Enforcement tools that store latitude first MUST swap on the wire;
   implementations SHOULD reject positions whose first element is
   outside −180..180 (400 `invalid-request`, `errors[].pointer` naming
   the position).
2. `guided` and `manual` detections SHOULD carry `observerLocation` (the
   officer's position) — it is the evidence that the officer could see
   the vehicle and the signage. `automated` detections SHOULD copy
   `location` from the underlying Observation.
3. Coordinates are personal data when joined to a plate; Part 9 §9.6
   applies in full (scope minimization, retention, purge).

## 19.10 Enforcement policy (normative)

Jurisdictions increasingly regulate *when* a private-lot violation may
be issued or mailed, *how much* it may exceed the unpaid fee, and *how*
it may escalate. APX carries those rules as an `EnforcementPolicy` bound
to a HierarchyElement — a machine-readable summary the operator
configures from the governing statute (`authority.citation`), never the
statute itself — and makes them **binding on the server**, the same
principle Part 17 applies to allowed actions.

1. **Binding and inheritance.** A policy binds to `place` and applies to
   its subtree. The policy in force at an element is the nearest
   ancestor-or-self `active` policy whose `effectiveFrom`/`effectiveTo`
   covers the instant. `GET /v1/enforcement/policies/effective?place=`
   returns exactly what the server applies, so handhelds, pipelines, and
   appeals portals see the same rules.
2. **At `issue` the server MUST** resolve the policy in force at
   `Violation.place` at `detectedTime` and refuse with 422 when:
   - `notice.deliveryMethod` is not listed in a `deliveryRules` entry
     covering `detection.mode` → `delivery-method-not-permitted`;
   - the matching entry's `noticeDeadline` has elapsed since
     `detectedTime` → `notice-deadline-passed`;
   - the violation carries fewer `observations`/`evidence` images than
     the entry's `minimumEvidence`, or `locationRequired` and no
     `location.observedLocation` → `delivery-method-not-permitted`
     (`detail` names the missing evidence);
   - `amount` exceeds the cap and `onExceed` is `refuse` →
     `penalty-exceeds-cap`; with `clamp`, the server issues at the cap.
     The cap is the lowest `penaltyCap` bound; the relative bounds
     (`maximumMultipleOfUnpaid`, `maximumPercentOverUnpaid`) apply to
     the unpaid parking fee for the stay, which the server derives from
     the rate deck in force and records as `unpaidAmount`, with the
     resulting cap as `capAmount`;
   - `signageRequired` is true and no Signage (§19.11) was in force at
     `place` or an ancestor at `detectedTime` → `signage-required`.
   On success the server freezes `policy` (VersionedReference),
   `signage[]`, `unpaidAmount` and `capAmount` (where a cap applied),
   and the first `amountHistory` entry onto the violation. That entry
   is `reason: issued` with the issued amount; when the amount was
   clamped it is instead a single entry `reason: cap` whose `amount` is
   the clamped figure and whose `requestedAmount` is what `issue` asked
   for. A place with **no** policy in force issues without these
   checks; implementations SHOULD alert (Part 7) when a place under an
   enforcement class has none.
3. **Escalation is server-applied.** For a violation in `issued`, each
   `escalation[]` step fires once, when `afterDays` have elapsed since
   `notice.issuedTime` (and never before `paymentGraceDays`), adding
   `addAmount` and/or `addPercent` of the current amount, subject to
   `overallCeiling`: a step that would pass the ceiling clamps to it.
   The result is appended to `amountHistory[]` (`reason: escalation`,
   `step`) and `apx.violations.status.v1` is published. **The clock
   pauses during an appeal:** steps MUST NOT fire while `appealed`, and
   days spent `appealed` do not count toward `afterDays` or
   `paymentGraceDays`. A violation returning to `issued` after
   `upheld`/`reduced` therefore resumes the schedule where it stood
   when the appeal opened; nothing that "fell due" during the appeal
   fires on return. A policy step whose fixed `addAmount` alone exceeds
   `overallCeiling.maximumAmount` is refused at policy create or update
   (422 `request-unprocessable`); steps that merely could reach the
   ceiling are lawful. Clients MUST NOT compute penalties themselves.
4. `appealWindowDays`: an appeal opened within the window, from
   `issued` or from `paid`, MUST be accepted (subject to §19.1 rule 3);
   outside it, implementations MAY refuse with 409 `appeal-closed`.
5. A violation keeps the policy version it was issued under; policy
   updates (`PUT`, with the version last read per Part 4 §4.2a) affect
   issuance and escalation from then on only.
6. **Errors on policy and signage writes.** A body Reference that names
   nothing visible (an unknown `place`) is 422 `reference-unknown`; an
   inconsistent policy (an inverted effective window, the fixed step of
   rule 3) is 422 `request-unprocessable`; a body that fails the schema
   is 400 `invalid-request` (Part 12 §12.4).
7. **Stay grace.** `stayGrace` states how long a stay may go uncovered
   at each end before it needs a right, as two independent members:
   - `entryMinutes`: the time after entry before a right (a payment, a
     permit, a reservation) must cover the stay;
   - `exitMinutes`: the time after the end of the last covering right
     before the stay must be over.
   Both are OPTIONAL integers of at least 0. An absent member means no
   grace on that side; `0` states explicitly that there is none. The
   policy carries the minutes only. How they combine with the rights
   into a stay's outcome (a late start, an overstay) is the evaluating
   system's rule and stays out of APX. Because the grace sits on the
   policy, it inherits down the place tree (rule 1) and keeps its
   versions (rule 5), so an appeal can show which grace applied on the
   day. It is the source of `EligibilityResult.graceUntil` (§19.3 rule
   5) and of the `policy` a stay-compliance evaluation names (Part 5
   §5.9). Charging idle grace (Part 23) and reservation no-show grace
   (Part 14 §14.1) are separate and unchanged.

## 19.11 Signage (normative)

Notice-and-signage laws make "what was posted at the place on the day"
evidence, as much as the plate photo. `Signage` records the posted text
(`MultilingualString`, verbatim), a photo (`imageLink`, access-controlled
per Part 9 §9.6), the sign's own `location`, `signType`, and the window
it was in force. It binds to a HierarchyElement with subtree inheritance;
`GET /v1/enforcement/signage/effective?place=&at=` returns everything in
force.

1. **History is immutable.** Changing the wording is a new record (or
   new version) with a new `effectiveFrom`, and `effectiveTo` set on the
   old one. `PUT` that alters `text` on a record any issued violation
   references is refused (422 `signage-referenced`).
2. **Frozen at issue.** The server records on the violation, as
   `signage[]` VersionedReferences, every Signage in force at `place` or
   an ancestor at `detectedTime`. Appeals see the sign text and photo
   the officer saw.
3. When the policy in force sets `signageRequired`, issuance without any
   such record is refused (`signage-required`, §19.10).
4. Signage is not a rate deck: it records what was displayed, not what
   is charged. Where the two disagree, the discrepancy is what the appeal
   is about, and the standard preserves both.

APDS 4.1 has no signage concept (`PlaceInformation` carries operating
restrictions and times, not posted text); this is net-new, built to APDS
conventions and subject to Part 3 §3.3(8).

## 19.12 Charging bays (Part 23)

A vehicle in an EV charging bay that is not plugged in — an ICE vehicle
or an EV not charging — is the existing `restrictedSpace` type, evidenced
by the bay Observation the Part 23 §23.3 fusion recorded. Registered in
`apx-violation-types` v2: `chargingBayIdle` — a vehicle remained connected in a
charging bay after charging completed beyond the posted idle limit; the
candidate is raised by a consumer of `apx.charging.idle.started.v1` when
`idlePolicy.maxIdleMinutes` elapses (Part 23 §23.10). Whether idle is a
fee, a violation, or both is the place's enforcement policy (§19.10) and
signage (§19.11).

## 19.13 Violations issued by an external enforcement system (normative)

Enforcement vendors issue citations on their own systems and report them
afterwards. The lifecycle of §19.1 assumes this server detected and
issued the violation through its own API calls, stamped with its own
time; an already-issued citation from elsewhere arrives in whatever
state its source has reached, with the source's times.

1. **Import.** `POST /v1/enforcement/violation-imports` (scope
   `apx.violations:manage`, `Idempotency-Key` REQUIRED) records a
   `ViolationImport`: a violation in any state, with the
   `statusHistory[]` times and actors the source recorded, its notice,
   amounts and payments, and the decoration
   `apds-ext:apx:violation-source@1.0` (`ViolationSource`: `system`,
   `recordId`, `sourceStatus`, `sourceRevision`, `sourceUpdatedTime`,
   and the server-set `receivedTime`). `ViolationImport` is its own
   request body, not a `Violation`: the members a `Violation` marks
   read-only (`violationStatus`, `notice`, `amountPaid`, `payments`) are
   ordinary members here, because the source, not this server, decided
   them. The server MAY keep the source's `id` or mint its own.
2. **Keyed on the source.** The pair `system` + `recordId` identifies the
   record. A later import with a higher `sourceRevision` — or, when the
   source keeps no counter, a later `sourceUpdatedTime` — updates it
   (200, version + 1, `apx.violations.status.v1`); the same revision is
   a no-op 200; an older one is 409 `version-conflict`. With neither
   ordering member, every import applies.
3. **What an import must say.** A state at or past issuance (`issued`,
   `paid`, `appealed`, `closed`) MUST carry `notice` (with
   `noticeNumber` and `issuedTime`) and `amount`, else 400
   `invalid-request`. `paid` MUST have `amountPaid` at least `amount`; a
   part-paid citation is `issued` with its `payments[]` (§19.6a), and
   `paid` below `amount` is 422 `request-unprocessable`. Payments the
   source took itself carry its own `reference` (§19.6a rule 1).
   A warning with no fine (a source status such as `warned`) is
   `issued` with `notice.noticeKind` `warning` and an `amount` of zero
   in the place's currency, as `POST …/{id}/issue` records one natively;
   `sourceStatus` keeps the source's word, and a warning the source
   treats as finished MAY be imported as `closed`.
   `statusHistory[]` is append-only: the server keeps the entries it
   already holds and appends only the source's entries later than its
   last one, never rewriting history (Part 4 §4.2).
4. **The source decided.** The server performs no eligibility check
   (§19.3) and applies no policy clamp (§19.10) to an import; the
   source's status vocabulary is kept in `sourceStatus` and mapped onto
   `violationStatus` (a partial payment is `issued` with `amountPaid`,
   §19.6a). The first import publishes `apx.violations.detected.v1`,
   and `apx.violations.issued.v1` when the imported state is at or
   beyond `issued`. `sourceRecordId`, like `noticeNumber`, is a lookup
   that names no place (Part 9 §9.3a rule 3).
5. **Thereafter.** An imported violation follows §19.1 like any other:
   later transitions are API calls stamped by this server.
6. **Linking a Session after creation.** `POST /v1/violations/{id}/session`
   (`ViolationSessionLink`) sets, corrects, or clears (`session: null`)
   `relatedSession`, and writes the match evidence as the decoration
   `apds-ext:apx:match-evidence@1.0` (Part 5 §5.8), with the Session as
   `target`.
   - The link is not a lifecycle transition: it is valid in every
     state, terminal ones included, and appends nothing to
     `statusHistory[]`.
   - `If-Match` carries the Violation `version` last read (Part 4
     §4.2a); a stale one is 409 `version-conflict` and nothing is
     written. A change increments `version` and publishes
     `apx.violations.status.v1`; resending what the violation already
     holds is a no-op (200, no new version, no event).
   - A Session that does not exist or is not visible to the caller is
     422 `reference-unknown`; one at a place outside the hierarchy of
     the violation's `place` — neither the same element, an ancestor,
     nor a descendant — is 422 `request-unprocessable`.
   - Recording lenient or unapplied evidence on a violation is allowed;
     it then blocks `issue` (§19.4 rule 5).

---

<a id="20-validations"></a>

# APX Part 20 — Validations (optional class `apx-validations`)

Validation **program management**: everything an operator does around the
two pieces Part 6 §6.3 already defines — the provider lookup
(`GET /v1/validations/providers`) and the `applyValidation` command.
This Part adds merchant enrolment, instrument stock (codes, QR, stamps),
the redemption ledger every channel writes, and merchant billing.

**APDS alignment.** APDS 4.1 records that a validation *happened*
(`PaymentTypeEnum: validation`, `Segment.validationType/validationId`,
`RateTable.validation`, `RateDiscount`) and nothing about who may
validate, on what terms, or who pays. APX composes those constructs:
the merchant is a `Contact`/`Organisation`, the place a
`HierarchyElement`, the ticket a `Session`, money `AmountInCurrency`,
comped time `Duration`, and the applied validation's id **is** the APDS
`Segment.validationId`. No APDS entity is redefined; Part 3 §3.3(8)
applies if APDS later standardizes a validation program natively.

## 20.1 ValidationProgram

See schema. A program is one merchant's enrolment at one place (or
subtree root): `benefit` (exactly one of `amount`, `duration`,
`percentage`), `rules` (§20.3), `billing` (§20.6), `issuanceMethods`, and
`programStatus`.

1. **Lifecycle:** `active ⇄ suspended`; either → `ended` (terminal).
   Transitions and every other change go through `PUT` (Part 5 §5.1
   full-update semantics) with the version last read as the
   precondition, sent as `If-Match` or as the body's `version` (Part 4
   §4.2a); a stale version is 409 `version-conflict`. Any transition out
   of `ended` is 422 `program-not-active`. Every transition appends
   `statusHistory[]` and publishes `apx.validations.program.status.v1`.
   On enrolment or update, a body Reference (`place`, `provider`, an
   `applicableRateTables` entry) that names nothing visible is 422
   `reference-unknown`; a `benefit` carrying none or more than one of
   `amount`, `duration`, `percentage` is 422 `request-unprocessable`.
2. **Provider list derivation (normative).** When `apx-validations` is
   claimed, `GET /v1/validations/providers?place=` MUST return exactly the
   `active` programs whose `place` is the queried element or an ancestor
   of it, each row carrying the additive `ValidationProvider.program`
   reference, with `benefit` and `validationType` copied from the
   program. The Part 6 enforcement rule (422 `validation-provider-unknown`
   for `applyValidation` with an unlisted provider) therefore becomes
   "not an active program here".
3. Benefit and rule changes apply to redemptions from the update onward.
   Instruments already issued keep the benefit they were issued with
   (`ValidationInstrument.benefit`).

## 20.2 Instruments and issuance

`POST /v1/validations/programs/{id}/issuances` issues a batch of
`quantity` instruments of one `method` to the program the path names.
The body (`ValidationIssuanceRequest`) need not repeat `program`; one
that differs from the path is 422 `request-unprocessable`, as is a
`method` not in the program's `issuanceMethods`. **Idempotency-Key
REQUIRED** — a retried print job must not double the stock.

1. For `code`, `qrCode`, and `digital`, the server generates `codes[]`
   and returns them **exactly once**, in the 201 response (the Part 8 §8.1
   secret convention). Idempotent replays and later reads omit them.
   Codes MUST be unguessable (≥ 64 bits of entropy) and unique across the
   implementation, not merely across the program.
2. `stamp` batches carry no codes (physical stock, counted for billing);
   `api` batches represent redemptions a merchant system records directly
   without a per-instrument code.
3. `validTo` defaults from the program's `rules.validityWindow`; an
   instrument past `validTo` is `expired`.
4. `GET /v1/validations/instruments/{code}` returns the instrument's
   status, program, window, and benefit. A code outside the caller's grant
   is **404, never 403** — the read MUST NOT act as an oracle for guessing
   codes. Implementations SHOULD rate-limit it per credential (Part 12
   §12.3).
5. **Voiding stock.** `POST …/issuances/{issuanceId}/void` moves a batch
   `issued → void` and every instrument in it that is `valid` or
   `expired` to `void` — the stolen sheet of codes; instruments already
   `redeemed` are unaffected and their redemptions stand until reversed.
   `POST /v1/validations/instruments/{code}/void` voids one code; a
   `redeemed` code is 422 `instrument-invalid` (reverse the redemption
   first). Both take `{ reason, note }`, record `voidDetail`, and return
   the resource unchanged (200) when it is already `void`. Scope:
   `manage`, or `redeem` for the owning provider; outside that scope the
   batch or code is 404 (§20.5). A voided code is refused at redemption
   as `instrument-invalid` (§20.3 rule 2).

## 20.3 Redemption and the rule set (normative)

A redemption is one validation applied to one ticket/session. Every
channel writes the same resource:

| Channel | How it arrives |
|---|---|
| `payStation`, `lane`, `merchantApp`, `api` | `POST /v1/validations/redemptions` (**Idempotency-Key REQUIRED**) |
| `callCenter` (an agent acting on a lane) | the Part 6 `applyValidation` command; the server materializes the redemption with `command` set |

At redemption the server MUST evaluate, in order, and refuse with 422:

1. `program-not-active` — program not `active`. A redemption whose
   `place` is neither the program's place nor within its subtree is
   instead `request-unprocessable`, with `detail` naming the program's
   place, so a device can tell "wrong place, retry" from "program
   suspended".
2. `instrument-invalid` — `instrumentCode` given but unknown, `void`,
   `expired`, or already `redeemed`; or the program requires an
   instrument (no `api` method) and none was given.
3. `redemption-limit-exceeded` — `rules.maxPerTicket` or
   `rules.maxPerDay` would be exceeded; or stacking is refused, which
   is evaluated **both ways**: the incoming program is not stackable and
   another program is already applied to the ticket, **or** any program
   already applied to the ticket is not stackable; or the session's
   rate table is not in `rules.applicableRateTables` (or does not accept
   validations per APDS `RateTable.validation`).

On success the server MUST: materialize the APDS-native record
(a Payment of type `validation` and the session segment's
`validationType`/`validationId`, per Part 6 §6.3) and return it as
`validationId`; set `amountReduced` (and `durationComped`) to the
**actual** effect on the amount due, never the nominal benefit; consume
the instrument (`redeemed`); and publish `apx.validations.redeemed.v1`.
Where the implementation also claims `apx-control`, the lane's
`currentTicket.validations[]` (Part 6) reflects the redemption. A
redemption materialized from an `applyValidation` command carries it as
`command`, and `GET /v1/validations/redemptions?command=` finds it from
the command's id.

The rule set above is closed so that two implementations evaluate the
same program identically. Rules beyond it (day-of-week, minimum spend,
customer segment) ride in `extensions` (Part 3 §3.3) and are the
implementer's to enforce.

## 20.4 Reversal

`POST /v1/validations/redemptions/{id}/reverse` moves `applied →
reversed`, restores the amount due on the session (and the APDS-native
record) while the session is still open, and returns the instrument to
`valid` while still inside its window. Already reversed is 409
`redemption-reversed`. For a session already closed and settled, the
reversal is a billing correction only; it does not reopen the amount
due.

**Reversal after closure (normative).** A redemption already billed on
a **closed** statement is reversed like any other — merchant
disputes after invoicing are the normal case. The closed statement is
never edited: the reversal records it as `reversal.closedStatement`,
and the next statement closed for the program carries a negative
`credit` line for the redemption (§20.6), whose id is then set as
`reversal.creditedOn`. There is one ledger and no separate credit
operation. Servers MUST NOT refuse such a reversal with
`statement-closed`; that type is no longer returned by this operation.

## 20.5 Merchant scope (normative)

`apx.validations:redeem` is the merchant-facing scope. A token carrying it
(and not `:read`/`:manage`) is confined to programs whose `provider`
equals the token's `apx_org` (Part 9 §9.3): it MAY read those programs,
issue their stock, check their codes, record and list their redemptions,
and read their statements; it MUST NOT see other providers' programs or
redemptions, enrol or update programs, reverse redemptions, or close
statements. **Another provider's resource is 404** `target-not-found`,
exactly as one that does not exist, on every route — a program,
issuance, instrument, redemption, or statement addressed by id, and
every `…/programs/{id}/…` sub-route (Part 9 §9.3a rule 2: a narrower
ownership scope inside a granted place). A place outside the token's
`apx_places` grant remains 403 `insufficient-grant` (Part 9 §9.3), and
lists simply omit other providers' records. Operator scopes: `apx.validations:read` (all of the above,
read-only, across the place grant) and `apx.validations:manage`
(everything). Plate values never appear on validation resources; ticket
numbers and session references do, and are subject to Part 9 §9.6
minimization.

## 20.6 Billing statements

`billing.model` says who pays for a redemption: `merchantPays` (the
merchant is billed `unitPrice` per redemption, or the actual
`amountReduced` when `unitPrice` is absent), `operatorAbsorbs`
(statements are informational, `billableAmount` is zero), or `split`
(`merchantShare` percent of `amountReduced`).

- `GET /v1/validations/programs/{id}/statement?from=&to=` computes a
  **preview** (`statementStatus: preview`, no `id`) for any period.
- `POST /v1/validations/programs/{id}/statements` (**Idempotency-Key
  REQUIRED**) closes a period into an **immutable** statement
  (`closed`, with `id`, `lines[]`, `closedTime`, `closedBy`) that the
  operator's accounting system invoices from. Closed periods MUST NOT
  overlap (409 `statement-overlap`). Publishes
  `apx.validations.statement.closed.v1`.
- Redemptions reversed after closure (§20.4) appear as `credit` lines —
  `lineKind: credit`, a negative `billable`, and `originalStatement`
  naming the statement that billed them — on the next statement closed
  for the program, never as edits to the closed one. `billableAmount` is
  net of them and `creditCount` counts them; the preview shows the
  credits the period would carry. Closing the statement sets each
  reversal's `reversal.creditedOn`.
- A preview whose `from` is not before `to`, or a close whose
  `periodStart` is not before `periodEnd`, is 400 `invalid-request`.

APX does not invoice or take money for statements; that stays in the
operator's accounting system (Part 0 §0.4). A statement's `id` is the
reconciliation key.

## 20.7 Eventing

- `apx.validations.redeemed.v1` — a redemption was applied or reversed
  (data: `ValidationRedemption`).
- `apx.validations.program.status.v1` — a program was enrolled,
  suspended, resumed, or ended (data: `ValidationProgram`).
- `apx.validations.statement.closed.v1` — a period was closed (data:
  `ValidationStatement`, `lines[]` omitted).

Place binding (Part 8 §8.5) uses the resource's `place`. A
`apx.validations:redeem` subscription receives only events for its own
provider's programs (§20.5; Part 9 §9.6 rule 4).

## 20.8 Endpoints (summary)

| Operation | Scope |
|---|---|
| `POST /v1/validations/programs`, `PUT …/{id}` | `manage` |
| `GET /v1/validations/programs`, `GET …/{id}` | `read` or `redeem` (own) |
| `POST …/{id}/issuances` | `manage` or `redeem` (own) |
| `GET …/{id}/issuances`, `GET /v1/validations/instruments/{code}` | `read` or `redeem` (own) |
| `POST …/{id}/issuances/{issuanceId}/void`, `POST /v1/validations/instruments/{code}/void` | `manage` or `redeem` (own) |
| `POST /v1/validations/redemptions` | `manage` or `redeem` (own) |
| `GET /v1/validations/redemptions` (incl. `?command=`), `GET …/{id}` | `read` or `redeem` (own) |
| `POST …/{id}/reverse` | `manage` |
| `GET …/programs/{id}/statement` (preview), `GET …/programs/{id}/statements`, `GET /v1/validations/statements/{id}` | `read` or `redeem` (own) |
| `POST …/programs/{id}/statements` (close) | `manage` |

Every list is paginated in the APDS `PaginatedList` shape and constrained
to the caller's place grant.

---

<a id="21-credentials"></a>

# APX Part 21 — Credentials (optional class `apx-credentials`)

The lifecycle of access credentials — keycards, fobs, RFID tags and
transponders, mobile credentials, hangtags, and plates used as the
credential: issue, activate, suspend, resume, report lost, revoke,
replace, and the access-event history behind "why was I denied at the
gate?".

**APDS alignment.** APDS 4.1 models a credential as a `CredentialAssigned`
on an `AssignedRight` (`type: CredentialTypeEnum`, `identifier:
Reference`, `issuer`) and lets clients filter `/rights/assigned` by
`credential_type`/`credential_id`. It has no status, no replacement, no
physical media, and no access events. APX adds those as a
**CredentialRecord** (APDS owns the name `Credential`) and keeps APDS
authoritative for *what the credential entitles*: the record is
materialized onto the AssignedRight (§21.2), so a plain APDS lane or
enforcement client always sees the current credential. `credentialType`
is APDS's own enum; where a technology has no APDS value (magstripe,
NFC), implementations map to the nearest (`ticket`, `electronicID`) and
carry the exact technology in `extensions` — a Part 3 §3.3(8)
reconciliation item for the APDS committee, not something APX extends
on its own.

## 21.1 CredentialRecord and lifecycle

See schema. One record per physical or virtual credential: `credentialType`
(read technology), `credentialIdentification` (what the lane reads),
`credentialAssignedType`, `holder`/`account`, `assignedRights[]`,
`places[]`, `validity`, `activateOnStart`, `media` (form, serial, batch,
deposit), status, `replaces`/`replacedBy`, and the immutable
`statusHistory[]`. Server-assigned members (`id`, `version`,
`credentialStatus`, `suspension`, `replacedBy`, `statusHistory`,
`recordInfo`) sent in a create body are ignored, never stored.

**Lifecycle (normative).**

```
issued ──activate (or validity.start, when activateOnStart)──▶ active ◀──resume── suspended
                       │  ▲                 │
                       │  └───suspend───────┘
   active | suspended ──report-lost──▶ lost ──replace──▶ replaced (+ successor active)
   active | suspended | lost ──replace──▶ replaced (+ successor active)
   issued | active | suspended | lost ──revoke──▶ revoked
   validity.end passes ──▶ expired (server-side)
```

1. `revoked`, `expired`, and `replaced` are terminal. Any other
   transition request is 409 `credential-transition-illegal`; `replace`
   from a terminal state is 409 `credential-not-replaceable`. `replace`
   from `issued` is 409 `credential-transition-illegal` (a card never
   handed over is revoked and issued anew, not replaced);
   `credential-not-replaceable` is reserved for the terminal states.
2. `credentialIdentification` MUST be unique per `credentialType` across
   the implementation among non-terminal records (409
   `credential-identification-in-use`) — a lane cannot tell two cards
   with the same number apart, whichever place issued them. The 409's
   `detail` MUST NOT identify the holding record, its holder, or its
   place (§21.5). A terminal record's identification MAY be reissued;
   the new record `replaces` nothing.
3. `suspend` MAY carry `until`; the server resumes automatically at that
   instant and records the transition with actor `system`.
4. `expired` is applied server-side when `validity.end` passes and
   publishes `apx.credentials.status.v1` like any other transition.
5. Every transition appends `statusHistory[]` and publishes
   `apx.credentials.status.v1`.
6. **Activation at `validity.start`.** A record issued with
   `activateOnStart: true` moves `issued → active` server-side when
   `validity.start` is reached — at issue, when `validity.start` is
   absent or already past — with actor `system`, materialized per §21.2
   and published like any other transition. Without the flag a record
   stays `issued` until `activate` is called.
7. **Windows already over.** `POST /v1/credentials` with a
   `validity.end` that is not in the future, or that precedes
   `validity.start`, and `suspend` with an `until` that is not in the
   future are refused synchronously with 422 `request-unprocessable`,
   and nothing is written. The server never issues-and-expires, or
   suspends-and-resumes, in one call.

## 21.2 APDS materialization (normative)

1. While a record is `active`, the server MUST present it as a
   `CredentialAssigned` on each AssignedRight in `assignedRights[]`, with
   `type` = `credentialType` and `identifier` = a Reference to the
   CredentialRecord (`className: CredentialRecord`). A stock APDS client
   reading `/rights/assigned` — or filtering by
   `credential_type`/`credential_id` — therefore sees exactly the
   credentials that will open the gate.
2. On `suspend`, `report-lost`, `revoke`, and `expired`, the server MUST
   remove (or end) that `CredentialAssigned` so the AssignedRight no
   longer validates the credential; on `resume` it is restored. The lane
   MUST deny a credential that is not `active` from the instant of the
   transition — this is the security property the whole Part exists for.
3. `replace` is atomic from the AssignedRight's point of view: the
   successor's `CredentialAssigned` appears and the predecessor's
   disappears in one update (one `AssignedRightUpdated` event).
4. Part 13 `Account.cardNumber` and `Account.plates` remain the
   APDS-only summary; `Account.credentials[]` (additive) references the
   records when this class is claimed.

## 21.3 Replacement

`POST /v1/credentials/{id}/replace` issues the successor in one call:
inherits `holder`, `account`, `assignedRights`, `places`, and `validity`
unless the request overrides them; `credentialIdentification` is
server-assigned from pre-encoded stock when absent; `media` describes the
new item and `oldDepositStatus` settles the old deposit. The predecessor
becomes `replaced` with `replacedBy` set; the successor carries
`replaces`. **Idempotency-Key REQUIRED** — a retried "issue me a new card"
must not issue two.

**What the predecessor keeps (normative).** The predecessor retains
every field it had — `holder`, `account`, `assignedRights`, `places`,
`validity`, `media` — as the record of what the card was. Only
`credentialStatus` (→ `replaced`), `replacedBy`, `media.depositStatus`
(per `oldDepositStatus`), `statusHistory[]`, `version`, and `recordInfo`
change. Its `assignedRights[]` then records where it *was* materialized;
§21.2 materializes only `active` records, so it opens nothing.

## 21.4 Access events

Every presentation of a credential at a lane produces an access event —
`direction`, `outcome`, and when denied a `denialReason` from the open
`apx-access-denial-reasons` registry (Part 11; seeded values:
`credentialNotYetActive`, `credentialSuspended`, `credentialLost`,
`credentialRevoked`, `credentialReplaced`, `credentialExpired` — one per
non-active lifecycle state, `credentialNotYetActive` being `issued` —
plus `unknownCredential`, `passback`, `accountBalance`,
`outsidePlace`, `outsideValidity`).

1. When the presented value matches a CredentialRecord the event is a
   `CredentialAccessEvent` and names the record in `credential`.
2. When it matches none (`denialReason: unknownCredential`) there is no
   record to name: the event is an `AccessEvent` without `credential`,
   and `presented` (`credentialType`, `credentialIdentification` — what
   the reader read) is REQUIRED. `AccessEvent` is the superset shape;
   every `CredentialAccessEvent` is a valid `AccessEvent`.
3. `GET /v1/credentials/{id}/access-events` returns one credential's
   history (filters `since`, `until`, `outcome`, `direction`).
   `GET /v1/access-events` returns every attempt at the caller's granted
   places, including unmatched ones, filtered by `place`
   (comma-separated, subtree-inclusive), `lane`, `device`, `credential`,
   `outcome`, `denialReason`, `direction`, `since`, and `until` — the
   supervisor's "every denial at lane 1 tonight". Both are
   `apx.credentials:read` and grant-scoped.

`apx.credentials.access.v1` publishes each event (APDS EventTypeEnum has
no access events — this fills the gap the same way
`apx.data.observation.created.v1` does for sensors). Passback state
itself stays in Part 17 (`GET /v1/credentials/{id}/passback` and the
`resetPassback`/`forceIn`/`forceOut` commands); an access denied for
passback carries `denialReason: passback` here and the corrective
action lives there.

## 21.5 Privacy

`credentialIdentification`, `media.serialNumber`, and access events are
personal data when joined to a holder; Part 9 §9.6 applies (scope
minimization — identification values only under `apx.credentials:*`,
retention published and purgeable). Implementations MUST NOT expose an
"is this identification in use" oracle: `POST /v1/credentials` returns
409 `credential-identification-in-use` only to `apx.credentials:manage`
callers within their place grant. Because uniqueness is
implementation-wide (§21.1 rule 2), that 409 may be caused by a record
outside the caller's grant; it then says only that the value is taken
for that `credentialType` — its `detail` MUST NOT name the holding
record, holder, place, or state — and a read (`GET /v1/credentials?identification=`)
under the caller's grant still returns an empty 200 (Part 9 §9.3a).
`presented` values on unmatched access events are personal data on the
same terms.

## 21.6 Eventing

- `apx.credentials.status.v1` — any lifecycle transition (data:
  `CredentialRecord`).
- `apx.credentials.access.v1` — a lane access attempt (data:
  `AccessEvent`; a `CredentialAccessEvent` whenever a record matched,
  §21.4).

Place binding (Part 8 §8.5): `places[]` for status events; the event's
`place` for access events.

## 21.7 Endpoints (summary)

| Operation | Scope |
|---|---|
| `POST /v1/credentials`, `POST …/{id}/activate`, `/suspend`, `/resume`, `/report-lost`, `/revoke`, `/replace` | `apx.credentials:manage` |
| `GET /v1/credentials`, `GET …/{id}`, `GET …/{id}/access-events`, `GET /v1/access-events` | `apx.credentials:read` |
| `GET …/{id}/passback` (Part 17) | `apx.control:read` (unchanged) |

Every list is paginated in the APDS `PaginatedList` shape and constrained
to the caller's place grant.

---

<a id="22-valet"></a>

# APX Part 22 — Valet (optional class `apx-valet`)

Valet custody: taking a vehicle from its owner, knowing where it and its
keys are, bringing it back when asked — from a text, an app, a web page,
a voice bot, a kiosk, or the stand — and handing it to the right person
with the evidence to answer "that scratch wasn't there".

**A net-new surface.** APDS 4.1 has no valet entity; its only mention is
the `valetOnly` operating restriction. Everything is still built to APDS
conventions: the stay is a native `Session` (opened at drop-off, closed
at handback, priced by the place's rates), the claim ticket MAY be an
`AssignedRight`, the parked position is a `Space`, the customer a
`Contact`/`RightHolder` or Part 13 `Account`, money rides on the Session
and the Part 13 payment surface. APX adds only what APDS lacks: custody,
keys, condition evidence, the retrieval queue, staging, and handback, as
a `ValetTicket` that **references** the Session and never restates it.

## 22.1 ValetTicket and lifecycle

See schema. One ticket per custody: `vehicle`, `customer` (minimized),
`dropOff` (time, lane, attendant, mileage, fuel, key tag, items left,
condition report), `storage` (space or zone, key location), `retrieval`
(channel, requested/scheduled time, ETA, promised time, staging),
`handback` (verification, condition report), status, and the immutable
`statusHistory[]`.

**Lifecycle (normative).**

```
dropped ──park──▶ parked ──retrieve──▶ requested ──pickup──▶ retrieving ──stage──▶ staged ──handback──▶ handedBack ──▶ closed
   │               ▲  ▲                    │                     │                   │
   │               │  └──cancel-retrieval──┴─────────────────────┘                   │
   │               └──────────────────park (re-park)─────────────────────────────────┘
   │                             requested ──stage──▶ staged   (pickup is optional)
   ├──handback (before the car moved)──▶ handedBack
   └──cancel──▶ cancelled (custody never taken; terminal)
```

1. `park` is valid from `dropped`, MAY be repeated while `parked` to
   update `storage`, and re-parks a `staged` car the customer is not
   collecting yet (`staged → parked`: new `storage`, staging cleared,
   `retrieval.cancelledTime` set with `cancelReason: re-parked`).
   `retrieve` from `parked` — from `dropped` it is 409
   `valet-vehicle-not-located` (nobody knows where the car is yet), and
   a repeat while `requested` or `retrieving` is answered 200 with the
   current ticket (§22.3 rule 4). `pickup` from `requested`;
   `cancel-retrieval` from `requested | retrieving`; `stage` from
   `requested | retrieving`; `handback` from `staged`, or from `dropped`
   for a customer who changes their mind before the car moved; `cancel`
   from `dropped` only. Any other transition is 409
   `valet-transition-illegal`.
2. `requested → retrieving` is the runner picking the car up. The
   operator's app records it with `POST …/{id}/pickup`, which sets
   `retrieval.retrievingBy`; an implementation MAY instead infer it from
   its own runner assignment. The step is optional — `stage` directly
   from `requested` is conforming — but where it is recorded, `pickup`
   is the only API route that records it.
3. `handedBack → closed` is server-side when the APDS Session settles
   (paid, or comped); both are settled states. `cancelled` is terminal:
   `POST …/{id}/cancel` (with a `reason`) ends a drop-off whose custody
   was never taken — the driver left for the self-park deck
   mid-walk-around — and the server SHOULD close the APDS Session it
   opened at no charge.
4. Every transition appends `statusHistory[]` and publishes
   `apx.valet.ticket.status.v1`; `retrieve` additionally publishes
   `apx.valet.retrieval.requested.v1`.
5. **Server-written members (normative).** `ValetTicket` is both the
   create body and the resource. On `POST /v1/valet/tickets` the server
   MUST ignore any `storage`, `retrieval`, `handback`, `statusHistory`,
   or `recordInfo` the client sends — they are written only by `park`,
   `retrieve`, `pickup`, `stage`, `handback`, and the server itself, and
   `statusHistory[]` is the authoritative audit (Part 4 §4.2). `session`
   and `assignedRight` MAY be sent when the operator opened them before
   the drop-off call; otherwise the server opens the Session.
6. **Concurrency.** `park` accepts `If-Match` with the version last read
   (Part 4 §4.2a): two runners moving the same car get one 200 and one
   409 `version-conflict`, rather than a ticket pointing at a space the
   car is not in. Without `If-Match` it is last-writer-wins.

## 22.2 Drop-off and condition evidence (normative)

1. A drop-off SHOULD carry a `conditionReport`: free-text `notes`,
   structured `damage[]` entries (area, description, severity, image),
   and `imageLinks[]` for the walk-around or scan captures. Imagery is
   carried as access-controlled links under the same grant as the ticket
   (Part 9 §9.6), never inline.
2. `customerAcknowledged` records that the customer saw the report
   (signature, tap, spoken confirmation via a bot). Implementations
   SHOULD capture it; a damage claim is answered by comparing the
   drop-off and handback reports, and acknowledgement is what makes the
   drop-off report binding.
3. The drop-off condition report is immutable once the ticket leaves
   `dropped`; corrections are new `damage[]` entries with a later
   `recordedTime`, never edits. `POST …/{id}/condition` is the route:
   it appends each `damage[]` entry to `dropOff.conditionReport.damage[]`
   with a server-set `recordedTime` and `recordedBy`, appends
   `imageLinks[]`, and records `notes` as a `statusHistory[]` entry
   without changing `valetStatus`. It is legal in every state except
   `closed` and `cancelled`, and never alters the report's original
   entries, `notes`, or `customerAcknowledged`.

## 22.3 Retrieval and the queue

`POST /v1/valet/tickets/{id}/retrieve` asks for the car: `channel` names
where the request came from (`sms`, `app`, `web`, `voiceBot`, `kiosk`,
`attendant`, `callCenter`), `requestedFor` schedules a pickup, and
`interaction` carries an opaque Part 17 §17.6 interaction id when a bot
or support channel raised it — no telephony identifiers in the contract.

1. The server MUST set `retrieval.etaMinutes` and `promisedTime` on
   every request. `promisedTime` is `requestedFor` for a scheduled
   pickup, else `requestedTime` plus the operator's estimate;
   `etaMinutes` is the whole minutes from now until `promisedTime`,
   never negative (`max(0, floor(promisedTime − now))`), recomputed on
   every read and event — so a 07:30 pickup booked at 22:10 reads 560
   and counts down. The customer-facing read (§22.5) is how a text, app,
   or bot shows "your car will be ready in 8 minutes".
2. `GET /v1/valet/queue?place=` returns tickets in `requested`,
   `retrieving`, or `staged` ordered by `promisedTime` — the runner
   board. Scheduled pickups appear once inside `horizonMinutes`.
3. `stage` records the car at the staging lane and SHOULD notify the
   customer's `contactChannel`; how (push, SMS, callback) is the
   implementer's — APX carries the state, not the message.
4. **Repeated requests.** Guests text twice. A `retrieve` while the
   ticket is already `requested` or `retrieving` returns 200 with the
   current ticket and ETA; nothing is appended to `statusHistory[]`,
   `apx.valet.retrieval.requested.v1` is not republished, and a
   `requestedFor` in the repeat is ignored (rescheduling is
   `cancel-retrieval` then `retrieve`). From `staged`, `handedBack`,
   `closed`, or `cancelled` it remains 409 `valet-transition-illegal`.

## 22.4 Handback (normative)

1. The attendant MUST verify the claimant (`verificationMethod`: ticket,
   phone, one-time code, ID, or attendant recognition). A failed
   verification is 403 `valet-verification-failed` and MUST be recorded
   in `statusHistory[]` (attempted handback, method, actor) — a car
   handed to the wrong person is the incident the audit exists for.
   `verificationValue` is checked and discarded, never stored.
2. The handback `conditionReport` follows §22.2; `mileage` at handback
   against `dropOff.mileage` is the joyride check.
3. Money is not taken here. The APDS Session is closed and paid through
   the place's normal flow (pay station, Part 13 take-payment, a payment
   link, or an account); the valet fee is a rate line on the Session.
   Gratuities are out of scope for this edition.

## 22.5 Customer scope (normative)

`apx.valet:request` is the customer-facing scope for texts, PWAs, web
pages, and voice bots. A token carrying it (and not `:read`/`:manage`)
is confined to the ticket(s) it was minted for (the implementation binds
the token to the ticket at drop-off, e.g. via the claim link or code):
it MAY read those tickets, `retrieve`, and `cancel-retrieval`; it MUST
NOT list, park, pick up, stage, cancel, amend the condition report, or
hand back. Every ticket it receives — from `GET …/{id}`, `retrieve`, and
`cancel-retrieval`, and on any event subscription it holds — is
**minimized** to exactly these members:

- `id`, `version`, `place`, `ticketNumber`, `vehicle`, `valetStatus`,
  `extensions`;
- `customer.displayName`;
- `dropOff.time`;
- `retrieval.requestedTime`, `requestedFor`, `channel`, `etaMinutes`,
  `promisedTime`, `stagingLane`, `stagedTime`, `cancelledTime`;
- `handback.time`.

Everything else is omitted — `storage`, `statusHistory[]` (its `actor`
values are attendant principals), every other `dropOff` member
(including `keyTag` and the condition report), `retrieval.requestedBy`,
`retrievingBy`, `interaction`, the rest of `customer` and `handback`,
`session`, `assignedRight`, and `recordInfo`. A bot acting for the
customer uses the same scope and sets `channel: voiceBot` with an
`interaction` id.

**Tickets the caller may not see (normative, Part 9 §9.3a).** An
operator token (`apx.valet:read` or `:manage`) addressing a ticket at a
place outside its `apx_places` grant receives 403 `insufficient-grant`.
An `apx.valet:request` token addressing any ticket it is not bound to —
whether that ticket exists or not — receives 404 `target-not-found`, so
a claim link cannot be used to enumerate tickets.

## 22.6 Privacy

Plates, customer handles, and condition imagery are personal data under
Part 9 §9.6. `customer.contactChannel.handle` is opaque or masked;
implementations MUST NOT carry raw phone numbers or e-mail addresses on
the ticket (the contact system holds them, referenced by `contact`). A
server receiving one SHOULD refuse the request with 422
`personal-data-not-permitted` (`detail` naming the member) rather
than store it or mask it silently, so the integration that sent it
finds out.
Retention for condition imagery MUST be published; the drop-off report
SHOULD be retained at least as long as the operator's damage-claim
window.

## 22.7 Eventing

- `apx.valet.ticket.status.v1` — every transition (data: `ValetTicket`).
- `apx.valet.retrieval.requested.v1` — a retrieval was requested, with
  channel, ETA, and promised time (data: `ValetTicket`).

Place binding (Part 8 §8.5) uses `ValetTicket.place`. A
`apx.valet:request` subscription (where offered) receives only its own
ticket's events.

## 22.8 Endpoints (summary)

| Operation | Scope |
|---|---|
| `POST /v1/valet/tickets`, `POST …/{id}/park`, `/pickup`, `/stage`, `/handback`, `/cancel`, `/condition` | `apx.valet:manage` |
| `GET /v1/valet/tickets`, `GET /v1/valet/queue` | `apx.valet:read` |
| `GET …/{id}` | `apx.valet:read`, or `apx.valet:request` (own, minimized) |
| `POST …/{id}/retrieve`, `/cancel-retrieval` | `apx.valet:manage`, or `apx.valet:request` (own) |

Every list is paginated in the APDS `PaginatedList` shape and constrained
to the caller's place grant.

## 22.9 Charging (Part 23)

`ValetTicket.charging` (optional, additive) records that the guest asked
for the car to be charged — `requested`, `targetStateOfChargePercent`,
`instructions` — and, once the valet plugs in, references the Part 23
`ChargingSession`, which points back via `valetTicket`. Valet charging
settles on the stay's APDS Session by default. The customer read (§22.5)
exposes `charging.requested` and `charging.session`; the charge itself is
read under `apx.charging:status`.

Where an external pairing engine matched the ticket to its Session, the
ticket carries the decoration `apds-ext:apx:match-evidence@1.0` (Part 5
§5.8), saying how the match was made. A valet stay inside a facility is
an area Session: its `apds-ext:apx:session-detail@1.0` names the visit as
`parent` and closes with `endReason: valetHandback` (Part 5 §5.8 rules 3
and 6).

---

<a id="23-charging"></a>

# APX Part 23 — EV Charging (optional class `apx-charging`)

The parking side of charging: which chargers are free, what is actually
in the bay in front of each one, the charge a vehicle took during its
stay — energy, state, the idle time after it finished, what it cost, and
how that cost is collected — and the control an operator needs when a
cable will not release.

**What APDS already has.** APDS 4.1 describes the static infrastructure
completely: `ElectricChargingEquipment` is a `HierarchyElement` (a
`SupplementalFacility` carrying an `ElectricChargingPoint`), whose
`Connector`s carry IEC connector type, format, charging mode, and power;
`RefillPoint` carries accepted `AuthenticationAndIdentificationEnum`
methods, `reservability`, `deliveryUnit` (`kWh`), and a `refillPointIndex`
that exists "to link static and dynamic publications"; `PlaceInformation`
counts `evChargingPoints`; `electricVehicleOnly` is an operating
restriction; a `Space` is the bay. APX references all of it and redefines
none of it.

**What APDS does not have, and what this Part adds.** Nothing dynamic: no
charger availability, no charging session or energy record, no idle
handling, and no link from a charge to the parking `Session`. This Part
adds a `ChargingPointStatus` overlay (the dynamic half of APDS's
static/dynamic pair), a `ChargingSession` that references the APDS
Session, the bridge's event ingest, settlement, a customer scope, and the
bay-presence fusion that lets the parking system say "there is a car in
the EV bay and it is not plugged in".

**What this Part is not.** It is not a charger protocol and not a roaming
protocol. OCPP (charger ↔ charging-network back end) and OCPI (network ↔
network) already exist and are used by the charging industry; the
operator's *bridge* — its integration with its charging network — speaks
them. APX carries their identifiers (`evseId`, `ocppTransactionId`,
`ocpiSessionId`, the CDR id) **opaquely** so records reconcile, and
borrows their vocabulary where a parking consumer must understand it
(`chargingStatus` maps to OCPP 2.0.1 `chargingState`; `availability`
follows the OCPI EVSE status list). Smart charging and load management,
tariff publication, and roaming clearing stay with the charging network.

## 23.1 Charging points and live status

`GET /v1/charging/points?place=&availability=` and `GET …/{id}` return
`ChargingPointStatus`, keyed by Reference to the APDS
`ElectricChargingEquipment` — the same pattern as Part 6 `DeviceStatus`.
It carries `refillPointIndex` and `evseId` so consumers can join it to
the static APDS record and to the charging network's record, point-level
and per-connector `availability`, `currentSession`, `reservedFor` (a
Part 14 AssignedRight), and `bay` (§23.3).

1. `availability` (closed): `available`; `occupied` — cable connected,
   no energy flowing (plugged in, suspended, complete, or idle);
   `charging`; `reserved`; `blocked` — the bay holds a vehicle that is
   not plugged in, or is obstructed (derived from `bay.presence`, §23.3);
   `inoperative` (planned); `outOfOrder` (fault); `unknown`. The point's
   value is the busiest of its connectors, except that a `blocked` bay
   (§23.3) overrides `available` and `unknown` — never a connected cable:
   a point that is `occupied` or `charging` stays so whatever the camera
   sees around it.
2. The bridge writes availability with `PUT …/{id}/status`
   (`apx.charging:manage`), replacing the connector states,
   `currentSession`, `reservedFor`, and `lastCommunication`. It MUST NOT
   write `bay`; a `bay` in the body is ignored. `connectorIndex` is the
   1-based position in the static APDS `connectors[]`.
3. A change of point-level `availability` or of `bay.presence` publishes
   `apx.charging.point.status.v1` (data: `ChargingPointStatus`).
4. Static descriptive fields (connector type, rated power, usage type)
   are NOT repeated here; `connectors[].connectorType` is an optional
   echo for consumers that do not hold the hierarchy. `outOfOrder` SHOULD
   also raise a Part 7 `deviceFault` alert where `apx-alerts` is
   implemented.

## 23.2 ChargingSession and lifecycle

See schema. One session per charge: the point and connector, the
parking `Session` it happened inside, the `AssignedRight` (a charging
reservation or the permit that entitles charging), `vehicle` (minimized),
`authorization`, the `timeline`, `energy`, `stopReason`, the
`idlePolicy` snapshot, `cost`, `settlement`, `network` identifiers, and
the immutable `statusHistory[]`.

**Lifecycle (normative).**

```
authorized ──pluggedIn──▶ pluggedIn ──chargingStarted──▶ charging ◀──resumed──┐
    │                         │                              │ ──suspended──▶ suspended
    │                         │                              │                    │
    │                         ├────────chargingEnded─────────┴────────────────────┴──▶ complete
    │                         │                                                          │
    │                         │            (grace elapses, server-side) ◀────────────────┤
    │                         │                        │                                 │
    │                         │                        ▼                                 │
    │                         │                       idle ──unplugged──▶ unplugged ◀──unplugged
    │                         └────unplugged (no energy)───────────────────▶  │
    │                                                                          └──settled (server-side)──▶ closed
    └──cancelled──▶ cancelled

any state from authorized to idle ──fault──▶ faulted ──unplugged──▶ unplugged
                                              faulted ──cancelled──▶ cancelled   (only if never plugged in)
```

Transition table (normative; the diagram is a rendering of it):

| Event / cause | From | To |
|---|---|---|
| `pluggedIn` | `authorized` | `pluggedIn` |
| `chargingStarted` | `pluggedIn` | `charging` |
| `suspended` (`suspendedBy`) | `charging` | `suspended` |
| `resumed` | `suspended` | `charging` |
| `chargingEnded` (`stopReason`) | `pluggedIn`, `charging`, `suspended` | `complete` |
| grace elapsed (server) | `complete` | `idle` |
| `unplugged` | `pluggedIn` (no energy flowed), `complete`, `idle`, `faulted` | `unplugged` |
| `fault` | `authorized` … `idle` | `faulted` |
| `cancelled` | `authorized`; `faulted` with no `pluggedInTime` | `cancelled` |
| settled (server) | `unplugged` | `closed` |

1. Transitions are driven by `POST …/{id}/events` (`ChargingEvent`, the
   APX counterpart of an OCPP 2.0.1 `TransactionEvent`) exactly as the
   table says. `meterValue` updates `energy` without a transition and is
   valid in `pluggedIn`, `charging`, `suspended`, `complete`, and `idle`.
   Anything not in the table is 409 `charging-transition-illegal`. Events
   MUST be applied in `time` order.
2. `complete → idle` is **server-side**: when `idlePolicy.graceMinutes`
   has elapsed since `timeline.chargingEndedTime` and the cable is still
   connected, the server sets `timeline.idleStartedTime`, transitions,
   and publishes `apx.charging.idle.started.v1` in addition to the
   status topic. A `graceMinutes` of 0 means idle begins at `complete`;
   an absent `idlePolicy` means the operator does not track idle and the
   session stays `complete` until unplugged.
3. `unplugged → closed` is server-side on settlement (§23.5). `closed`
   and `cancelled` are terminal; `faulted` exits via `unplugged`, or via
   `cancelled` when the vehicle was never plugged in.
4. Every transition appends `statusHistory[]` (state, time, actor,
   detail) and publishes `apx.charging.session.status.v1`.
5. `chargingStatus` maps to OCPP 2.0.1 `chargingState` as follows
   (informative, for bridge authors): `pluggedIn` = EVConnected;
   `charging` = Charging; `suspended` = SuspendedEV / SuspendedEVSE;
   `complete` and `idle` = EVConnected after energy delivery ended;
   `unplugged` = Idle. The split of EVConnected into three APX states is
   the point: the parking side cares whether the car is waiting, done, or
   overstaying.

`POST /v1/charging/sessions` opens the session (`Idempotency-Key`
REQUIRED). It is 409 `charging-point-unavailable` when the point or
connector is `inoperative`, `outOfOrder`, `reserved` for a different
right, or already serving an active session; 422 when `settlement.mode`
is `parkingSession` and no `parkingSession` reference is given.

## 23.3 Bay presence and fusion (normative)

The charger knows whether a cable is connected. It does not know whether
a vehicle is in the bay. An overhead camera (computer-vision occupancy),
a ground sensor, or an attendant knows whether something is in the bay,
but not whether it is charging. The parking system is where the two
meet, and the questions that matter — *is an ICE vehicle blocking the
charger?* *is an EV parked there without plugging in?* *is the bay
coned off?* — are only answerable by fusing them.

1. Presence sources report with `POST …/points/{id}/bay`
   (`ChargingBayPresence`, `Idempotency-Key` REQUIRED): `presence`,
   `source`, the APDS `Observation` carrying the evidence,
   `observedTime`, the minimized `vehicle` seen (plate when the camera
   has LPR; `evCapable` when make/model or badge recognition can tell),
   `confidence`, and an access-controlled `imageLink`.
2. The server MUST fuse the latest presence report with the charger's
   connector state into `ChargingPointStatus.bay.presence` and the
   point's `availability`:

   | Camera / sensor says | Charger says cable | `bay.presence` | `availability` |
   |---|---|---|---|
   | nothing in bay | not connected | `empty` | connector state (`available`…) |
   | nothing in bay | connected | `unknown` (sources disagree) | connector state |
   | vehicle | connected | `vehiclePluggedIn` | connector state (`occupied`/`charging`) |
   | vehicle | not connected | `vehicleNotPluggedIn` | `blocked` |
   | obstruction / straddling | not connected | `blocked` | `blocked` |
   | obstruction / straddling | connected | `blocked` | connector state (a connected cable wins) |
   | no report | any | `unknown` | connector state |

   `source` names the report that decided the row; `sinceTime` is when
   the current `presence` began and is the dwell clock.
3. A `vehicleNotPluggedIn` presence that persists beyond the place's
   grace (operator configuration) SHOULD raise the proposed
   `evBayNotCharging` alert (Part 7); with `vehicle.evCapable: false` the
   existing seeded `iceInEvSpace` alert applies instead. A `blocked` bay
   SHOULD raise the proposed `evBayBlocked` alert. A `vehicleNotPluggedIn`
   condition MAY be handed to Part 19 as a `restrictedSpace` violation
   candidate under the place's enforcement policy (a `blocked` bay has no
   vehicle to cite); the Observation referenced in `bay` is the evidence.
4. Presence with a plate is personal data (Part 9 §9.6): `bay.vehicle`
   and `bay.imageLink` appear only under `apx.charging:read` /
   `:manage`; the customer scope (§23.6) receives `presence` alone.
5. Where the bay camera reads a plate and the charging point free-vends
   (authorization `unlimitedAccess`), the server MAY use that plate to
   attribute the ChargingSession to the parking Session the plate
   entered on (`authorization.methodDetail` records that this was
   done). This is how "one bill at exit" works in a garage whose
   chargers have no authentication of their own.

## 23.4 Energy and meter values

`energy` carries the charger's cumulative meter at start and stop (Wh,
as chargers report them), `deliveredKwh` (APDS `DeliveryUnitEnum`
`kWh`), current and peak power, and the vehicle's state of charge when
the vehicle reports it (ISO 15118). `meterValue` events update
`deliveredKwh`, `currentPowerKw`, `peakPowerKw`, `stateOfChargePercent`,
and `lastMeterTime`; `chargingEnded` fixes `meterStopWh` and the final
`deliveredKwh`. Servers MUST NOT invent `deliveredKwh` from time and
rated power — an absent value means the bridge did not report it.
`energy.targetStateOfChargePercent` is the customer's or valet's request
(§23.8) and, where the charger supports it, the `targetReached` stop
reason.

## 23.5 Cost and settlement (normative)

`cost` is what the parking system knows: `energy`, `time`, `idle`,
`fees`, `total` as APDS `AmountInCurrency`, `tariffReference` (opaque),
`pricedAt`. Absent components are unknown, not zero.

`settlement.mode` says how the money is collected:

1. **`parkingSession`** — the charge is a line on the APDS `Session`
   (priced by the place's rates like any other segment) and is paid with
   the parking through the operator's normal flow (pay station, Part 13
   take-payment, a payment link, an account). `parkingSession` is
   REQUIRED. The ChargingSession `closed` follows the APDS Session's
   settlement; `POST …/{id}/payment` is 409
   `charging-settlement-conflict`.
2. **`directPayment`** — the charge is settled on its own by a Part 13
   payment. `POST …/{id}/payment` attaches the `payment` Reference and
   moves an `unplugged` session to `closed`. Attaching before `unplugged`
   (the final cost is not known) or to a `closed` session is 409
   `charging-settlement-conflict`.
3. **`chargingNetwork`** — the charging network (or its roaming partner)
   bills the driver. The parking side records energy, state, and idle,
   and `cost` where the network shares it; `POST …/{id}/payment` MAY
   record the network's charge detail record id in
   `settlement.externalReference`; `closed` follows `unplugged` once the
   record is final.

**Idle charges.** `idlePolicy` is snapshotted at authorization and is
never re-priced retroactively. `cost.idle` accrues from
`timeline.idleStartedTime` at `idleRate` per `idleRatePer` while the
session is `idle`, billed per started unit (a 12 min 49 s idle at a
per-minute rate is 13 minutes); where `appliesToSettlement` is false the operator
records idle time but the charging network bills it. The customer read
(§23.6) MUST expose `idlePolicy` so an app or text can say "10 minutes
grace, then $0.40 per minute" before the fact.

## 23.6 Customer scope (normative)

`apx.charging:status` is the customer-facing scope for apps, texts, web
pages, and voice bots. A token carrying it (and not `:read`/`:manage`)
is confined to the session(s) it was minted for (bound at authorization,
e.g. via the app account or a claim link) and to the place's charger
availability. It MAY read its own `ChargingSession` and the place's
`ChargingPointStatus` list; it MUST NOT list sessions, write status,
report events, or attach payments. The reads are **minimized** by an
omit-list; every field not listed is returned as for operator scopes:

- On `ChargingSession`, servers MUST omit `vehicle.credentialIdentification`,
  `authorization.credential`, `authorization.contractId`,
  `authorization.methodDetail`, `authorization.authorizedBy`, `network`,
  `statusHistory`, `parkingSession`, `space`, and `evseId`.
- On `ChargingPointStatus`, servers MUST omit `bay.vehicle`,
  `bay.imageLink`, `bay.observation`, `currentSession`, and
  `connectors[].currentSession`.

What remains is what an app needs: status, `energy` and state of
charge, `timeline`, `idlePolicy`, `cost`, `settlement.mode`, the point
and connector, and each point's `availability` and `bay.presence`.

## 23.7 Occupancy and discovery integration

1. Part 5 `OccupancySnapshot` gains an optional `evCharging` block
   (`pointsTotal`, `available`, `charging`, `occupiedNotCharging`,
   `blocked`, `inoperative`), derived from `ChargingPointStatus` under
   the element. Implementations claiming `apx-charging` MUST populate it
   where they hold point status; others omit it. "Is there a free
   charger" is thereby answered by the same read as "is there a free
   space", and `apx.data.occupancy.v1` carries it.
2. Part 16 discovery lists `apx-charging` in `conformanceClasses` and the
   `/v1/charging/*` endpoints for a client holding an `apx.charging`
   scope, under the §16.2 soundness rule.

## 23.8 Valet cross-link

Part 22 `ValetTicket` gains an optional `charging` block: `requested`,
`targetStateOfChargePercent`, `instructions`, and `session` (the
ChargingSession the valet opened when they plugged the car in). The
ChargingSession's `valetTicket` points back. Valet charging is
`settlement.mode: parkingSession` by default — it rides on the valet
stay's APDS Session. The valet-scope customer read (Part 22 §22.5)
exposes `charging.requested` and `charging.session`; the customer then
reads the session under `apx.charging:status`.

## 23.9 Charger control via Part 6

Physical actions on a charger are Part 6 commands, never Part 23
writes (Part 17 §17.4 rule 1). Proposed `apx-command-types` entries:

| Command type | Target | Parameters | What it does |
|---|---|---|---|
| `startCharging` | `ElectricChargingEquipment` | `connectorIndex`; `chargingSession` (Reference), or an `authorization` object (the `ChargingSession.authorization` shape) for the server to open one | Remote start for a plugged-in vehicle whose own authorization failed |
| `stopCharging` | `ElectricChargingEquipment` | `connectorIndex`; `chargingSession` | Remote stop; the bridge reports `chargingEnded` with `stopReason: remote` |
| `unlockConnector` | `ElectricChargingEquipment` | `connectorIndex` | Release a cable the charger will not let go of |

The bridge translates them to the charger protocol. `confirmationLevel`
applies as in Part 6 §6.1: an agent may say "the cable is released" only
at `physicallyConfirmed` (the charger reported the connector unlocked or
the bay camera saw the cable move); at `deviceAcknowledged` it may say
"the unlock was accepted". Part 17 resolution contexts SHOULD offer these
as allowed actions when the lane or place has a charging session in
`faulted`, `idle`, or `complete`.

## 23.10 Enforcement hook

`apx.charging.idle.started.v1` is the hand-off: an enforcement or
notification consumer starts the clock at `idleStartedTime` and, when
`idlePolicy.maxIdleMinutes` elapses, MAY record a Part 19 violation
candidate under the proposed `apx-violation-types` entry
`chargingBayIdle` ("a vehicle remained connected in a charging bay after
charging completed beyond the posted idle limit"), evidenced by the
ChargingSession and the bay Observation. An ICE vehicle or an unplugged
EV in the bay (§23.3) is the existing `restrictedSpace` type. Whether
idle is a fee (§23.5), a violation, or both is the place's enforcement
policy (Part 19 §19.10) and posted signage (§19.11) — APX carries the
facts, the policy decides.

## 23.11 Privacy

Plates, contract ids, and bay imagery are personal data under Part 9
§9.6. `authorization.contractId` is masked or opaque; raw payment
credentials never appear. `bay.vehicle` and `bay.imageLink` are limited
to operator scopes and their retention MUST be published; the
customer scope receives no plate, credential, contract, or network
identifiers. Bay observations are Observations and follow Part 13 §13.3.

## 23.12 Eventing

Proposed `apx-topics` entries:

- `apx.charging.session.status.v1` — every ChargingSession transition
  (data: `ChargingSession`).
- `apx.charging.idle.started.v1` — a session entered `idle`; carries the
  idle policy and the vehicle summary (data: `ChargingSession`) — drives
  the customer's "move your car" text and enforcement.
- `apx.charging.point.status.v1` — a point's availability or bay
  presence changed (data: `ChargingPointStatus`).

Place binding (Part 8 §8.5) uses `ChargingSession.place` and
`ChargingPointStatus.place`. A `apx.charging:status` subscription (where
offered) receives only its own session's events.

## 23.13 Endpoints (summary)

| Operation | Scope |
|---|---|
| `GET /v1/charging/points?place=`, `GET …/points/{id}` | `apx.charging:read`, or `apx.charging:status` (minimized) |
| `PUT …/points/{id}/status`, `POST …/points/{id}/bay` | `apx.charging:manage` |
| `POST /v1/charging/sessions`, `POST …/{id}/events`, `POST …/{id}/payment` | `apx.charging:manage` |
| `GET /v1/charging/sessions` | `apx.charging:read` |
| `GET …/sessions/{id}` | `apx.charging:read`, or `apx.charging:status` (own, minimized) |
| `startCharging`, `stopCharging`, `unlockConnector` | Part 6 `POST /v1/commands`, `apx.control:execute` |

Every list is paginated in the APDS `PaginatedList` shape and constrained
to the caller's place grant.

## 23.14 Registry entries

This Part's vocabularies are registered (Part 11):

| Registry | Entries |
|---|---|
| `apx-conformance-classes` v7 | `apx-charging` — requires the base classes; §23.9 requires `apx-control` |
| `apx-topics` v11 | `apx.charging.session.status.v1`, `apx.charging.idle.started.v1`, `apx.charging.point.status.v1` (§23.12) |
| `apx-command-types` v4 | `startCharging`, `stopCharging`, `unlockConnector` (§23.9, Part 6 §6.8) |
| `apx-alert-types` v4 | `evBayNotCharging`, `evBayBlocked` (§23.3, Part 7 §7.5) |
| `apx-violation-types` v2 | `chargingBayIdle` (§23.10, Part 19 §19.12) |
| Part 12 problem types | `charging-transition-illegal` (409), `charging-point-unavailable` (409), `charging-settlement-conflict` (409) |

The conformance requirements are Annex A.21 (APX-EVC-01 to -10).

## 23.15 Out of scope (this edition)

Charger-to-network protocol behaviour (OCPP), roaming and clearing
(OCPI/OICP), tariff publication, smart charging and load management,
reservation of a specific connector beyond the Part 14 AssignedRight
reference, vehicle-to-grid, and battery swap. Fleet depot charging
schedules are a candidate for a later Part.

---

<a id="24-plate-lists"></a>

# APX Part 24 — Plate Lists (optional class `apx-plate-lists`)

Lists about a plate that someone other than the vehicle's keeper
maintains: a city's scofflaw list, an operator's watch list, the plates
allowed to park without a right of their own, the enforcement and
service vehicles whose visits are recorded but never enforced. A list
says what to do when a listed plate is seen at a place.

**What APDS has.** Nothing. A plate list is not a credential (Part 21
models the media a customer holds) and not a right (Part 14 models what
a holder is entitled to). The vehicle credential itself is the APDS
`CredentialTypeEnum` pair a lane reads, reused by `$ref`.

**What this Part adds.** `PlateList` (the policy: category, owner,
places with per-place flags), `PlateListEntry` (one credential on a
list), the routes under `/v1/plate-lists`, the Part 5 change feed on
both, and the topic `apx.platelists.changed.v1`. Screening — matching a
read against the lists and deciding what happens — stays with the
implementation and its operator; Part 7 alerts and Part 6 commands carry
the outcome.

## 24.1 Lists (normative)

1. A list's `category` is a definedValue of `apx-plate-list-categories`
   (Part 11) or an implementer category: `allow`, `watch`, `scofflaw`,
   `enforcementVehicle`.
2. `places[]` says where the list applies and what it does there.
   `enforced` — a match counts at this place (`allow`: as compliant;
   `scofflaw`, `watch`: as enforceable). `alertOnArrival` — a match
   raises the category's arrival alert (§24.4). `refuseService` — a
   match refuses service at a lane (§24.4). A list with no `places`
   applies nowhere; its entries are still kept.
3. A list belongs to the `apx_org` that created it (`owner`). Visibility
   follows Part 9 §9.3a: a list is visible to a token whose grant covers
   any of its `places`, and to tokens of its owner. Every
   `places[].place` written MUST lie inside the writer's grant (403
   `insufficient-grant`).
4. `status` `suspended` or `retired` stops the list applying anywhere;
   its entries remain readable for audit.
5. `PUT /v1/plate-lists/{id}` replaces the policy with the version last
   read as `If-Match` or body `version` (409 `version-conflict`, Part 4
   §4.2a). Entries are not part of that body.

## 24.2 Entries (normative)

1. An entry's credential is an APDS `CredentialTypeEnum` value and its
   identification, with an optional `jurisdiction`. `validity` bounds
   when the entry is in force; an absent bound is open.
2. Adding an entry whose credential is already on the list with an
   overlapping validity updates that entry (200) instead of creating a
   second one, so a nightly load is naturally idempotent.
3. `details` records why the plate is listed, as the list owner says
   (amount owed, notice count, the owner's reference). It carries no
   personal data beyond the plate.
4. Removing an entry is `DELETE …/entries/{entryId}`; the entry feed
   emits a tombstone.
5. **Privacy.** Plate values appear only under `apx.platelists:read` or
   `:manage` (Part 9 §9.6). A consumer that only screens MAY hold the
   `apds-ext:apx:credential-digest@1.0` decoration of each entry instead
   and match by digest (Part 9 §9.6 rule 6). The list owner publishes the
   retention of its entries.

## 24.3 Change feed and eventing (normative)

1. `GET /v1/plate-lists` and `GET /v1/plate-lists/{id}/entries` serve
   the Part 5 §5.2 change feed (`mode=change&cursor=`), so a site or an
   edge implementation keeps a local copy current and catches up after a
   link outage.
2. Every create, change, or removal of a list or an entry publishes
   `apx.platelists.changed.v1` (data: the `PlateList` with its
   `entryCount`; subject: the list). The event carries no plate values;
   consumers resync entries through the feed.

## 24.4 Screening (informative)

A lane, an LPR system, or an enforcement handheld screens each read
against the lists that apply at its place. APX defines the facts, not the
decision:

- A match on a list with `alertOnArrival` raises the Part 7 alert of its
  category — `watchListArrival`, `scofflawArrival` — with `relatedEntity`
  the `PlateListEntry` and the read as evidence (Part 7 §7.6).
- A match on a list with `refuseService` at a gated lane withholds the
  vend and shows the refused-service message through a Part 6
  `displayMessage` command, whose `result.display` is the proof of what
  was shown (Part 6 §6.1a); the alert is `refusedServiceArrival`. Where
  the edge drove the sign itself, with no command, the proof is a Part 26
  §26.9 `DisplayRecord` of kind `entranceRefused`.
- A match on an `allow` list with `enforced` satisfies the Part 19 §19.3
  eligibility check at that place; the eligibility result names the list
  entry as its basis.
- A match on an `enforcementVehicle` list is recorded and never screened
  further.

Whether a match refuses service, raises an alert, or both is the list's
place flags; how a match is made (exact, alias, fold) is the
implementation's, and where a Session results it is recorded as
`MatchEvidence` with `matchType: list` (Part 5 §5.8).

## 24.5 Conformance

`apx-plate-lists` requires the base classes and: §24.1 lists with
per-place flags, ownership, and the visibility and grant rules; §24.2
entries with the upsert rule and the plate-value scope rule; §24.3 the
change feed on both routes and `apx.platelists.changed.v1`. Raising the
§24.4 alerts is conditional on `apx-alerts` (Annex A.22).

## 24.6 Registry entries

| Registry | Entries |
|---|---|
| `apx-conformance-classes` v8 | `apx-plate-lists` |
| `apx-plate-list-categories` v1 | `allow`, `watch`, `scofflaw`, `enforcementVehicle` |
| `apx-topics` v12 | `apx.platelists.changed.v1` |
| `apx-alert-types` v5 | `watchListArrival`, `scofflawArrival`, `refusedServiceArrival` |

Scopes: `apx.platelists:read`, `apx.platelists:manage`.

---

<a id="25-ui-launch"></a>

# APX Part 25 — UI Launch (optional class `apx-ui-launch`)

Most remote-assistance actions still happen in a vendor's own web screen,
not through an API: an agent opens the PARCS portal in another window,
issues the lost ticket or applies the validation there, and types a note
into the call record. The two systems never agree afterwards. This Part
lets an implementation offer those screens to an authorised agent with
automatic login, and makes everything done on them come back as APX
data, so the call record and the vendor's record stay in step.

"Screen-pop" already names the lane inquiry (Part 6 §6.2). This Part
uses **UI launch** for opening a vendor's own screen.

**What APDS has.** Nothing. APDS defines no user interface, no agent
identity, and no way to refer a person from one system's screen to
another's.

**What this Part adds.** A registry of **surfaces** (the screens an
implementation may offer, named per module), the `UiLaunch` resource
(one opening of a surface for one agent, with a single-use URL or a
signed token), the `UiActionRecord` (what the agent did there, when no
APX operation covers it), the routes under `/v1/ui/launches`, the
discovery member `uiSurfaces[]`, the Part 17 execution descriptor
`type: ui`, and the topics `apx.ui.launch.changed.v1` and
`apx.ui.action.recorded.v1`. It is defined once and used by every
module: tolling, LPR, reservations or validations get the same launch
and loopback by adding a surface to the registry, not by new design.

## 25.1 Surfaces (normative)

1. A **surface** is a screen an implementation offers to staff. Its name
   is a definedValue of `apx-ui-surfaces` (Part 11), written
   `<module>.<view>`: `lane.console`, `session.detail`,
   `reservation.detail`, `reservation.search`, `lpr.read.review`,
   `violation.detail`, `violation.appeal`, `validation.redeem`,
   `credential.detail`, `toll.transaction`, `toll.dispute`,
   `valet.ticket`, `charging.session`, `place.console`, `module.home`.
   Each registry entry names the **subject class** the surface opens (a
   Session, a Violation, a VehicularAccess); `reservation.search`,
   `place.console` and `module.home` are general surfaces with no
   subject, or with the place as subject.
2. An implementation lists the surfaces it offers as `uiSurfaces[]`
   (`UiSurface`) in `/.well-known/apx-configuration` and in every
   discovery document whose client holds `apx.ui:launch` or `apx.ui:read`
   (Part 16). Each entry gives the surface, its `displayModes` (`embed`,
   `popup`, `tab`), its `launchModes` (`handshake`, `signedToken`), the
   `actions` it can report back (§25.4), its `launchUrl` where the
   signed-token mode is offered, and the `places` where it is offered
   (absent means every granted place). The discovery soundness rule
   applies (Part 16 §16.2): a listed surface MUST be launchable by that
   client for at least one granted place.
3. A launch that names a surface the implementation does not offer, or
   does not offer at that place, is refused with
   `422 ui-surface-unknown`; a `displayMode` the surface does not list is
   `422 ui-display-mode-unsupported`.

## 25.2 Launching (normative)

Both launch modes log the agent in automatically. The agent never sees a
vendor login page and never holds vendor credentials.

1. **Handshake.** The host calls `POST /v1/ui/launches` (scope
   `apx.ui:launch`) with its normal APX token and a `UiLaunchRequest`:
   `surface`, `place`, `agent`, `agentType`, `correlationId` and
   `displayMode`, plus optionally `subject`, `lane`, `interactionId`,
   `returnFields[]` and `expiresIn`. The implementation answers 201 with
   the `UiLaunch`: a `url` that is **single-use** and lives at most 60
   seconds (`expiresAt`), the `displayMode` granted, and `status: issued`.
   The host opens the `url` for the agent. This mode also serves
   implementations that authenticate the host by request header, because
   a browser cannot add headers when it opens a page: the server-to-server
   call carries the header, the one-time `url` carries nothing but its own
   identity.
2. **Signed token.** The host signs a launch token and sends it to the
   surface's `launchUrl`, by HTML form POST or in the URL fragment, never
   in the query string. The token is a JWS (RFC 7515) whose payload
   carries the `UiLaunchRequest` members as claims, with `iss` (the host),
   `aud` (the implementation), `iat`, `exp` at most 60 seconds after
   `iat`, and a unique `jti`. The implementation verifies the signature
   against the host's keys, registered at onboarding, refuses an expired
   or replayed `jti`, creates the `UiLaunch` itself, and opens the surface.
   Accepted algorithms are `ES256` and `RS256`; `none` MUST be refused.
3. **Binding.** A launch is bound to one surface, one subject (where
   given), one agent and one place. The implementation MUST open exactly
   that screen for that agent and MUST NOT let the opened session reach
   another subject outside the agent's grant.
4. `expiresIn` is at most 60 seconds; a larger value is refused (400
   `invalid-request`), and the effective `expiresAt` is returned. `Idempotency-Key` is optional (Part 4
   §4.2a); a replay returns the launch as it currently stands (200).
5. `GET /v1/ui/launches/{id}` and `GET /v1/ui/launches` (scope
   `apx.ui:read`; filters `place`, `agent`, `status`, `correlationId`,
   `interactionId`) return launches whose `place` lies inside the grant.
   The read never returns a usable `url` after the launch has been
   opened, closed or expired.

## 25.3 Lifecycle (normative)

`issued → opened → closed | expired`, and `issued → expired`.

- `issued` on creation. `opened` on the first use of the `url` or token;
  a second use is refused by the surface (the launch is single-use), and
  the implementation SHOULD record the attempt in its audit log.
- `closed` when the surface reports the end of the session with
  `POST /v1/ui/launches/{id}/close` (scope `apx.ui:record`), or when the
  implementation observes the session end.
- `expired` when `expiresAt` passes while `issued`, or when an opened
  launch outlives the implementation's session limit without being
  closed (RECOMMENDED 1 hour after `openedAt`).
- `openedAt` and `closedAt` are set on those transitions. Every
  transition publishes `apx.ui.launch.changed.v1`. Terminal states
  (`closed`, `expired`) never transition again; an action or close on a
  terminal launch is `409 ui-launch-closed`, and one on an `issued` launch
  whose `expiresAt` has passed is `409 ui-launch-expired`.

## 25.4 Loopback (normative)

This is the heart of the Part. Everything an agent does on a launched
surface MUST appear in APX exactly as if it had been done through the
API.

1. **Native first.** An action whose APX operation the implementation
   supports MUST produce that operation's own effect: the Part 6 Command
   (a vend, a lost ticket), the Part 13 refund, the Part 20 redemption,
   the Part 17 plate correction, and so on. The resulting record MUST
   carry the launch's `correlationId` and the launch's `agent` and
   `agentType` (as `Command.agent`, `recordInfo`, or the member the
   operation defines), and MUST publish the events that operation
   publishes. A consumer following `correlationId` then sees the action
   with no knowledge that a screen was involved.
2. **Fallback.** An action with no supported APX operation MUST instead be
   recorded as a `UiActionRecord` on the launch: `action` (an
   `apx-command-types` value, the effective operationId of the nearest
   APX operation, or `other`), `outcome` (`completed`, `failed`,
   `noAction`), `occurredAt`, a short `summary`, and `result` where a
   native resource was nevertheless created. The surface, or its backend,
   records it with `POST /v1/ui/launches/{id}/actions` (scope
   `apx.ui:record`); an implementation whose surface and APX server are
   one system MAY record it internally and MUST then behave as if the
   route had been called. Each record publishes
   `apx.ui.action.recorded.v1`.
3. **Return fields.** The host MAY ask, in `returnFields[]`, for the values
   it needs back, named as JSON Pointers into the APX schema the action
   concerns (`/amountDue`, `/session/id`, `/ticketNumber`). A
   `UiActionRecord` carries them in `fields`, keyed by the pointer. A
   value the implementation cannot supply is listed in `unavailable[]`
   with `reason` `notSupported` (the surface never knows it) or
   `notApplicable` (it does not apply to this action), never silently
   omitted. The host decides what it needs; the implementation says what
   it can give.
4. **Completeness.** A launch that is `closed` with no native effect and
   no `UiActionRecord` means the agent did nothing; the implementation
   MUST NOT infer an action from the screen having been opened.
5. **Summaries, not transcripts.** `summary` and `fields` carry no
   personal data beyond what the referenced APX resources already carry
   under the host's scopes (Part 9 §9.6).

## 25.5 Part 17 link (normative)

1. `AllowedAction.execution.type` gains the value `ui`. With
   `type: ui` the descriptor carries `surface` (REQUIRED) and `subject`
   (OPTIONAL), so an agent's console offers "do it in the vendor's
   screen" when no API operation exists for the action, and the policy
   decision (`allowed`, `requiresApproval`, `reason`) applies to the
   launch exactly as to a command (Part 17 §17.3).
2. A launch created from an allowed action SHOULD carry the context's
   `correlationId`; the `UiLaunch` and its `UiActionRecord`s MAY be
   referenced from `SupportInteraction.actions[]` by className, so a
   support record lists what was done on a screen beside what was done by
   command.

## 25.6 Security (normative)

1. A launch `url` and a launch token are bearer secrets: single-use,
   valid at most 60 seconds, bound per §25.2 rule 3, HTTPS only. They
   MUST NOT appear in a query string, a referrer, or a log line; the
   fragment or a form POST carries the token, and the `url` identifies
   the launch by an unguessable value (at least 128 bits of entropy).
2. Personal data and secrets MUST NOT be carried in a launch URL or
   token; the surface reads what it needs through its own back end.
3. `embed` requires the host's origin to be registered with the
   implementation, which MUST set `Content-Security-Policy:
   frame-ancestors` to exactly the registered origins; an unregistered
   origin is offered `popup` or `tab` instead.
4. The keys a host signs tokens with, and any secret an implementation
   issues for the handshake mode, are rotatable with an overlap window
   (as Part 9 §9.4) and MUST NOT be returned by any read operation
   (Part 9 §9.7).
5. The opened session MUST end when the launch closes or expires; a
   surface MUST NOT leave the agent logged into the vendor system beyond
   the launch.
6. Everything done on a launched surface is attributable: the agent of
   the launch, not a shared vendor account, appears on every record it
   produces (§25.4).

## 25.7 Conformance

`apx-ui-launch` requires the base classes and: §25.1 surfaces listed in
discovery with the soundness rule; §25.2 at least one launch mode, with
automatic login and the 60-second single-use rule; §25.3 the lifecycle
and its topic; §25.4 loopback, native first, `UiActionRecord` otherwise,
with `returnFields` answered or declared unavailable; §25.6 the security
rules. §25.5 applies when `apx-resolution` is also claimed. Annex A.23.

## 25.8 Registry entries

| Registry | Entries |
|---|---|
| `apx-conformance-classes` v9 | `apx-ui-launch` |
| `apx-ui-surfaces` v1 | `lane.console`, `session.detail`, `reservation.detail`, `reservation.search`, `lpr.read.review`, `violation.detail`, `violation.appeal`, `validation.redeem`, `credential.detail`, `toll.transaction`, `toll.dispute`, `valet.ticket`, `charging.session`, `place.console`, `module.home` |
| `apx-topics` v13 | `apx.ui.launch.changed.v1`, `apx.ui.action.recorded.v1` |

Scopes: `apx.ui:launch`, `apx.ui:read`, `apx.ui:record`.

---

<a id="26-site-configuration"></a>

# APX Part 26 — Site Configuration (optional class `apx-site-config`)

A system that joins a location today has to learn its layout from three
places at once: the PARCS vendor's portal for the lanes and devices, the
call platform for which intercom rings where, and a spreadsheet for the
cameras. This Part makes one source enough. From APX alone a consumer can
tell what hardware a location has and where it stands, how a parker at
any point of it reaches help, what each camera looks at and how to see
it, which other systems know the location by which ids, and whom to
escalate to, in that order.

**What APDS has.** The place hierarchy (Campus, Place, VehicularAccess as
the lane, SupplementalEquipment as a device, Space), each with a name, a
location and opening times; `AccessLaneSpecific` for a lane's type and
sequence; `SupplementalFacility.externalIdentifiers` as untyped strings;
`EquipmentTypeEnum` for the kind of equipment, written for rest areas
and general parking: it has `paymentMachine`, `tollTerminal` and
`publicPhone`, and no gate, ticket dispenser, reader, camera, intercom
or door.

**What this Part adds.** Four Level B decorations on APDS elements:
`equipment-detail` (what a device is and does, with a fixed list of
roles), `camera` (what a camera looks at and how it streams),
`external-ids` (typed ids in other systems) and `place-operations` (the
call platform's account for the site and the escalation roles). One new
resource, `CommsPoint`: any way a parker reaches help from a lane or a
location. A second new resource, `DisplayRecord`: the record that a sign showed a
message, for which read or stay (§26.9). The routes `GET /v1/equipment`,
`/v1/comms-points`, `/v1/display-records`,
`POST /v1/identifiers:resolve` and `POST /v1/cameras/{id}/views`, the
change feed on equipment, comms points and display records, and the
topics `apx.equipment.changed.v1`, `apx.commspoints.changed.v1` and
`apx.displays.shown.v1`. Equipment
*type* stays the APDS `EquipmentTypeEnum`; this Part never redefines it.

## 26.1 Equipment detail (normative)

1. Every piece of hardware at a location, cameras, signs, readers, doors
   and servers included, is an APDS `SupplementalEquipment` in the place
   hierarchy, and carries the decoration
   `apds-ext:apx:equipment-detail@1.0` (`EquipmentDetail`, Part 4 §4.3).
2. `role` (REQUIRED) says what the item does on the lane or in the place.
   It is a definedValue of `apx-equipment-roles` (Part 11), a **fixed**
   code list: an implementation uses one of its codes and never free
   text; only the APX maintainers add codes, in a later version. The
   codes, by group:

   | Group | Codes |
   |---|---|
   | Vehicle barriers | `gate`, `overheadDoor`, `bollard`, `trafficSpikes` |
   | Pedestrian barriers | `accessDoor`, `turnstile` |
   | Ticketing and payment | `ticketDispenser`, `exitVerifier`, `exitPayStation`, `payOnFoot`, `cashierBooth`, `validationTerminal`, `paymentTerminal` |
   | Credential readers | `cardReader`, `tagReader`, `barcodeReader` |
   | Vehicle detection | `loopDetector`, `presenceSensor` |
   | Cameras | `camera` (what it looks at is the §26.3 decoration) |
   | Communications | `intercom`, `analogDialer`, `emergencyPhone` |
   | Signs and displays | `laneDisplay`, `laneSignal`, `countSign` |
   | On-site infrastructure | `laneController`, `facilityServer`, `videoRecorder`, `videoGateway` |
   | Fallback | `other` (a code, for hardware nothing above fits) |

   The APDS `equipmentType` is still written, as the nearest APDS value
   (`paymentMachine` for a pay station, `other` for a gate); `role` adds
   to it and never contradicts it.
3. Identity of the item: `manufacturer`, `model`, `serialNumber`, and
   `macAddresses[]`, one per network interface, as twelve lower-case
   hexadecimal digits with no separators. The MAC is carried because it
   is sometimes the serial number, because serial labels come off while a
   MAC can always be read back from the network, and because it is a
   hardware identity, not a way in.
4. `firmwareVersion` and `softwareVersion` are OPTIONAL and RECOMMENDED,
   readable by every client that may read the place. Knowing what runs
   on each device is what makes integration at scale possible; without
   it, the version on a device is very hard to find out any other way. A
   station that runs both carries both.
5. Where the item is: `area` (a definedValue of `apx-area-types`, §26.2),
   `level` (the floor's label as it is signed, `P2`, `Roof`), and `lane`
   (a Reference to the VehicularAccess it serves). `lane` is optional: an
   access door, a pay-on-foot station or a server belongs to the place,
   not to a lane. `installedAt` is the date it went into service.
6. How items relate: `builtInto` names the host equipment of an item
   built into another (an intercom inside an access door's call box, a
   driver camera inside a pay station); `controls[]` names the barriers
   this item opens (an exit verifier that controls gate 2, a card reader
   that controls an access door). A command that targets a reader's
   barrier targets the barrier itself; `controls[]` is description.
7. `serviceProvider` names who services the item: an `organisation`
   Reference and a `supportReference` (the provider's own ticket or
   contract id), never a person.
8. **No IP addresses (non-normative note).** Network addresses are
   deliberately left out of this decoration in the interest of security.
   Together with model and firmware version, an address map of a site's
   devices tells an attacker which unit to reach and which known weakness
   to try, and no APX flow needs one: commands go through the API, camera
   views arrive as short-lived links (§26.3), launches as single-use URLs
   (Part 25). Network setup stays in the vendor's or operator's own tools,
   behind their own access controls. An optional member behind a
   restricted scope could be added in a later edition if a real need
   arises; taking one out after implementations publish it would be far
   harder.
9. `GET /v1/equipment?place=` (scope `apx.siteconfig:read`; `place`
   REQUIRED, optional `role`, `area`, `lane`, `level`) lists the
   SupplementalEquipment of a place subtree with their decorations;
   `GET /v1/equipment/{id}` reads one. The list serves the Part 5 §5.2
   change feed. A `place` outside the grant is `403 insufficient-grant`.
   The native `/places` routes keep serving the same elements with the
   same decorations (Part 5); this route is the filtered view a consumer
   wants when it asks "what is on lane 3" or "every camera on P2".

## 26.2 Areas (normative)

`apx-area-types` names where something stands, for equipment (§26.1
rule 5), cameras and comms points. It is a fixed code list:

| Group | Codes |
|---|---|
| Vehicle areas | `lane`, `entryPlaza`, `exitPlaza`, `ramp`, `parkingLevel`, `roof`, `evChargingArea`, `loadingDock`, `perimeter` |
| Pedestrian and staff areas | `elevator`, `elevatorLobby`, `lobby`, `stairwell`, `pedestrianEntrance`, `paymentArea`, `cashierBooth`, `office`, `equipmentRoom` |
| Fallback | `other` |

`elevator` is the inside of the car; `elevatorLobby` is the landing. An
item on a lane carries `area: lane` and names the `lane`.

## 26.3 Cameras (normative)

1. A camera is equipment with `role: camera` anywhere in the place, not
   only on a lane: the ramps, stairwells, lobbies and elevators have them
   too, and so does the pay station that looks at the driver. It carries
   the decoration `apds-ext:apx:camera@1.0` (`CameraDetail`) beside its
   equipment detail:
   - `mounting`: `builtIn`, `overhead`, `pole`, `wall`, `ceiling` or
     `other`. When `builtIn`, the equipment detail's `builtInto` names the
     host (the pay station with a driver camera; the overhead camera above
     the exit lane that watches the card slot is `overhead` with the pay
     station in view, not built in).
   - `view[]` (REQUIRED, at least one): `plate`, `vehicle`, `driver`,
     `equipmentInteraction` (hands, ticket and card at the machine),
     `pedestrians`, `areaOverview` or `other`. One camera may cover more
     than one.
   - `faces` (`inward`, `outward`), for cameras on lanes, plazas and
     ramps, with the meaning of Part 13 §13.3a(5).
   - `primary` (show this one first for its lane or area), `ptz`,
     `recorder` (the `videoRecorder` equipment and its `channel`), and
     `streams[]` (what it can deliver: `protocol` `webrtc`, `hls`,
     `rtsp`, `mjpeg` or `snapshot`, with `transport` and `resolution`).
   - `retentionDays`: how long its recordings are kept (rule 4).
   - `credentialRef`: a label the receiving system looks up in its own
     vault, for a camera that cannot sit behind a gateway. It is never
     the secret.
   The decoration carries no address, user name or password.
   `apds-ext:apx:lane-cameras@1.0` (Part 13 §13.3a(5)) is unchanged: it
   stays the LPR module's statement of which camera reads which lane.
2. **Views.** `POST /v1/cameras/{id}/views` (scope `apx.cameras:view`)
   with `kind` `live`, `snapshot` or `playback` (`from` and `to`, for a
   dispute: "I entered at 2:14"), `agent`, `correlationId` and optionally
   `interactionId` returns 201 `CameraView`: a `url`, its `protocol`, and
   `expiresAt` at most 5 minutes away. The link is single-use, bound to
   the agent, HTTPS, and requires no further credential. Whoever holds the
   camera answers: the PARCS, the video system, or an on-site gateway
   (`videoGateway`) that holds the camera's password and serves the link.
   The password never leaves the site. A camera that cannot deliver the
   kind asked for, or is offline, is `409 camera-view-unavailable`; a
   playback range outside `retentionDays` is
   `422 playback-range-unavailable`. `live` is the feature `cameraViews`
   and `playback` the feature `cameraPlayback` (`apx-features`); an
   implementation offers neither, one or both and lists what it offers
   (Part 16 §16.1).
3. **No passwords in APX.** Part 9 §9.7 applies: no request, response or
   event carries a camera, PARCS or device password, in any member or in
   nested JSON. The secret behind a `credentialRef` is exchanged once,
   outside APX, at onboarding.
4. **Privacy.** `driver` and `pedestrians` views capture faces, personal
   data in many jurisdictions. Each camera states `retentionDays`; every
   view and playback is logged against the agent and the `correlationId`;
   a view link MUST NOT be reused or shared outside the interaction it was
   issued for (Part 9 §9.6 rule 2 applies to the link).
5. **Reference (informative).** Camera models and stream profiles follow
   ONVIF (Profile S for streaming, Profile T for H.265 and TLS); APX adds
   only what parking needs: the lane, the facing and the view.

## 26.4 Comms points (normative)

A **comms point** is any way a parker reaches help from a lane or a
location. It is not always an intercom: a posted phone number on a sign,
an analog dialer on a phone line, a kiosk, a QR code or an app all
count, and a lane may have one of each.

1. `CommsPoint` carries `type` (a definedValue of `apx-comms-point-types`:
   `intercom`, `analogDialer`, `postedNumber`, `kiosk`, `qrCode`, `app`,
   `other`), `place` (REQUIRED), `lane` (when it serves exactly one),
   `equipment` (when it is inventoried hardware, the `intercom` or
   `analogDialer` item of §26.1), and `serves` (the barrier an agent opens
   for it: a gate, or an access door).
2. Three identifiers, of which at least one MUST be present:
   - `phoneNumber` (E.164): the number that is called when help is
     requested, dialed by the parker for a posted number, or by the
     intercom or dialer itself when its button is pushed;
   - `callerId` (E.164): the number the device calls out as;
   - `commsDeviceId`: the id the call systems use for this comms point
     when they identify it by neither caller id nor dialed number (a voice
     platform's device id).
3. `coverage[]` says when it is answered and in which languages: a
   `period` (APDS `Period`) and `languages[]` (BCP 47 tags). The call
   center itself is not named.
4. **No SIP data.** SIP user names, realms and passwords are deliberately
   left out. A user name with its realm is half of a line's login and
   helps toll-fraud attacks; they exist only for SIP devices, not for
   dialers, posted numbers or kiosks; and the call platform already maps
   its own SIP data to its device and account ids. A SIP password or any
   other credential MUST never appear in any member. The pair could be
   reconsidered if a real need arises.
5. **Site account.** The place carries `apds-ext:apx:place-operations@1.0`
   (`PlaceOperations`), whose `commsAccountId` is the id the call platform
   uses for the whole site. With `commsDeviceId` on the comms points, a
   call maps to a location and a lane from APX alone.
6. **Resolution.** A comms point resolves to its `lane` when it has one,
   and otherwise to its `place` only. A posted number shared by many
   lanes can never name the lane by itself: the agent or the IVR asks
   which lane. Part 17 §17.1 takes `commsPoint`, `callerId`,
   `dialedNumber` and `commsDeviceId` on the resolve request; the server
   that holds the comms points answers, and a server that holds none
   answers `422 comms-point-unknown`.
7. Routes: `GET /v1/comms-points` (scope `apx.siteconfig:read`; filters
   `place`, `lane`, `type`, `callerId`, `dialedNumber` which matches
   `phoneNumber`, `commsDeviceId`; the Part 5 §5.2 change feed),
   `GET /v1/comms-points/{id}`, and, under `apx.siteconfig:manage`,
   `POST /v1/comms-points` (`Idempotency-Key` optional),
   `PUT /v1/comms-points/{id}` (versioned replace with `If-Match` or body
   `version`, 409 `version-conflict`, Part 4 §4.2a) and
   `DELETE /v1/comms-points/{id}` (the feed emits a tombstone). Whoever
   runs the calls writes them: the call center or the voice platform. A
   PARCS implementation never has to hold, return or understand them, and
   one that claims this class without comms points of its own serves the
   routes for the records the call platform writes.

## 26.5 Place operations and escalation (normative)

`PlaceOperations` (rule 26.4(5)) also carries `escalationContacts[]`:
who to ask when the agent cannot settle it, as **roles only**, in calling
order with their hours: `role` (a definedValue of `apx-escalation-roles`:
`siteManager`, `attendant`, `security`, `operatorOnCall`, `maintenance`,
`other`), `order`, and `hours` (APDS `Period`). Names and phone numbers
stay with the call platform, so APX holds no personal contact data. A
Part 17 §17.3a approval request is addressed to a role from this list.

## 26.6 External identifiers (normative)

1. Any HierarchyElement (a place, a lane, a device) MAY carry
   `apds-ext:apx:external-ids@1.0` (`ExternalIdentifiers`): a list of
   `{system, id}`, where `system` is a URI the issuing system owns (its
   own domain, as a rule) and `id` is its value. APDS
   `externalIdentifiers` are untyped strings, so a reader cannot tell
   which system an id belongs to; the decoration adds the system and
   changes nothing else. APX names no products or platforms: the issuer
   names itself.
2. `POST /v1/identifiers:resolve` (scope `apx.siteconfig:read`) with
   `{system, id}` answers 200 with the `target` Reference, its `className`
   and its `place`, so any system finds the APX record for an id it holds
   in one call. An unknown pair, or one whose element lies outside the
   grant, is `404 target-not-found` (Part 9 §9.3a: the answer never
   confirms another party's record).

## 26.7 Change notices (normative)

1. Every create, change or removal of a SupplementalEquipment with an
   equipment detail, or of its decorations, publishes
   `apx.equipment.changed.v1` (data: the element with its extensions;
   subject: the element; place-bound on its place). Every create, change
   or removal of a comms point publishes `apx.commspoints.changed.v1`
   (data: the `CommsPoint`). Removal carries the last representation.
2. `GET /v1/equipment` and `GET /v1/comms-points` serve the Part 5 §5.2
   change feed, so another system keeps a copy of a site's setup current
   without re-reading the whole place, and catches up after an outage.

## 26.8 After-hours access (informative)

Both transient parkers and permit holders may have to authenticate at an
`accessDoor` to reach their car after the lanes close.

- The door is place-level equipment (`role: accessDoor`,
  `area: pedestrianEntrance`, no `lane`). Its reader (`role: cardReader`
  or `barcodeReader`, `controls: [door]`) takes a ticket, receipt, QR
  code, card or PIN; its comms point (`type: intercom`, `equipment` the
  intercom `builtInto` the door, `serves` the door) rings the agent.
- An attempt at the door is recorded like a lane access event: a Part 21
  `AccessEvent` with the door as the device, or an `accessEvent: entry`
  on the read where the door has a camera.
- An agent opens the door with a Part 6 `vendGate` command whose `target`
  is the door; the confirmation level discipline applies as at a lane.
- The scenarios a conformance suite covers: a transient parker with a
  paid ticket after hours; a permit holder with a card; and a refusal for
  an expired permit.

**Worked example.** A door at the Lakeside garage's street entrance,
described from APX alone:

| Element | Role | Detail |
|---|---|---|
| Door | `accessDoor` | `area: pedestrianEntrance`, `level: G` |
| Door intercom | `intercom` | `builtInto: door` |
| Door reader | `cardReader` | `controls: [door]` |
| Door camera | `camera` | `area: pedestrianEntrance`, `mounting: wall`, `view: [pedestrians]` |
| Comms point | `type: intercom` | `equipment: door intercom`, `serves: door`, `commsDeviceId`, `callerId` |

A call from the intercom resolves (Part 17 §17.1) through
`commsDeviceId` to the comms point, from it to the door, and the agent's
allowed actions include `vendGate` on the door, with the door camera's
view a click away.

## 26.9 Display records (normative)

A sign at a lane or a door shows a message for a vehicle: "Welcome",
"Payment not found", "Thank you, your permit is valid". Who drove the
sign often decided locally, from its own copy of the rights, and no APX
command was involved. A `DisplayRecord` is that system's record that the
message was shown: which sign, which kind of message, when, and for which
read or stay. It is the proof of display a refused-service message needs
(Part 24 §24.4), and it lets an operator measure whether the signs change
what parkers do.

1. **Who writes it.** Whichever system drove the display: the edge
   controller, the PARCS, the sign's own player. `POST /v1/display-records`
   (scope `apx.displays:record`, `Idempotency-Key` optional) creates one;
   the record is immutable once written. APX records that the message was
   shown; whether showing it changed anything is analysis, outside APX.
2. **The sign.** `display` (REQUIRED) is a Reference to the
   SupplementalEquipment that showed it, a device of §26.1 whose `role`
   is `laneDisplay` or `countSign` (or `other`); a reference to anything
   else is 422 `reference-unknown`. The server derives `place` from the
   device; a `place` in the body that disagrees is 422
   `request-unprocessable`.
3. **What was shown.** `messageKind` (REQUIRED) is a definedValue of
   `apx-display-message-kinds` (Part 11) or an implementer code (a
   location's own messages). The proof members are the
   `CommandResult.display` shape of Part 6 §6.1a, reused unchanged:
   `shownTime` (REQUIRED), `contentHash`, `wordingVersion`, `screenshot`
   and `acknowledgedBy`. The message text itself is never stored, only
   its hash and wording version, and the plate is never stored even when
   the sign shows it (Part 9 §9.6 rule 8).
4. **For whom.** `observation` names the read that triggered the message
   and `session` the stay it was for; both are OPTIONAL (an idle or a
   welcome message is for nobody). A reference naming nothing visible to
   the caller is 422 `reference-unknown`.
5. **Commanded displays.** When a Part 6 `displayMessage` command drove
   the sign, `command` names it, and the record MUST agree with that
   command's `CommandResult.display`: a different `shownTime` or
   `contentHash` is 422 `request-unprocessable`. A server that executes
   `displayMessage` itself SHOULD write the record.
6. **Reading.** `GET /v1/display-records` (scope `apx.siteconfig:read`;
   filters `place`, `display`, `messageKind`, `session`, `observation`,
   `since` and `until` on `shownTime`; the Part 5 §5.2 change feed) and
   `GET /v1/display-records/{id}`. A `place` outside the grant is 403
   `insufficient-grant`.
7. **Notice.** Every record publishes `apx.displays.shown.v1` (data: the
   `DisplayRecord`; subject: the display; place-bound on its place).

The seed of `apx-display-message-kinds` is the set of nine messages an
edge controller deployed in 2026 already shows:

| Sign | Kinds |
|---|---|
| Entrance | `transientEntrance`, `monthlyEntrance`, `entranceRefused` (the refused-service message, Part 24 §24.4) |
| Exit | `transientExitPaid`, `transientExitUnpaid`, `monthlyExit`, `monthlyExitBalanceDue` |
| Either | `idle`, `fallback` |

An exit for an overstay or a late start shows `transientExitUnpaid`;
the registry is add-only (Part 11), so kinds for those, and for
reservations, can be added later.

## 26.10 Conformance

`apx-site-config` requires the base classes and: §26.1 the equipment
detail with a registry `role` on every SupplementalEquipment it serves,
and `GET /v1/equipment` with its filters and feed; §26.2 areas; §26.3
the camera decoration where a camera is inventoried, the view route
where `cameraViews` or `cameraPlayback` is offered, and the privacy
rules; §26.4 comms points, their routes and the resolution rule; §26.5
place operations; §26.6 external identifiers and the resolve route;
§26.7 the two topics and the feeds; §26.9 display records, their routes
and topic. The Part 9 §9.7 secrets rule applies
to every read of this Part. Annex A.24.

## 26.11 Registry entries

| Registry | Entries |
|---|---|
| `apx-conformance-classes` v9 | `apx-site-config` |
| `apx-equipment-roles` v1 | the thirty codes of §26.1 rule 2 |
| `apx-area-types` v1 | the nineteen codes of §26.2 |
| `apx-comms-point-types` v1 | `intercom`, `analogDialer`, `postedNumber`, `kiosk`, `qrCode`, `app`, `other` |
| `apx-escalation-roles` v1 | `siteManager`, `attendant`, `security`, `operatorOnCall`, `maintenance`, `other` |
| `apx-features` v3 | `cameraViews`, `cameraPlayback` |
| `apx-topics` v13 | `apx.equipment.changed.v1`, `apx.commspoints.changed.v1` |
| `apx-topics` v14 | `apx.displays.shown.v1` |
| `apx-display-message-kinds` v1 | the nine kinds of §26.9 |

Scopes: `apx.siteconfig:read`, `apx.siteconfig:manage`, `apx.cameras:view`,
`apx.displays:record`.

---

<a id="annex-a-ics"></a>

# APX Annex A — Conformance Requirements and ICS Template (normative)

This annex numbers the testable requirements of each conformance class so a
certification body, procurement team, or implementer can build a test
matrix. Identifiers are stable: `APX-<CLASS>-<NN>` never renumber; new
requirements append. Each row cites the defining section — the cited text
is authoritative; the row is its handle.

Requirements marked **C** are conditional (apply only when the
implementation has the named capability).

## A.1 Cross-cutting (all classes)

| ID | Requirement | Source |
|---|---|---|
| APX-CORE-01 | APDS-defined routes/schemas/conventions used verbatim by $ref; no APDS entity redefined, subset, or re-shaped | §0.2 |
| APX-CORE-02 | A plain APDS 4.1 client works unmodified for the routes it uses | §0.2 |
| APX-CORE-03 | Every APX resource carries `id` (UUID) + `version`; client-supplied id collision → 409 | §4.1 |
| APX-CORE-04 | Unknown `extensions` keys preserved on round-trip; keys match the §4.3 pattern | §4.3 |
| APX-CORE-05 | All APX errors are RFC 9457 problem+json with registered `type` URIs | §12 |
| APX-CORE-06 | OAuth2 client-credentials supported; HTTPS everywhere | §9.1 |
| APX-CORE-07 | Missing scope → 403 `insufficient-scope`; out-of-grant target → 403 `insufficient-grant` | §9.2–9.3 |
| APX-CORE-08 | `apx_places` fail-closed: absent/empty = no places; all-places only via explicit `"*"` | §9.3 |
| APX-CORE-09 | `GET /.well-known/apx-configuration` served unauthenticated at host root | §16.1 |
| APX-CORE-10 | Personal-data minimization, access-controlled imagery, published retention with purge | §9.6 |
| APX-CORE-11 | Aggregated/imported HierarchyElements: source id preserved or aliased via `operatorDefinedReference` | §4.1a, §18.2 |
| APX-CORE-12 | `/.well-known/apx-configuration` `conformanceClasses` are closed under the §3.1 dependency table and list only registry values or vendor classes | §3.1, §16.1 |
| APX-CORE-13 | Vendor additions stay in the vendor's namespace: extension keys `apds-ext:<vendor-ns>:…`, endpoints under `/apx/x/<vendor-ns>/`, topics prefixed `<vendor-ns>.`; never the `apx` namespace or `apx.` prefix; no unregistered keys outside `extensions`; no APX-registered code-list value reused with different semantics | §3.3, §10.1–10.2 |
| APX-CORE-14 | Every registry version the implementation validates against is served or linked and advertised in `/.well-known/apx-configuration` `registries`; every advertised URL resolves | §11.2 |
| APX-CORE-15 **C** (receives a migrated place) | Onboarding at the receiving implementation: the grant names exactly the migrated subtree root(s), never `"*"`; imported historical entities keep their `recordInfo` provenance | §18.3 |
| APX-CORE-16 **C** (hands over a migrated place) | After the agreed cutover the releasing implementation publishes no events bound to the moved place, and its change feeds emit tombstones for the entities it no longer hosts | §18.4 |
| APX-CORE-17 **C** (publishes credential digests) | `apds-ext:apx:credential-digest@1.0` computed as HMAC-SHA-256 under the normalization published with each key; keys served only by `GET /v1/digest-keys` under `apx.digest:use`; rotation by `keyId` with both keys served during the overlap | §9.6 r6 |
| APX-CORE-18 | For each claimed class, every route of that class in the §5.2 rule 8 table accepts `mode=change&cursor=`: the route's own list envelope with `updateMode` (`change`, absent on a `mode=full` page), `cursor`, `deleted`, `grantAdditions`, `next`, and no `meta`; whole current representations, never an older state after a newer one; no cursor → an empty page with the head cursor; a change in a filtered feed when the resource matched before or after it; §5.2 rules 1, 2, 4–7 (a foreign or expired cursor is 404 `target-not-found`) | §5.2 r8 |
| APX-CORE-19 **C** (replicated site implementation) | Authority per place and entity class declared in discovery `replication[]`; a write against it is 409 `not-authoritative`; replicated writes keep origin `id`, `RecordInfo.origin`, `creationTime`, and the original `statusHistory[]` times under `apx.replication:write`; idempotency keys origin-scoped | §18.6, §16.2 |
| APX-CORE-20 | Every plate-, card-, or phone-keyed lookup served is also served in its body form (`…:search`, `…:check`) with the same rules and answers | §9.6 r7 |

## A.2 `apx-data`

| ID | Requirement | Source |
|---|---|---|
| APX-DATA-01 | The eight native APDS routes served | §5.6 |
| APX-DATA-02 | `APX-Update-Mode: full\|change` on updates (creates are always full); change body validated as `ChangePayload` with each member checked against the native property schema; explicit-null clears, absent unchanged; stale version → `version-conflict`; `Accept: application/problem+json` on a native route → registered Problem | §5.1, §5.1a |
| APX-DATA-03 | `mode=change` feed on /places, /sessions, /rates, /rights/assigned (and /observations where ingest is served) returning ChangeFeedPage | §5.2, §5.6 |
| APX-DATA-04 | Feed ordered and gapless per (class, credential, filter set); replay from any issued cursor exactly-once | §5.2 r1, r5–6 |
| APX-DATA-05 | Cursors honored ≥7 days; older or foreign → 404 `target-not-found` (always a Problem); tombstones emitted and retained for the window | §5.2 r2, r4–5 |
| APX-DATA-06 | Grant expansion signaled via `grantAdditions` on the first page after the change | §5.2 r7 |
| APX-DATA-07 | Client sending no APX headers/params observes pure APDS 4.1 behavior | §5.6 |
| APX-DATA-08 **C** (holds occupancy) | `GET /v1/places/{id}/occupancy` served; snapshot derivable from the Place hierarchy | §5.5 |
| APX-DATA-09 | `apds-ext:apx:session-detail@1.0` accepted on native Session writes and preserved; with `apx-lpr`, a read bound as `entry`/`exit` to another Session of the place is 409 `read-already-bound` unless `supersedes` names it, and `LprRead.session` follows the bindings, naming the Session without `parent` that holds the read | §5.8 r1–2, §5.6 |
| APX-DATA-10 **C** (holds occupancy) | `GET /v1/places/{id}/occupancy/history` buckets aligned in the place time zone, derived from held DemandTable records, never guessed | §5.5a |
| APX-DATA-11 | An `apds-ext:apx:session-detail@1.0` write refused with 422 `request-unprocessable`, nothing written, for two `representative` reads, `lifecycle` and `actualEnd` disagreeing (`open` with one, any other state without), or a `parent` whose element is not an ancestor; a read naming no visible Observation → 422 `reference-unknown`; `final` never refuses a later revision; `MatchEvidence` carries `target`, `matchType` (closed set), `applied`, and `plate.method` / `vehicle.level` when those blocks are present; evidence for one link agrees at both ends | §5.8 r1, r3, r5–6, §5.6 |
| APX-DATA-12 | `apds-ext:apx:stay-compliance@1.0` accepted on native Session writes and preserved; `status` and `family` required, a registry status with another family → 422 `request-unprocessable`; `pendingStatus` only with `reviewRequired` (else 422 `request-unprocessable`); `evaluatedTime` and `sequence` required, a lower `sequence` than held → 409 `evaluation-out-of-order`; `final` never refuses a later evaluation; a `policy` naming nothing visible → 422 `reference-unknown` | §5.9 r1–2, r4–5, §5.6 |
| APX-DATA-13 | The stay-compliance decoration left out of every Session served to a token (native reads, change feed, Session event deliveries) without `apx.compliance:read`; a write carrying it without `apx.compliance:write` → 403 `insufficient-scope`, nothing written; a write omitting it keeps the stored block | §5.9 r7–8, §9.6 r8 |

## A.3 `apx-events`

| ID | Requirement | Source |
|---|---|---|
| APX-EVT-01 | Stock APDS `EventSubscription` accepted; stock request → APDS 200/202 ResponseStatus with the new id as the single `ids[]` entry; `Prefer: return=representation` → 201 with one-time secret (webhook transport); `Idempotency-Key` replay returns the subscription without the secret | §8.1 |
| APX-EVT-02 | Every delivery HMAC-SHA256-signed (`APX-Signature`, `APX-Timestamp`, `APX-Delivery-Id`); ±5-min replay window enforced by receivers | §8.3, §9.4 |
| APX-EVT-03 | Retry schedule 0s/30s/2m/10m/1h then hourly to 24h, as delays between consecutive attempts (27 attempts); every attempt in `attemptHistory`; exhaustion → status `failed` + `apx.subscription.failed.v1` (data `SubscriptionFailure`) | §8.3 |
| APX-EVT-04 | Envelope `id` and body stable across retries; `APX-Delivery-Id` unique per attempt; every attempt re-signed with its own `APX-Timestamp` | §8.3 |
| APX-EVT-05 | Per-topic place binding populated; `filters.places` matches bound place; unbindable events never delivered beyond grant | §8.5 |
| APX-EVT-06 | EventTypeEnum topics published for every entity class served for writes; APX topics of every claimed class published | §8.6 |
| APX-EVT-07 | Secret ≥32 bytes entropy; `APX-Key-Id` on every delivery during rotation overlap; overlap ends on `retireKeyIds` or 24 h after rotation | §8.1, §9.4 |
| APX-EVT-08 | `filters.alertTypes` delivers only listed alert types on alert topics, combined with `severityFloor` and `places` | §8.5a |

## A.4 `apx-events-sse`

| ID | Requirement | Source |
|---|---|---|
| APX-SSE-01 | `GET /v1/events/stream` for transport=sse subscriptions; `id:` = per-subscription sequence | §8.4 |
| APX-SSE-02 | `Last-Event-ID` resumes strictly after that sequence; ≥1000 events or 15 min buffered; older than the buffer → 410 `stream-position-expired` | §8.4 |

## A.5 `apx-control`

| ID | Requirement | Source |
|---|---|---|
| APX-CTL-01 | `POST /v1/commands` with REQUIRED Idempotency-Key; same key+body → the command as it currently stands (200); different body → 409 `idempotency-conflict`; unknown `commandType` or missing `target` → 400 `invalid-request` | §6.1 |
| APX-CTL-02 | Commands perishable: past `expiryTime` → `expired`, MUST NOT fire | §6.1 |
| APX-CTL-03 | Immutable append-only `statusHistory` on every transition; transitions publish `apx.control.command.status.v1` | §6.1 |
| APX-CTL-04 | Cancel allowed until `dispatched`; after → 409 `command-not-cancellable`; a `holdGateOpen` is released by its `expiryTime`, a later `closeLane`, or a `setDeviceState` on the held gate, and then `succeeded` | §6.1 |
| APX-CTL-05 | `confirmationLevel` reported truthfully; consumers never overclaim beyond it | §6.1 |
| APX-CTL-06 | vendGate, lostTicket, pushRate, applyValidation implemented; lane inquiry; provider query; device status | §6.5 |
| APX-CTL-07 | `applyValidation` with unlisted provider → 422 `validation-provider-unknown` | §6.3 |
| APX-CTL-08 | Lost-ticket fee taken from the rate deck's disclosed lostTicketFee line; absent line → synchronous 422 `lost-ticket-fee-undefined`, no command created or dispatched | §6.1 |
| APX-CTL-09 | *(conditional — negotiated rates)* `pushNegotiatedRate` applies only to the target lane's current ticket, never the deck; a table not flagged `apds-ext:apx:ratepolicy@1.0` `negotiable` for the place → 422 `rate-not-negotiable`; the flag travels inside the RateTable on `/rates` | §6.6 |
| APX-CTL-10 | *(conditional — negotiated rates)* `agent` REQUIRED on `pushNegotiatedRate` (400 `agent-required`); the lane's `currentTicket.negotiatedRate` names the table version and the command | §6.6 |
| APX-CTL-11 | *(conditional — ticket matching)* Lane inquiry returns advisory `matchCandidates[]` when no ticket is in the machine or a `plate`/`phone`/`credential` lookup is given; the server never binds without `matchTicket` | §6.7 |
| APX-CTL-12 | *(conditional — ticket matching)* `matchTicket` with `agent` REQUIRED prices the exit from the matched session's true entry time, materializes the exit into the APDS Session on vend (`SessionUpdated`), refuses a closed/foreign session (422 `session-not-open`) and an empty lane (409 `lane-no-current-transaction`) | §6.7 |
| APX-CTL-13 | `GET /v1/commands` returns only grant-scoped commands and honours `target`, `place` (subtree), `commandType`, `status`, `agent`, `since`, `until`; `GET /v1/devices` honours `place` (subtree) and `deviceState`; a `place` outside the grant → 403 `insufficient-grant` | §6.1b, §6.4 |
| APX-CTL-14 | A `succeeded` `lostTicket`, `matchTicket`, or `pushNegotiatedRate` carries `result` with the §6.1a members, consistent with the lane inquiry | §6.1a |
| APX-CTL-16 **C** (negotiated amounts) | `pushNegotiatedRate` with `parameters.amount` sets the current ticket's `amountDue` to that amount, with no floor or ceiling; exactly one of `rateTable`/`amount`, `agent` and `reason` REQUIRED, the ticket's currency, no negative value (400 `invalid-request`); `amount` without the `negotiatedAmounts` feature → 422 `rate-not-negotiable`; the lane's `currentTicket.negotiatedAmount` carries `amount`, `reason`, `agent` and the command, and `currentTicket.negotiatedRate` (with its required `rateTable`) is absent | §6.6 |
| APX-CTL-15 **C** (display reports what it showed) | A `succeeded` `displayMessage` carries `result.display` (shown time, content hash, wording version, screenshot link, acknowledgement); template parameters accepted | §6.1, §6.1a |
| APX-CTL-17 **C** (offers them) | `ejectTicket`, `cancelTransaction`, `reprintReceipt` accepted as device-targeted commands with the §6.1 rules; a device that cannot perform one reports `failed` in `statusHistory`, never a silent success | §6.1 |
| APX-CTL-18 | A `DeviceStatus` in `fault` carries `fault` (`code`, `displayText`, `since`) where the device reports one; `consumables[]` with `kind` and `level` where the device holds them, a `low` or `empty` level raising `consumableLow`; `maintenanceMode` honoured; last contact reported only as `lastCommunication` | §6.4, §7.7 |

## A.6 `apx-alerts`

| ID | Requirement | Source |
|---|---|---|
| APX-ALT-01 | Alert lifecycle raised→acknowledged→resolved with immutable statusHistory; illegal transitions → 409 `alert-transition-illegal`; an `AlertTransition` body's `detail` and `agent` are recorded on the appended history entry | §7.2, §7.3 |
| APX-ALT-02 | Idempotent raise (Idempotency-Key semantics as APX-CTL-01; a replay returns the alert as it currently stands) | §7.2 |
| APX-ALT-03 | Alert types from `apx-alert-types` (open registry); subtree-inclusive place filtering; `device` and `relatedEntity` filters honoured | §7.1, §7.2 |
| APX-ALT-04 | An alert still `raised` or `acknowledged` at its `expiryTime` becomes `expired` and publishes `apx.alert.status.v1`; an alert without one never expires | §7.3 |
| APX-ALT-05 | An alert with no place binding is visible to every token of its organisation regardless of `apx_places`, never to other organisations, and excluded from `place`-filtered lists | §7.1 |
| APX-ALT-06 | A `webhookDeliveryFailed` alert never generates webhook deliveries to the subscription whose failure it reports | §7.4 |
| APX-ALT-07 **C** (claims `apx-plate-lists`) | List-arrival alerts (`watchListArrival`, `scofflawArrival`, `refusedServiceArrival`) carry the `PlateListEntry` as `relatedEntity` and the read as evidence | §7.6, §24.4 |
| APX-ALT-08 **C** (claims `apx-resolution`) | Service-request alerts (`receiptRequested`, `refundRequested`, `citationInquiry`, `damageReported`, `customerCallbackRequested`) carry `supportInteraction` naming the call they came from and `relatedEntity` naming what the site acts on; `consumableLow` names the device and the consumable | §7.1, §7.7 |

## A.7 `apx-discovery`

| ID | Requirement | Source |
|---|---|---|
| APX-DSC-01 | `GET /v1/discovery` reflects the presented token's scopes/org/places exactly; no `apx_places` claim → `places: []`; `conformanceClasses` ⊆ the bootstrap document's | §16.2 |
| APX-DSC-02 | Soundness: every listed endpoint/command callable for at least one target in the granted places; an unlisted endpoint the server implements → 403 `insufficient-scope`; an endpoint of an unclaimed class → 404 `target-not-found` | §16.2, §9.3a |
| APX-DSC-03 | Two clients with different grants receive different documents | §16.2 |
| APX-DSC-04 | Offered optional features listed in `features` of the bootstrap document and of every discovery document whose client holds a scope of the class | §16.1, §16.2, §6.5 |
| APX-DSC-05 **C** (claims `apx-ui-launch`) | `uiSurfaces[]` lists only surfaces the client may launch at a granted place, with display modes, launch modes, actions and places; `placeCapabilities[]`, where present, lists per place a subset of the document's `commandTypes`, `uiSurfaces` and `features` | §16.2, §25.1 |

## A.8 `apx-accounts` / A.9 `apx-payment-history`

| ID | Requirement | Source |
|---|---|---|
| APX-ACC-01 | Account lookup by any filter combination; results constrained to the caller's grant | §13.1, §13.5 |
| APX-ACC-02 | Take-payment idempotent; declines → 422; approved account payments reduce balance | §13.1 |
| APX-ACC-03 | `PaymentRecord.place` populated (site binding); `apx.accounts.payment.recorded.v1` published for every recorded payment | §13.5, §13.4 |
| APX-ACC-04 | Payment links / refund / void / capture as domain operations with Idempotency-Key; refunds gated per policy; payment links readable and cancellable | §13.1a |
| APX-ACC-05 | Payment state machine per the §13.1a table (a hold = `approved` + `captureStatus: authorized` via `captureLater`, capture/void, cumulative `refundedAmount`); every other action → 409 `payment-state-illegal`; holds excluded from `GET /v1/payments` unless `captureStatus=authorized`, never published or materialized until captured; the recorded event re-published on every status or amount change | §13.1a, §13.2, §13.4 |
| APX-ACC-06 | More than four card digits in any body member or query parameter → 422 `personal-data-not-permitted`, refused before persisting or logging | §13.1, §13.2 |
| APX-ACC-07 **C** (persists APDS Payment) | Every `approved`, captured PaymentRecord is materialized as, or bound to, a native APDS `Payment` with a `paymentLines` entry of `paymentType: payment` and `value` = `amount` (or, for a record itemized per §13.6a, with its `lines` unchanged as `paymentLines` and its `rateSold` copied, never the lines a second time in the decoration); declined records and uncaptured holds are not | §13.6, §13.6a |
| APX-ACC-08 | `POST /v1/accounts:search` and `POST /v1/payments:search` answer as their `GET`s, with the same key, window, hold, and grant rules | §13.1, §13.2 |
| APX-ACC-10 | `POST /v1/payments/{id}/receipt` re-sends a settled payment's receipt by `channel` with `destination` in the body, answering 202 `ReceiptDelivery`; unsupported channel → 422 `receipt-channel-unsupported`; unsettled payment → 409 `payment-state-illegal`; refund `reasonCode` accepted and returned | §13.1a |
| APX-ACC-09 **C** (itemizes payments) | `apds-ext:apx:payment-detail@1.0` accepted on `POST /v1/payments` with `lines` as APDS `PaymentLine`s, the code in `idCode` (from `apx-payment-line-codes` or the implementer's own list; `validation` ⇔ `paymentType: validation`); `paymentType: payment` lines that do not sum to `amount` (same `currencyType`) → 422 `request-unprocessable`, nothing charged; the decoration returned unchanged on the record, `GET /v1/payments`, and the recorded event; refunds never rewrite the lines; on a native `Payment` the decoration carries `rateSold` only (no `lines`); no invented lines and no zero-fee line inferred | §13.6a |
| APX-PHX-01 | Truncated-key lookups without `date` constrained to last 8 hours, not configurable wider; `ticketNumber` and `account` exempt; a query with no key → 400 (except `mode=change`, APX-PHX-02) | §13.2, §9.6 |
| APX-PHX-02 | `GET /v1/payments?mode=change` serves the payment feed without a key or window, exactly the records `apx.accounts.payment.recorded.v1` delivers (holds from capture, voided holds never); `ticketLast4`/`cardLast4` with `mode=change` → 400 `invalid-request` | §13.2, §5.2 r8 |

## A.10 `apx-lpr`

| ID | Requirement | Source |
|---|---|---|
| APX-LPR-01 | Ingest via native `POST /observations`; cross-lookup plate↔ticket (and by Observation id or Session) with confidence + imagery links; a lookup with no key → 400 `invalid-request`; value-keyed lookups bounded by the grant and never 403, entity-keyed ones outside it → 403 | §13.3, §13.5(4) |
| APX-LPR-02 | `LprRead.place` populated; `apx.data.observation.created.v1` published per ingest and `apx.data.observation.updated.v1` per replace; purged imagery removed and flagged `purgedImagery`, purged reads absent from every lookup | §13.5, §13.4, §13.3b |
| APX-LPR-03 | **C** (engine supplies it): `apds-ext:apx:lpr-read@1.0` accepted on ingest and projected as `LprRead.detail` / `PlateCandidate.detail`; winning values mirrored into the APDS-native fields; per-attribute confidences 0–1; nothing guessed when absent (no sentinel values, no 0,0 location); an engine read id reused as the Observation `id` makes a re-send `409 id-collision` | §13.3a(1–3, 8) |
| APX-LPR-04 | `accessEvent` accepted from the LPR system and projected on `detail`, never inferred by the server; revisions via `PUT /v1/lpr/reads/{observation}/access-event` (`If-Match`, stale → 409 `version-conflict`, same value → no-op) publish `apx.data.observation.updated.v1`; `movement` accepts `toward`/`away` and the deprecated `approaching`/`receding`; `laneTravel` deprecated | §13.3a(3–4), §13.4 |
| APX-LPR-05 | **C** (lane has LPR cameras): `apds-ext:apx:lane-cameras@1.0` accepted and preserved on `VehicularAccess`, `cameraId` unique within the Place (duplicate → 422 `request-unprocessable`) and equal to `Image.cameraID` on its reads | §13.3a(5) |
| APX-LPR-06 | Reads link their Session (`LprRead.session`); `GET /v1/lpr/reads?session=` lists a Session's reads, a Session outside the grant → 403; Session start and end follow reads' capture time, never arrival time | §13.3a(6, 8), §13.5(4) |
| APX-LPR-07 | `GET /v1/lpr/reads/{observation}` and `POST /v1/lpr/reads:search` served with the lookup's grant and purge rules | §13.3 |
| APX-LPR-08 **C** (reviews reads) | `detail.review` state and reasons; `PUT /v1/lpr/reads/{observation}/plate` writes the decision, keeps the engine's values (with confidences) in `review.original` on the first correction and never changes them, bumps the Observation version, publishes `apx.data.observation.updated.v1`, honours `If-Match` (stale → 409 `version-conflict`); same decision and values → no-op (no version, no event); `corrected` without `plate`, or plate values with another decision → 422 `request-unprocessable`; no Session changed; without `apx.lpr:read` the 200 echoes only caller-supplied plate values | §13.3a(4a), §9.6 |
| APX-LPR-09 | `LprRead.images[]` lists every unpurged image with its kind; `plateExtended`/`context` carried as strings | §13.3c |
| APX-LPR-10 | Vehicle attributes of a read with no plate country kept in `detail` and `vehicleAncillaryIdentification` omitted, never dropped | §13.3a(1) |
| APX-LPR-11 | Passage facts (`accessEvent`, `movement`, `plateFace`, `frameReads`, `platesRead`, `captureGroup`, `speed`, `engine`) served under `apx.data:read` and on the observation topics to subscribers without `apx.lpr:read`; plate-bearing members only under `apx.lpr:*` | §13.3a(7), §9.6 r1 |

## A.11 `apx-reservations` / A.12 `apx-permits`

| ID | Requirement | Source |
|---|---|---|
| APX-RSV-01 | Reservations as native Quote→AssignedRight with `apds-ext:apx:reservation@1.0`; lifecycle per the Part 14 transition table (illegal → 409); `plannedUses[0]` authoritative and mirrored by the extension | §14 |
| APX-RSV-02 | Holder ids treated as local; cross-system correlation by plate; place-scoped history lookups; recent lookup requires `plate` or `holder` (both intersect) | §14.1a |
| APX-RSV-03 | `PUT /v1/sessions/{id}/assigned-right` materializes into the APDS Session (`segments[].assignedRight`) + SessionUpdated; unlinkable right, or a different right on a linked session → 409 `right-not-linkable`; unlink reverts the segment and the reservation state | §14.1b |
| APX-PRM-01 | Pooled issuance over RightSpecification/RightPool; exhaustion → 409 `pool-exhausted`; refusals per §14.2a; Idempotency-Key replay consumes no second slot | §14.2, §14.2a |
| APX-PRM-02 | Issued permit materialized onto the native AssignedRight per §14.2b (CustomerCredential holder, VehicleCredential per vehicle, PlannedUse + `expiry`); native `credential_id` filters resolve against the identification string; cancellation or expiry returns the slot | §14.2b, §14.2c |
| APX-PRM-03 **C** (multi-vehicle permits with a limit) | `apds-ext:apx:permit@1.1` `maxConcurrentVehicles` honoured; an overage billed per `overageBilling` or cited as `permitMisuse`; no pool slot moves | §14.2d |

## A.13 `apx-tolling`

| ID | Requirement | Source |
|---|---|---|
| APX-TOL-01 | TollTransaction idempotent create binding Observations→pricing→Payment reference | §15 |
| APX-TOL-02 | Dispute lifecycle; re-dispute of closed → 409 `dispute-closed` | §15 |
| APX-TOL-03 | Transitions per the §15.1 table (price, payment, void, dispute, resolve); anything else → 409 `toll-transition-illegal`; re-attaching the attached payment → 200 unchanged; `adjusted` moves the old amount to `dispute.originalPricing` | §15.1, §15.2 |

## A.14 `apx-resolution`

| ID | Requirement | Source |
|---|---|---|
| APX-RES-01 | Context resolves from any parking-domain identifiers; never fails for absent ones; telephony values accepted only as the comms-point members (`callerId`, `dialedNumber`, `commsDeviceId`), never SIP identifiers; a server without comms points → 422 `comms-point-unknown` on those members | §17.1–17.2 |
| APX-RES-02 | Partial results supported; context never includes sections the token couldn't read directly | §17.2 |
| APX-RES-03 | AllowedActions evaluated server-side; denied/gated actions carry machine-readable reasons | §17.3 |
| APX-RES-04 | Policy decisions BINDING on execution, on commands and on domain operations alike: 403 `action-not-allowed` / `approval-required`; a command type the named context does not offer → 403 `action-not-allowed` | §17.3 |
| APX-RES-05 | `recommendedAction` is one of allowedActions (runtime check); execution descriptors name only supported actions and satisfy the `AllowedAction` conditional rules | §17.3 |
| APX-RES-06 | Support interactions recorded (idempotent under `Idempotency-Key`), completable by versioned PUT, queryable by subject or `correlationId`; summaries not transcripts | §17.6 |
| APX-RES-07 **C** (tracks passback) | Passback read + resetPassback/forceIn/forceOut commands; a known but untracked credential → 200 `state: unknown` | §17.4, §17.8 |
| APX-RES-08 **C** (stores LPR) | Plate candidates read + `PUT /v1/sessions/{id}/plate` materializing into the APDS Session + SessionUpdated; stale `If-Match` → 409 `version-conflict`; closed session outside the dispute window → 422 `session-not-open` | §17.5 |
| APX-RES-09 | `interactionId` treated as opaque (echoed on the context, recordable on support interactions, never interpreted); a caller's `correlationId` carried on the context, commands, payment links, events, and support interactions of the same episode (SHOULD) | §17.7 |
| APX-RES-10 **C** (gates any action) | `POST /v1/approvals` opens a `pending` request with `role`, `action`, `target`, `requestedBy` and `correlationId`; `decide` records `approved`/`denied` with `decidedBy`; a decision on a non-pending request → 409 `approval-closed`; `expiresAt` honoured; every transition publishes `apx.approvals.changed.v1`; an `approval` citing `approvalRequest` is verifiable against the request | §17.3a |
| APX-RES-11 | `reservationIssuer` and `guestReference` accepted as qualifiers (never counted as identifying members); `commsPoint`, `callerId`, `dialedNumber`, `commsDeviceId` resolved to the lane when the comms point has one, else the place only | §17.1, §26.4 |
| APX-RES-12 **C** (claims `apx-ui-launch`) | `execution.type: ui` carries `surface`; the policy decision binds the launch; `SupportInteraction.resolution.code` from `apx-interaction-outcomes` or an implementer code; `actions[]` may reference `UiLaunch` and `UiActionRecord` | §17.3, §17.6, §25.5 |

## A.15 `apx-violations`

| ID | Requirement | Source |
|---|---|---|
| APX-VIO-01 | Violation idempotent create (Idempotency-Key semantics as APX-CTL-01); eligibility check performed and recorded at creation; entitled vehicles recorded as `dismissed` with basis | §19.1, §19.2 |
| APX-VIO-02 | Lifecycle per §19.1 with immutable `statusHistory[]`; terminal states `dismissed`, `closed`, `voided`; illegal transitions → 409 `violation-transition-illegal`; void from every non-terminal state (incl. `paid`), never deletes | §19.1 |
| APX-VIO-03 | `guided`/`manual` detections reviewed before issuance; unreviewed issue → 409 `violation-not-issuable`; automated unreviewed-issuance rule applied from `EnforcementPolicy.unreviewedIssuance` where present, otherwise published in operator documentation | §19.4 |
| APX-VIO-04 | One appeal per violation, from `issued` or (within the appeal window) `paid`; `upheld`/`reduced` return to the state opened from, `dismissed` closes; `appeal-closed` for a second appeal, a closed/voided violation, a passed window, or a resolve with no open appeal; `amount` changes only via `reduced` | §19.1, §19.6 |
| APX-VIO-05 | Settlement by Payment reference only (Part 13 takes the money, and returns it on a pay-then-appeal via `refund`); `paid` only from `issued` | §19.6 |
| APX-VIO-06 | `GET /v1/enforcement/eligibility` derivable from held APDS rights/sessions; ancestor-bound rights count; `basis[]` never exceeds the token's `/rights/assigned` grant; `suggestedViolationType` advisory | §19.3 |
| APX-VIO-07 | Evidence and `lastRead` imagery as access-controlled links; plate values only under `apx.violations:*`; retention published; appellant media treated as untrusted | §19.5, §9.6 |
| APX-VIO-08 | `apx.violations.detected.v1`, `.issued.v1`, `.status.v1` published; place-bound on `Violation.place` | §19.7 |
| APX-VIO-09 | `Violation.location` modelled on the APDS Observation Location shape (optional `observerLocation`, added `accuracyMetres`), GeoJSON [lon, lat]; guided/manual carry `observerLocation`; automated copies from the Observation | §19.9 |
| APX-VIO-10 | Policy in force resolved by nearest-ancestor-or-self at `detectedTime`; `…/policies/effective` returns exactly what `issue` applies; refusals per §19.10 rule 2 with the named problem types; `policy`, `signage[]`, `unpaidAmount`, `capAmount`, and the first `amountHistory` entry (`issued`, or `cap` with `requestedAmount`) frozen at issue | §19.10 |
| APX-VIO-11 | Escalation server-applied per schedule, never before `paymentGraceDays`, never while `appealed` (days appealed do not count), clamped at `overallCeiling`; each step appended to `amountHistory` and published; clients never compute penalties | §19.10 |
| APX-VIO-12 | Signage history immutable (text change on a referenced record → 422 `signage-referenced`); in-force signage frozen on the violation at issue; `signageRequired` enforced | §19.11 |
| APX-VIO-13 | Partial payments appended to `payments[]` as `ViolationAppliedPayment` (amount, `paidTime`, Part 13 `payment` or a source `reference`) and summed in `amountPaid`; `paid` only when `amountPaid` ≥ `amount`; refunds reduce `amountPaid` | §19.6a |
| APX-VIO-14 | `POST /v1/enforcement/violation-imports` idempotent and keyed on `system`+`recordId`; higher `sourceRevision` (else later `sourceUpdatedTime`) updates, the same is a no-op, older is 409 `version-conflict`; at or past `issued` without `notice` or `amount` → 400 `invalid-request`; `paid` with `amountPaid` < `amount` → 422 `request-unprocessable`; `statusHistory[]` append-only, source times kept; no eligibility check or cap applied; `GET /v1/violations` filters `sourceSystem` and `sourceRecordId` | §19.13 r1–4, §19.2 |
| APX-VIO-15 | `POST /v1/violations/{id}/session` with `If-Match` (stale → 409 `version-conflict`) sets, corrects, or clears (`session: null`) `relatedSession` and writes the match-evidence decoration; valid in every state, terminal included; no `statusHistory[]` entry; a change bumps `version` and publishes `status.v1`, identical content is a no-op; unknown or invisible Session → 422 `reference-unknown`, Session outside the place hierarchy → 422 `request-unprocessable` | §19.13 r6 |
| APX-VIO-16 | `issue` refused with 409 `violation-not-issuable`, whatever the detection mode, while any LPR read in `observations[]` has plate review `pending` or `rejected` | §19.4 r6, §13.3a(4a) |
| APX-VIO-17 | `issue` refused with 409 `violation-not-issuable` when the `relatedSession` link's evidence (`apds-ext:apx:match-evidence@1.0` on the Violation, or the related Session's `session-detail.matches[]` entry for it) has `plate.method: lenient` or `applied: false` | §19.4 r5, §5.8 r5 |
| APX-VIO-18 | `EnforcementPolicy.stayGrace` `entryMinutes` and `exitMinutes` accepted as independent optional integers ≥ 0 (absent = no grace on that side, `0` = none) and inherited and versioned with the policy; `EligibilityResult.graceUntil` derived from the `stayGrace` of the policy in force at `checkedTime` (entry side while no right covers the stay, exit side once its rights have ended), absent when that side has no grace or a right covers the stay | §19.10 r7, §19.3 r5 |

## A.16 `apx-validations`

| ID | Requirement | Source |
|---|---|---|
| APX-VAL-01 | Program lifecycle `active ⇄ suspended → ended` via versioned `PUT` (`If-Match` or body `version`, Part 4 §4.2a); stale version → 409 `version-conflict`; out of `ended` → 422 `program-not-active`; ambiguous benefit → 422 `request-unprocessable`; `statusHistory[]` immutable | §20.1 |
| APX-VAL-02 | Provider list (Part 6) derived from active programs covering the queried place or an ancestor, each row carrying `program`; `applyValidation` materializes a `ValidationRedemption` | §20.1, §6.3 |
| APX-VAL-03 | Issuance idempotent, program taken from the path; `codes[]` returned exactly once; codes ≥ 64 bits entropy and implementation-unique; unknown/out-of-grant code → 404 never 403; batches and single codes voidable, redeemed codes unaffected | §20.2 |
| APX-VAL-04 | Redemption idempotent; rule set evaluated in order with 422 `program-not-active` / `instrument-invalid` / `redemption-limit-exceeded` (place mismatch → `request-unprocessable`); `stackable` evaluated both ways; APDS-native validation record materialized and returned as `validationId`; `amountReduced` is the actual effect | §20.3 |
| APX-VAL-05 | Reversal restores amount due (open session) and instrument; already reversed → 409 `redemption-reversed`; a redemption on a closed statement is reversed without editing it and credited on the next statement | §20.4 |
| APX-VAL-06 | `apx.validations:redeem` confined to the token's own provider (`apx_org`) for reads, issuance, voiding, redemption, and events; another provider's resource → 404; cannot enrol, reverse, or close | §20.5 |
| APX-VAL-07 | Statement preview computable for any period; closed statements immutable and non-overlapping (409 `statement-overlap`); post-closure reversals appear as `credit` lines on the next statement, `billableAmount` net of them | §20.6 |
| APX-VAL-08 | `apx.validations.redeemed.v1`, `.program.status.v1`, `.statement.closed.v1` published; place-bound | §20.7 |

## A.17 `apx-credentials`

| ID | Requirement | Source |
|---|---|---|
| APX-CRD-01 | Lifecycle per §21.1 with immutable `statusHistory[]`; illegal transitions (including `replace` from `issued`) → 409 `credential-transition-illegal`; terminal replace → 409 `credential-not-replaceable`; identification unique per type among non-terminal records → 409 `credential-identification-in-use` whose `detail` identifies no record, holder, or place; a window already over (`validity.end`, suspend `until`) → 422 `request-unprocessable` | §21.1 |
| APX-CRD-02 | Active records materialized as APDS `CredentialAssigned` (`identifier` → the record) on every listed AssignedRight; removed/ended on suspend, lost, revoke, expire; restored on resume; lane denies non-active credentials from the transition instant | §21.2 |
| APX-CRD-03 | `replace` idempotent and atomic on the AssignedRight (one `AssignedRightUpdated`); `replaces`/`replacedBy` linked; deposit settled per `oldDepositStatus`; the predecessor retains all other fields | §21.3 |
| APX-CRD-04 | Access events recorded with outcome and seeded `denialReason`; unmatched presentations recorded as `AccessEvent` with `presented`; `GET …/{id}/access-events` and `GET /v1/access-events` served, grant-scoped; `apx.credentials.access.v1` published; passback corrections remain Part 17 | §21.4 |
| APX-CRD-05 | Identification, serials, and access events under Part 9 §9.6; no identification-in-use oracle outside `apx.credentials:manage` | §21.5 |
| APX-CRD-06 | `apx.credentials.status.v1` on every transition including server-side `expired`, timed resume, and activation at `validity.start` (`activateOnStart`) | §21.1, §21.6 |

## A.18 `apx-valet`

| ID | Requirement | Source |
|---|---|---|
| APX-VLT-01 | Lifecycle per §22.1 with immutable `statusHistory[]`, every state reachable by a route (`pickup`, `cancel`, re-`park` from `staged`); illegal transitions → 409 `valet-transition-illegal`; `retrieve` from `dropped` → 409 `valet-vehicle-not-located`; drop-off idempotent and ignores client-sent server-written members; `park` honours `If-Match` (409 `version-conflict`) | §22.1 |
| APX-VLT-02 | ValetTicket references the APDS Session (and AssignedRight where issued) and never restates the stay or the money; `closed` follows Session settlement | §22, §22.1, §22.4 |
| APX-VLT-03 | Drop-off condition report immutable after `dropped` (corrections appended through `POST …/condition` as new dated entries); imagery as access-controlled links; `customerAcknowledged` captured where obtainable | §22.2 |
| APX-VLT-04 | `retrieve` sets `etaMinutes` (minutes until `promisedTime`, never negative) and `promisedTime` and publishes `apx.valet.retrieval.requested.v1`; a repeat while `requested`/`retrieving` is 200 with nothing republished; queue ordered by `promisedTime`; scheduled pickups surface inside the horizon | §22.3 |
| APX-VLT-05 | Handback verifies the claimant; failure → 403 `valet-verification-failed` recorded in `statusHistory`; `verificationValue` never stored; handback condition report recorded | §22.4 |
| APX-VLT-06 | `apx.valet:request` confined to the caller's own ticket(s) — any other ticket → 404 `target-not-found`; operator tokens outside the grant → 403 `insufficient-grant`; minimized read returns exactly the §22.5 member list (no `statusHistory`, storage, key tag, or condition report); can only read, retrieve, cancel-retrieval | §22.5 |
| APX-VLT-07 | No raw phone/e-mail on the ticket (`contactChannel.handle` opaque/masked; SHOULD refuse with 422 `personal-data-not-permitted`); condition-imagery retention published | §22.6 |
| APX-VLT-08 | `apx.valet.ticket.status.v1` on every transition; place-bound on `ValetTicket.place` | §22.7 |

## A.19 `apx-mtls`

| ID | Requirement | Source |
|---|---|---|
| APX-TLS-01 | Client certificate required on every APX and APDS-native route except `/.well-known/apx-configuration`; TLS 1.2 minimum (1.3 RECOMMENDED); a connection without one is refused at the handshake | §9.1, §16.3 |
| APX-TLS-02 | `/.well-known/apx-configuration` retrievable without a client certificate and lists `apx-mtls` | §16.3 |
| APX-TLS-03 | Tokens certificate-bound per RFC 8705 (SHOULD); a token whose binding does not match the connection certificate → 401 `unauthenticated` | §16.3 |

## A.20 Implementation Conformance Statement (template)

An ICS is a filled-in copy of this annex plus the header below. Publish it
with the implementation's documentation; `/.well-known/apx-configuration`
MUST advertise exactly the classes the ICS claims.

```
Implementation: <product, version>
Supplier:       <organisation>
APX edition:    <e.g. 0.3.0>   APDS version: 4.1
Date:           <ISO 8601>

Classes claimed: [ ] apx-data  [ ] apx-events  [ ] apx-events-sse
  [ ] apx-control  [ ] apx-alerts  [ ] apx-discovery  [ ] apx-accounts
  [ ] apx-payment-history  [ ] apx-lpr  [ ] apx-reservations
  [ ] apx-permits  [ ] apx-tolling  [ ] apx-resolution  [ ] apx-violations
  [ ] apx-validations  [ ] apx-credentials  [ ] apx-valet  [ ] apx-mtls
  [ ] apx-charging  [ ] apx-plate-lists  [ ] apx-ui-launch
  [ ] apx-site-config

For each requirement row of every claimed class (and A.1):
  <ID>: PASS | N/A (conditional not applicable) | DEVIATION (explain)
```

A claim with any unexplained non-PASS row on a claimed class is
non-conforming (§3.1: no partial classes).

## A.21 `apx-charging` (Part 23)

| ID | Requirement | Source |
|---|---|---|
| APX-EVC-01 | ChargingPointStatus keyed by Reference to the APDS ElectricChargingEquipment; carries `refillPointIndex`/`evseId`; static descriptive fields never duplicated; bridge `PUT …/status` cannot write `bay` | §23.1 |
| APX-EVC-02 | Lifecycle per §23.2 driven by `ChargingEvent`s applied in time order with immutable `statusHistory[]`; illegal transitions → 409 `charging-transition-illegal`; open on an unavailable point → 409 `charging-point-unavailable` | §23.2 |
| APX-EVC-03 | `complete → idle` server-side after `idlePolicy.graceMinutes`; `idleStartedTime` set; `apx.charging.idle.started.v1` published | §23.2, §23.12 |
| APX-EVC-04 | Bay presence fused per the §23.3 table; `vehicleNotPluggedIn`/`blocked` yield `availability: blocked` when no cable is connected (a connected cable wins); camera Observation carried as evidence; `bay.vehicle`/`imageLink` operator-scope only | §23.1, §23.3 |
| APX-EVC-05 | `deliveredKwh` never derived from time × rated power; absent energy means unreported | §23.4 |
| APX-EVC-06 | `settlement.mode` honoured: `parkingSession` requires the Session reference and refuses `…/payment` (409 `charging-settlement-conflict`); `directPayment` closes on the payment attached after `unplugged` (earlier → 409); `chargingNetwork` records energy and idle without money; `idlePolicy` snapshotted, never re-priced, idle billed per started unit | §23.5 |
| APX-EVC-07 | `apx.charging:status` confined to the caller's own session(s) and the place's point availability; reads minimized exactly per the §23.6 omit-lists | §23.6 |
| APX-EVC-08 | `OccupancySnapshot.evCharging` populated where point status is held | §23.7 |
| APX-EVC-09 | Charger actions only via Part 6 commands; `confirmationLevel` discipline applies | §23.9 |
| APX-EVC-10 | `apx.charging.session.status.v1` on every transition; `apx.charging.point.status.v1` on availability/presence change; place-bound on `.place` | §23.12 |

## A.22 `apx-plate-lists` (Part 24)

| ID | Requirement | Source |
|---|---|---|
| APX-PLL-01 | Lists with a registry or implementer `category`, an owning organisation, per-place flags, and `status`; every `places[].place` written inside the writer's grant (403 `insufficient-grant`) | §24.1 |
| APX-PLL-02 | List visibility: a place in the grant, or the caller's `apx_org` owns it; versioned `PUT` (409 `version-conflict`) | §24.1, Part 9 §9.3a |
| APX-PLL-03 | Entries with an APDS `CredentialTypeEnum` credential and validity; adding a credential already on the list with overlapping validity updates it (200) | §24.2 |
| APX-PLL-04 | Plate values only under `apx.platelists:*`; digests MAY stand in (Part 9 §9.6 r6) | §24.2 |
| APX-PLL-05 | Change feed on `/v1/plate-lists` and `/v1/plate-lists/{id}/entries`; entry removal tombstoned | §24.3, Part 5 §5.2 r8 |
| APX-PLL-06 | `apx.platelists.changed.v1` on every list or entry change, carrying no plate values | §24.3 |

## A.23 `apx-ui-launch` (Part 25)

| ID | Requirement | Source |
|---|---|---|
| APX-UIL-01 | Offered surfaces are `apx-ui-surfaces` values listed in `uiSurfaces[]` of the bootstrap and discovery documents under the soundness rule; an unoffered surface → 422 `ui-surface-unknown`; an unlisted display mode → 422 `ui-display-mode-unsupported` | §25.1 |
| APX-UIL-02 | Handshake mode: `POST /v1/ui/launches` returns 201 with a single-use `url` valid at most 60 s (`expiresIn` clamped), bound to the surface, subject, agent and place; the agent is logged in automatically; `Idempotency-Key` replay → 200 | §25.2 |
| APX-UIL-03 **C** (offers signed tokens) | A JWS (`ES256`/`RS256`, never `none`) with `aud` the implementation, `exp` ≤ 60 s after `iat`, unique `jti`, and the request members as claims, delivered by form POST or URL fragment, opens the surface and creates the launch; expired, replayed or badly signed tokens refused | §25.2, §9.8 |
| APX-UIL-04 | Lifecycle `issued → opened → closed \| expired` with `openedAt`/`closedAt`; a second use of the url or token refused; action or close on a terminal launch → 409 `ui-launch-closed`, on an expired issued launch → 409 `ui-launch-expired`; every transition publishes `apx.ui.launch.changed.v1`; reads never return a usable `url` after `issued` | §25.2, §25.3 |
| APX-UIL-05 | Loopback, native first: every action with a supported APX operation produces that operation's own record and events, carrying the launch's `correlationId`, `agent` and `agentType` | §25.4 |
| APX-UIL-06 | Every other action recorded as a `UiActionRecord` (`action`, `outcome`, `occurredAt`, `summary`, `result` where any) via `POST /v1/ui/launches/{id}/actions` or an equivalent internal record, publishing `apx.ui.action.recorded.v1`; no action inferred from an opened launch | §25.4 |
| APX-UIL-07 | `returnFields[]` answered in `fields` by pointer, each unsupplied field listed in `unavailable[]` with `notSupported` or `notApplicable`, never omitted silently | §25.4 |
| APX-UIL-08 | Launch material never in a query string, referrer or log; no personal data or secrets in a url or token; `embed` only for registered origins with `frame-ancestors` set, else `popup`/`tab`; the opened session ends with the launch; signing keys and handshake secrets rotatable and never returned by a read | §25.6, §9.7, §9.8 |
| APX-UIL-09 **C** (claims `apx-resolution`) | `AllowedAction.execution.type: ui` offered with `surface` where no API operation exists; the decision binds the launch; launches and action records referenced from `SupportInteraction.actions[]` | §25.5 |

## A.24 `apx-site-config` (Part 26)

| ID | Requirement | Source |
|---|---|---|
| APX-SCF-01 | Every SupplementalEquipment served carries `apds-ext:apx:equipment-detail@1.0` with a `role` from `apx-equipment-roles` (a code, never free text); `equipmentType` still the nearest APDS value; `macAddresses[]` twelve lower-case hex digits; no IP address member | §26.1 |
| APX-SCF-02 | `GET /v1/equipment?place=` with `role`, `area`, `lane`, `level` filters and the Part 5 change feed; `GET /v1/equipment/{id}`; a `place` outside the grant → 403 `insufficient-grant`; the same elements and decorations served on the native `/places` routes | §26.1 r9 |
| APX-SCF-03 | `area` from `apx-area-types`; `level`, `lane`, `builtInto`, `controls[]`, `serviceProvider` as defined; `firmwareVersion`/`softwareVersion` readable by every client that may read the place | §26.1, §26.2 |
| APX-SCF-04 **C** (inventories cameras) | A camera is `role: camera` with `apds-ext:apx:camera@1.0`: `view[]` (≥ 1), `mounting` (with `builtInto` when `builtIn`), `faces` on lanes/plazas/ramps, `recorder`, `streams[]`, `retentionDays`; no address, user name or password; `lane-cameras@1.0` unchanged | §26.3 |
| APX-SCF-05 **C** (offers `cameraViews`/`cameraPlayback`) | `POST /v1/cameras/{id}/views` returns 201 with a single-use `url` bound to the agent, `expiresAt` ≤ 5 min; `playback` honours `from`/`to`; offline or unsupported kind → 409 `camera-view-unavailable`; range outside `retentionDays` → 422 `playback-range-unavailable`; every issue and use logged against agent and `correlationId`; the feature listed in discovery | §26.3, §9.8 |
| APX-SCF-06 | `CommsPoint` with a registry `type`, `place`, optional `lane`/`equipment`/`serves`/`coverage[]`, and at least one of `phoneNumber` (E.164), `callerId` (E.164), `commsDeviceId`; no SIP user name, realm or password accepted (422 `personal-data-not-permitted` for a password) | §26.4, §9.7 |
| APX-SCF-07 | `GET /v1/comms-points` honours `place`, `lane`, `type`, `callerId`, `dialedNumber` (= `phoneNumber`), `commsDeviceId` and serves the change feed; `POST`/`PUT`/`DELETE` under `apx.siteconfig:manage` with versioned `PUT` (409 `version-conflict`) and tombstoned `DELETE`; a comms point resolves to its lane when it has one, else its place only | §26.4 |
| APX-SCF-08 | `apds-ext:apx:place-operations@1.0` carries `commsAccountId` and `escalationContacts[]` as `apx-escalation-roles` values with `order` and `hours`; no names or numbers | §26.4 r5, §26.5 |
| APX-SCF-09 | `apds-ext:apx:external-ids@1.0` accepted and preserved on any HierarchyElement with `system` a URI; `POST /v1/identifiers:resolve` answers `target`, `className`, `place` for a held pair and 404 `target-not-found` otherwise, never confirming an element outside the grant | §26.6 |
| APX-SCF-10 | `apx.equipment.changed.v1` on every create, change or removal of a decorated SupplementalEquipment; `apx.commspoints.changed.v1` on every comms-point change; removals carry the last representation; both lists serve the change feed | §26.7 |
| APX-SCF-11 | No read, event or feed page of this Part carries a PARCS key, camera or device password, or credential-bearing stream address, in any member or nested text | §9.7 |
| APX-SCF-12 | `POST /v1/display-records` under `apx.displays:record` (Idempotency-Key optional), immutable once written; `display` a SupplementalEquipment (else 422 `reference-unknown`), `place` derived from it (a disagreeing body `place` → 422 `request-unprocessable`); `messageKind` a registry value or implementer code; `shownTime` required and the Part 6 §6.1a display members as defined; no message text or plate stored; with `command`, `shownTime` and `contentHash` agree with its `CommandResult.display` (else 422 `request-unprocessable`) | §26.9 r1–5, §9.6 r8 |
| APX-SCF-13 | `GET /v1/display-records` honours `place`, `display`, `messageKind`, `session`, `observation`, `since`/`until` and serves the change feed; `GET /v1/display-records/{id}`; `apx.displays.shown.v1` published for every record, place-bound | §26.9 r6–7 |

