# REST API Specification: [API name] v[1]

> Complete specification for an HTTP/JSON API: environments, security,
> conventions, versioning, pagination, idempotency, rate limits, errors as
> RFC 9457 problem details, endpoint-by-endpoint contracts, streaming
> responses, webhooks, an OpenAPI 3.2 skeleton, changelog and service
> levels. Based on HTTP semantics (RFC 9110), Problem Details for HTTP APIs
> (RFC 9457) and the OpenAPI Specification 3.2.0 (published September
> 2025). Replace every `[bracketed]` value and every row marked *Example*.
> Worked examples use an order-management API.

<!--
Guidance: How to use this template
- Fill sections 3 to 9 (the cross-cutting rules) before writing any endpoint.
  Endpoints that each invent their own pagination or error shape are the most
  common defect in API specifications.
- Keep the OpenAPI document (section 12) as the machine-readable source of
  truth and this document as the human contract. When they disagree, fix both
  in the same change.
- Every example request and response must be valid against the schema. Copy
  them from a real test run when the API exists.
- Write new descriptions as `openapi: 3.2.0`. Version 3.2 is a minor
  release, so an existing 3.1 document upgrades by changing the version
  field; then adopt the new features in section 12.2 where they help.
- Delete sections that do not apply (for example webhooks) but leave a one-line
  note saying why, so reviewers know it was a decision and not an omission.
-->

## Document control

<!-- Guidance: Identify the specification and the API version it describes. A specification is a contract: record who approved it. -->

| Field | Value |
|---|---|
| Document ID | [API-SPEC-ORDERS-001] |
| API name | [Orders API] |
| API version described | [v1] |
| Document version | [0.1] |
| Status | [Draft / In review / Approved / Deprecated] |
| Owner (team) | [@team-orders] |
| Technical contact | [@api-owner] |
| Classification | [Internal / Partner / Public] |
| Last updated | YYYY-MM-DD |
| Related | [OpenAPI file path], [System design], [Security architecture] |

### Approvals

| Role | Name | Decision | Date |
|---|---|---|---|
| API owner | [@name] | [Approved / Changes requested] | YYYY-MM-DD |
| API governance / design review | [@name] | [Decision] | YYYY-MM-DD |
| Security | [@name] | [Decision] | YYYY-MM-DD |
| Consumer representative | [@name] | [Decision] | YYYY-MM-DD |

## Revision history

<!-- Guidance: Record contract changes here and in the public changelog (section 13). Mark breaking changes clearly. -->

| Version | Date | Author | Change summary |
|---|---|---|---|
| 0.1 | YYYY-MM-DD | [@name] | First draft for design review |
| 0.2 | YYYY-MM-DD | [@name] | *Example:* Switched list endpoints from offset to cursor pagination |
| 1.0 | YYYY-MM-DD | [@name] | *Example:* Approved for general availability |

## Contents

<!-- Guidance: Keep this list aligned with the headings. -->

1. [Overview](#1-overview)
2. [Base URLs and environments](#2-base-urls-and-environments)
3. [Authentication and authorisation](#3-authentication-and-authorisation)
4. [Conventions](#4-conventions)
5. [Versioning and deprecation](#5-versioning-and-deprecation)
6. [Pagination, filtering and sorting](#6-pagination-filtering-and-sorting)
7. [Idempotency and concurrency](#7-idempotency-and-concurrency)
8. [Rate limiting](#8-rate-limiting)
9. [Errors](#9-errors)
10. [Resources and endpoints](#10-resources-and-endpoints)
11. [Webhooks](#11-webhooks)
12. [OpenAPI 3.2 document](#12-openapi-32-document)
13. [Changelog](#13-changelog)
14. [Service levels and support](#14-service-levels-and-support)
15. [Glossary and references](#15-glossary-and-references)

## 1. Overview

<!-- Guidance: One page a new consumer can read alone. What the API does, who it is for, what it does not do, and the main resources. -->

### 1.1 Purpose

[What business capability the API exposes and for whom. Example: "The
Orders API lets partner storefronts create, read, update and cancel
customer orders, and subscribe to order status changes."]

### 1.2 Audience and scope

| Item | Value |
|---|---|
| Intended consumers | *Example:* Partner storefronts, internal mobile apps, the support console |
| In scope | *Example:* Orders, order lines, shipments, refunds |
| Out of scope | *Example:* Product catalogue and pricing (see Catalogue API), payment capture |
| API style | REST over HTTPS, JSON bodies, resource-oriented URLs |
| Maturity | [Beta / Generally available / Deprecated] |

### 1.3 Resource model

The main resources and how they relate. Each resource has a stable,
opaque identifier and its own collection URL.

```mermaid
classDiagram
  class Order {
    id: string
    status: OrderStatus
    customerId: string
    totalAmount: Money
    createdAt: datetime
  }
  class OrderLine {
    id: string
    sku: string
    quantity: int
    unitPrice: Money
  }
  class Shipment {
    id: string
    carrier: string
    trackingNumber: string
  }
  class Refund {
    id: string
    amount: Money
    reason: string
  }
  Order "1" --> "1..*" OrderLine
  Order "1" --> "0..*" Shipment
  Order "1" --> "0..*" Refund
```

## 2. Base URLs and environments

<!-- Guidance: List every environment a consumer may call, with its data policy. Consumers must never need production credentials to integrate. -->

| Environment | Base URL | Data | Credentials | Rate limit tier |
|---|---|---|---|---|
| Production | `https://api.[example].com/orders/v1` | Live customer data | Production client | Contracted tier |
| Sandbox | `https://sandbox.api.[example].com/orders/v1` | Synthetic, reset weekly on Sunday 00:00 UTC | Sandbox client, self-service | 10 requests/s |
| Staging (internal) | `https://staging.api.[example].internal/orders/v1` | Masked copy of production | Internal only | 50 requests/s |
| [Environment] | [URL] | [Data policy] | [Credentials] | [Tier] |

All environments accept TLS 1.2 or later only. Plain HTTP requests are
rejected, not redirected, so that credentials are never sent in clear text.

## 3. Authentication and authorisation

<!-- Guidance: State how a caller proves identity and what it may do. Prefer OAuth 2.0 client credentials for server-to-server calls and authorization code with PKCE for user-delegated access. Never accept credentials in query strings. -->

### 3.1 Authentication methods

| Method | Use for | How to send | Lifetime |
|---|---|---|---|
| OAuth 2.0 client credentials | Server-to-server integrations | `Authorization: Bearer <access_token>` | Access token 15 min |
| OAuth 2.0 authorization code with PKCE | Apps acting for a signed-in user | `Authorization: Bearer <access_token>` | Access token 15 min, refresh token 30 days |
| OAuth 2.0 device authorization grant (RFC 8628) | *Example:* Warehouse handheld scanners and TV dashboards without a browser | `Authorization: Bearer <access_token>` after the user approves the code on another device | Access token 15 min |
| API key (deprecated) | *Example:* Read-only internal dashboards; being replaced by client credentials, sunset YYYY-MM-DD | `X-API-Key: <key>` header | Rotated every 90 days |
| [Method] | [Use] | [Header] | [Lifetime] |

Token endpoint: `[https://auth.example.com/oauth2/token]`. Tokens are JWTs
signed with `[RS256 / ES256]`; consumers must treat them as opaque. The
authorization server publishes RFC 8414 metadata at
`[https://auth.example.com/.well-known/oauth-authorization-server]`, and
the OpenAPI document points to it with `oauth2MetadataUrl` so tools can
discover endpoints instead of hard-coding them. Schemes that are being
retired are marked `deprecated: true` in the OpenAPI document (section 12).

### 3.2 Scopes

| Scope | Grants | Typical consumer |
|---|---|---|
| `orders:read` | Read orders, lines, shipments and refunds | Storefronts, support console |
| `orders:write` | Create and update orders, cancel unshipped orders | Storefronts |
| `refunds:write` | Create refunds up to the order total | Support console |
| `webhooks:manage` | Register and delete webhook subscriptions | Partner back ends |
| [scope] | [Grant] | [Consumer] |

### 3.3 Authorisation rules

- A caller sees only orders belonging to its tenant (`tenantId` in the
  token). Requests for another tenant's order return `404`, not `403`, so
  that identifiers cannot be probed.
- Missing or invalid credentials return `401` with a `WWW-Authenticate`
  header. A valid token without the required scope returns `403`.
- [Additional rule, for example field-level restrictions on personal data.]

## 4. Conventions

<!-- Guidance: Rules that apply to every endpoint. Decide once and link here instead of repeating in each endpoint. -->

### 4.1 Resource naming and URLs

- Collections are plural nouns in kebab-case: `/orders`, `/order-lines`.
- Single resources: `/orders/{orderId}`. Nesting is limited to one level
  where the child cannot exist alone: `/orders/{orderId}/shipments`.
- Actions that do not map to CRUD use a sub-resource verb with `POST`:
  `/orders/{orderId}/cancel`.
- URLs never contain file extensions, trailing slashes or verbs for CRUD.

### 4.2 HTTP methods

Methods follow RFC 9110 semantics.

| Method | Meaning | Safe | Idempotent | Typical success |
|---|---|---|---|---|
| GET | Read a resource or collection | Yes | Yes | 200 |
| POST | Create a resource or run an action | No | No (yes with `Idempotency-Key`) | 201 or 202 |
| PUT | Replace a resource completely | No | Yes | 200 |
| PATCH | Partial update with JSON Merge Patch (RFC 7396) | No | No | 200 |
| DELETE | Remove or disable a resource | No | Yes | 204 |
| QUERY | Search with a request body (complex filters too long or sensitive for a query string) | Yes | Yes | 200 |

`QUERY` is the safe, idempotent method with a request body being
standardised by the IETF HTTP working group; OpenAPI 3.2 describes it with
the `query` operation field. Offer it only where gateways, proxies and
client libraries in the consumer base support it, and keep an equivalent
`POST /orders/search` while support is uneven. Any other non-standard
method the API uses (for example `PURGE` on a cache endpoint) is described
under `additionalOperations` in the OpenAPI document, never by overloading
`POST`.

### 4.3 Data formats

| Topic | Rule | Example |
|---|---|---|
| Media type | `application/json; charset=utf-8` for bodies; errors use `application/problem+json` | |
| Property casing | camelCase | `customerId` |
| Enumerations | UPPER_SNAKE_CASE strings; consumers must tolerate unknown values | `PENDING_PAYMENT` |
| Dates and times | ISO 8601 / RFC 3339 in UTC with `Z` | `2026-03-14T09:30:00Z` |
| Dates only | ISO 8601 calendar date | `2026-03-14` |
| Money | Object with decimal string amount and ISO 4217 currency | `{"amount": "149.90", "currency": "EUR"}` |
| Identifiers | Opaque strings with a type prefix; never sequential integers | `ord_01HV6Z3K8Q` |
| Null and absent | Absent means "not set"; `null` only where documented | |
| Unknown fields | Consumers must ignore unknown response fields | |

### 4.4 Standard headers

| Header | Direction | Purpose |
|---|---|---|
| `X-Request-Id` | Both | Correlation ID; echoed in responses and logs; generated if absent |
| `Idempotency-Key` | Request | Safe retries for `POST` (section 7) |
| `ETag` / `If-Match` | Both | Optimistic concurrency (section 7.2) |
| `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` | Response | Quota state (section 8) |
| `Deprecation`, `Sunset`, `Link` | Response | Lifecycle signals (section 5) |

### 4.5 Streaming and sequential responses

Long exports and live feeds are streamed as a sequence of items rather
than one large JSON document, so clients can process each item as it
arrives.

| Use | Media type | Item | Notes |
|---|---|---|---|
| *Example:* Bulk export of orders | `application/jsonl` (JSON Lines) | One `Order` per line | Client reads line by line; connection closes at the end |
| *Example:* Live order status feed | `text/event-stream` (server-sent events) | One event per status change | Reconnect with `Last-Event-ID` to resume |
| [Use] | [Media type] | [Item schema] | [Notes] |

In OpenAPI 3.2 these responses declare `itemSchema` on the media type, so
the schema validates each item in the stream independently; `schema`
alone would describe the whole stream as one array.

## 5. Versioning and deprecation

<!-- Guidance: Define what counts as a breaking change and how long consumers have to move. Promises here are contractual. -->

### 5.1 Versioning scheme

The major version is in the path (`/v1`). Non-breaking changes are added
to the current major version without notice beyond the changelog.

| Change | Breaking? |
|---|---|
| Add an optional request field or a new response field | No |
| Add a new endpoint or a new enum value | No (consumers must tolerate unknown values) |
| Remove or rename a field, endpoint or enum value | Yes |
| Make an optional request field required | Yes |
| Change a field type, format or meaning | Yes |
| Tighten validation or change default sort order | Yes |

### 5.2 Deprecation policy

- Breaking changes ship in a new major version. The previous major version
  remains available for at least [12] months after the new one is GA.
- Deprecated endpoints return the `Deprecation` header (RFC 9745) with the
  date it was deprecated, and the `Sunset` header (RFC 8594) with the
  removal date, plus a `Link` header pointing to the migration guide.
- Consumers are notified by [email to registered contacts and the developer
  portal] at deprecation, at 90 days and at 30 days before sunset.

```http
HTTP/1.1 200 OK
Deprecation: @1767225600
Sunset: Wed, 30 Jun 2027 23:59:59 GMT
Link: <https://developer.example.com/orders/migrate-v2>; rel="deprecation"
```

## 6. Pagination, filtering and sorting

<!-- Guidance: Use one pagination style for every collection. Cursor pagination is stable under inserts and scales; offset pagination is simpler but skips or repeats items when data changes. -->

### 6.1 Cursor pagination

Collections return a page of items and an opaque cursor for the next page.
Cursors expire after [24 hours] and must not be parsed by consumers.

| Parameter | Type | Default | Maximum | Meaning |
|---|---|---|---|---|
| `limit` | integer | 25 | 100 | Items per page |
| `cursor` | string | none | | Cursor from the previous response's `nextCursor` |

```json
{
  "data": [{"id": "ord_01HV6Z3K8Q", "status": "SHIPPED"}],
  "nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTAzLTE0VDA5OjMwOjAwWiJ9",
  "hasMore": true
}
```

### 6.2 Filtering

Filters are query parameters named after the field. Multiple values are
comma-separated and combined with OR; different fields combine with AND.

| Pattern | Example | Meaning |
|---|---|---|
| Equality | `status=SHIPPED,DELIVERED` | Status is SHIPPED or DELIVERED |
| Range | `createdAt[gte]=2026-03-01T00:00:00Z&createdAt[lt]=2026-04-01T00:00:00Z` | Created in March 2026 |
| Search | `q=smith` | Free-text match on customer name and email |

### 6.3 Sorting

`sort` takes a comma-separated list of fields; a leading `-` means
descending. Default: `-createdAt`. Only fields listed per endpoint are
sortable; others return `400` with problem type `invalid-sort`.

## 7. Idempotency and concurrency

<!-- Guidance: Networks fail. Say exactly what a client may retry and what happens when two clients update the same resource. -->

### 7.1 Idempotency keys

- Every `POST` that creates a resource or moves money accepts an
  `Idempotency-Key` header (a client-generated UUID).
- The server stores the key, request fingerprint and response for
  [24 hours]. A repeat with the same key and body returns the stored
  response with header `Idempotent-Replayed: true`.
- The same key with a different body returns `422` with problem type
  `idempotency-key-reused`. A request still in progress returns `409`.

### 7.2 Optimistic concurrency

`GET` responses include a strong `ETag`. `PUT` and `PATCH` require
`If-Match`; a stale ETag returns `412 Precondition Failed`, and a missing
one returns `428 Precondition Required`.

## 8. Rate limiting

<!-- Guidance: Publish the limits and the headers so clients can back off before they are throttled. -->

| Tier | Sustained rate | Burst | Scope |
|---|---|---|---|
| Sandbox | 10 requests/s | 20 | Per client ID |
| Standard | 50 requests/s | 100 | Per client ID |
| Premium | [200] requests/s | [400] | Per client ID |
| Write endpoints | [20] requests/s | [40] | Per client ID, in addition to tier |

Each response carries `RateLimit-Limit`, `RateLimit-Remaining` and
`RateLimit-Reset` (seconds until the window resets). When the limit is
exceeded the API returns `429` with a `Retry-After` header. Clients
should retry with exponential backoff and jitter:

$$
t_{wait} = \min\left(t_{max},\; t_{base} \cdot 2^{n}\right) + \text{random}(0, t_{base})
$$

where $n$ is the retry attempt, $t_{base}$ = [0.5 s] and $t_{max}$ = [30 s].

## 9. Errors

<!-- Guidance: All errors use RFC 9457 problem details. Keep a registry of problem types so consumers can switch on `type` rather than parse `detail`. -->

### 9.1 Error format

Error responses use media type `application/problem+json` with the
standard members `type`, `title`, `status`, `detail` and `instance`, plus
the documented extension members below.

```http
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
X-Request-Id: 4b1f0c2e-1d3a-4c7a-9a77-0b0c6a5e2f11

{
  "type": "https://api.example.com/problems/validation-failed",
  "title": "Request validation failed",
  "status": 422,
  "detail": "2 fields are invalid.",
  "instance": "/orders/v1/orders",
  "requestId": "4b1f0c2e-1d3a-4c7a-9a77-0b0c6a5e2f11",
  "errors": [
    {"pointer": "/lines/0/quantity", "detail": "must be at least 1"},
    {"pointer": "/shippingAddress/countryCode", "detail": "must be an ISO 3166-1 alpha-2 code"}
  ]
}
```

### 9.2 Problem type registry

| Type (suffix of base URI) | Status | Title | When it occurs | Client action |
|---|---|---|---|---|
| `validation-failed` | 422 | Request validation failed | Body or parameters violate the schema | Fix the fields in `errors[]` |
| `invalid-sort` | 400 | Unsupported sort field | `sort` names a non-sortable field | Use a listed sort field |
| `unauthenticated` | 401 | Authentication required | Missing or expired token | Obtain a new token |
| `insufficient-scope` | 403 | Missing scope | Token lacks the endpoint's scope | Request the scope |
| `not-found` | 404 | Resource not found | Unknown ID or other tenant's resource | Check the ID |
| `order-not-cancellable` | 409 | Order cannot be cancelled | Order already shipped | Create a return instead |
| `idempotency-key-reused` | 422 | Idempotency key reused | Same key with a different body | Generate a new key |
| `precondition-failed` | 412 | Resource changed | `If-Match` does not match current ETag | Re-read and retry |
| `rate-limited` | 429 | Too many requests | Quota exceeded | Wait for `Retry-After` |
| `internal-error` | 500 | Internal error | Unexpected server fault | Retry with backoff; quote `requestId` to support |
| [type] | [code] | [Title] | [Condition] | [Action] |

## 10. Resources and endpoints

<!-- Guidance: One subsection per resource. For each endpoint give method, path, scope, parameters, request, response, status codes and side effects. Use the same layout every time so readers can scan. -->

### 10.1 Endpoint summary

| ID | Method | Path | Summary | Scope |
|---|---|---|---|---|
| EP-01 | GET | `/orders` | List orders | `orders:read` |
| EP-02 | POST | `/orders` | Create an order | `orders:write` |
| EP-03 | GET | `/orders/{orderId}` | Retrieve an order | `orders:read` |
| EP-04 | PATCH | `/orders/{orderId}` | Update shipping address or notes | `orders:write` |
| EP-05 | POST | `/orders/{orderId}/cancel` | Cancel an unshipped order | `orders:write` |
| EP-06 | GET | `/orders/{orderId}/shipments` | List shipments for an order | `orders:read` |
| EP-07 | POST | `/orders/{orderId}/refunds` | Refund part or all of an order | `refunds:write` |
| [EP-nn] | [Method] | [Path] | [Summary] | [Scope] |

### 10.2 Orders: create an order (EP-02)

Creates an order in status `PENDING_PAYMENT`. Prices are taken from the
catalogue at creation time; client-supplied prices are ignored.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `Idempotency-Key` | header | UUID | Yes | Makes retries safe |
| `customerId` | body | string | Yes | Existing customer ID, prefix `cus_` |
| `lines` | body | array of line | Yes | 1 to 200 lines |
| `lines[].sku` | body | string | Yes | Catalogue SKU |
| `lines[].quantity` | body | integer | Yes | 1 to 999 |
| `shippingAddress` | body | Address | Yes | Delivery address |
| `notes` | body | string | No | Up to 500 characters |

```http
POST /orders/v1/orders HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
Content-Type: application/json
Idempotency-Key: 0b8f6a2e-3c1d-4e5f-8a9b-1c2d3e4f5a6b
```

```json
{
  "customerId": "cus_01HV5R2M7T",
  "lines": [{"sku": "TSHIRT-BLK-M", "quantity": 2}],
  "shippingAddress": {"line1": "12 Rue de Rivoli", "city": "Paris", "postalCode": "75001", "countryCode": "FR"}
}
```

```http
HTTP/1.1 201 Created
Location: /orders/v1/orders/ord_01HV6Z3K8Q
ETag: "3f2a9c"
Content-Type: application/json
```

```json
{
  "id": "ord_01HV6Z3K8Q",
  "status": "PENDING_PAYMENT",
  "customerId": "cus_01HV5R2M7T",
  "lines": [{"id": "lin_01HV6Z3KA1", "sku": "TSHIRT-BLK-M", "quantity": 2, "unitPrice": {"amount": "19.90", "currency": "EUR"}}],
  "totalAmount": {"amount": "39.80", "currency": "EUR"},
  "createdAt": "2026-03-14T09:30:00Z"
}
```

| Status | Meaning |
|---|---|
| 201 | Order created; `Location` points to it |
| 401 / 403 | Authentication or scope failure |
| 409 | Request with this `Idempotency-Key` still in progress |
| 422 | Validation failed, unknown SKU, or idempotency key reused |
| 429 | Rate limited |

Side effects: emits webhook `order.created` (section 11).

### 10.3 Orders: list orders (EP-01)

Returns orders for the caller's tenant, newest first by default.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `status` | query | enum list | No | Filter by one or more statuses |
| `customerId` | query | string | No | Filter by customer |
| `createdAt[gte]`, `createdAt[lt]` | query | date-time | No | Creation range |
| `sort` | query | string | No | `createdAt`, `-createdAt`, `totalAmount`, `-totalAmount` |
| `limit`, `cursor` | query | | No | Section 6.1 |

```http
GET /orders/v1/orders?status=SHIPPED&limit=2 HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
```

Response: `200` with the paginated envelope in section 6.1.

### 10.4 Orders: cancel an order (EP-05)

Cancels an order that has not shipped. Returns the updated order with
status `CANCELLED`. An order with any shipment returns `409` with problem
type `order-not-cancellable`. Calling cancel on an already cancelled order
returns `200` with the unchanged order, so the action is idempotent.

| Status | Meaning |
|---|---|
| 200 | Cancelled, or already cancelled |
| 404 | Order not found for this tenant |
| 409 | Order already shipped |

### 10.5 [Resource]: [operation] ([EP-nn])

[Repeat the layout above for each remaining endpoint: purpose, parameter
table, request example, response example, status table, side effects.]

## 11. Webhooks

<!-- Guidance: Webhooks are an API in the other direction. Document payloads, signing, retries and ordering as carefully as requests. -->

### 11.1 Events

| Event type | Trigger | Payload |
|---|---|---|
| `order.created` | Order created | Full order |
| `order.status_changed` | Status transitions | Order ID, old and new status, timestamp |
| `shipment.created` | Carrier label created | Shipment |
| `refund.succeeded` | Refund settled | Refund |
| [event] | [Trigger] | [Payload] |

### 11.2 Delivery

- Payloads are `POST`ed as JSON to the subscriber's HTTPS URL with headers
  `Webhook-Id`, `Webhook-Timestamp` and `Webhook-Signature`.
- The signature is HMAC-SHA256 over `id.timestamp.body` with the
  subscription secret. Subscribers must verify it and reject timestamps
  older than [5 minutes] to block replays.
- A `2xx` response within [10 s] acknowledges delivery. Other responses are
  retried with exponential backoff for [72 hours], then the subscription is
  marked failing and the owner notified.
- Delivery is at least once and not ordered. Subscribers deduplicate on
  `Webhook-Id` and use the `occurredAt` field to discard stale updates.

```json
{
  "id": "evt_01HV7A0B2C",
  "type": "order.status_changed",
  "occurredAt": "2026-03-15T14:02:11Z",
  "data": {"orderId": "ord_01HV6Z3K8Q", "from": "PAID", "to": "SHIPPED"}
}
```

## 12. OpenAPI 3.2 document

<!-- Guidance: Keep the full OpenAPI file in the repository and generate reference docs, mocks and contract tests from it. The skeleton shows the required top-level structure and shared components. -->

The authoritative machine-readable contract is `[api/openapi.yaml]`,
written against OpenAPI 3.2.0.

### 12.1 Skeleton

The skeleton below shows the top-level structure, the shared components
every endpoint reuses and one example of each 3.2 feature the API relies
on.

```yaml
openapi: 3.2.0
$self: https://api.example.com/orders/v1/openapi.yaml
info:
  title: Orders API
  version: 1.4.0
  contact:
    name: Orders team
    email: orders-api@example.com
servers:
  - url: https://api.example.com/orders/v1
    description: Production
  - url: https://sandbox.api.example.com/orders/v1
    description: Sandbox
security:
  - oauth2: [orders:read]
tags:
  - name: Orders
    summary: Orders
    description: Create, read, update and cancel customer orders.
    kind: nav
  - name: Order exports
    summary: Exports
    parent: Orders
    kind: nav
  - name: Beta
    summary: Beta
    description: Endpoints that may change without a major version.
    kind: badge
paths:
  /orders:
    get:
      operationId: listOrders
      tags: [Orders]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of orders
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderPage'
        default:
          $ref: '#/components/responses/Problem'
    post:
      operationId: createOrder
      tags: [Orders]
      security:
        - oauth2: [orders:write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderCreate'
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        default:
          $ref: '#/components/responses/Problem'
    query:
      operationId: searchOrders
      summary: Search orders with a filter document in the body
      tags: [Orders, Beta]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderSearch'
      responses:
        '200':
          description: A page of matching orders
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderPage'
        default:
          $ref: '#/components/responses/Problem'
  /orders/export:
    get:
      operationId: exportOrders
      tags: [Order exports]
      responses:
        '200':
          description: Orders streamed as JSON Lines, one order per line
          content:
            application/jsonl:
              itemSchema:
                $ref: '#/components/schemas/Order'
        default:
          $ref: '#/components/responses/Problem'
  /cache/orders/{orderId}:
    parameters:
      - name: orderId
        in: path
        required: true
        schema: {type: string}
    additionalOperations:
      PURGE:
        operationId: purgeOrderCache
        tags: [Orders]
        responses:
          '204':
            description: Cached representation removed
components:
  securitySchemes:
    oauth2:
      type: oauth2
      oauth2MetadataUrl: https://auth.example.com/.well-known/oauth-authorization-server
      flows:
        clientCredentials:
          tokenUrl: https://auth.example.com/oauth2/token
          scopes:
            orders:read: Read orders
            orders:write: Create and update orders
        deviceAuthorization:
          deviceAuthorizationUrl: https://auth.example.com/oauth2/device_authorization
          tokenUrl: https://auth.example.com/oauth2/token
          scopes:
            orders:read: Read orders
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key
      deprecated: true
  parameters:
    Limit:
      name: limit
      in: query
      schema: {type: integer, minimum: 1, maximum: 100, default: 25}
    Cursor:
      name: cursor
      in: query
      schema: {type: string}
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: {type: string, format: uuid}
  responses:
    Problem:
      description: Error
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
  schemas:
    Problem:
      type: object
      properties:
        type: {type: string, format: uri}
        title: {type: string}
        status: {type: integer}
        detail: {type: string}
        instance: {type: string}
    Order:
      type: object
      required: [id, status, customerId, lines, totalAmount, createdAt]
      properties:
        id: {type: string, pattern: '^ord_'}
        status: {type: string, enum: [PENDING_PAYMENT, PAID, SHIPPED, DELIVERED, CANCELLED]}
        customerId: {type: string}
        lines: {type: array, items: {$ref: '#/components/schemas/OrderLine'}}
        totalAmount: {$ref: '#/components/schemas/Money'}
        createdAt: {type: string, format: date-time}
    OrderSearch:
      type: object
      properties:
        status: {type: array, items: {type: string}}
        customerIds: {type: array, maxItems: 500, items: {type: string}}
        createdAfter: {type: string, format: date-time}
    OrderCreate:
      type: object
      required: [customerId, lines, shippingAddress]
      properties:
        customerId: {type: string}
        lines: {type: array, minItems: 1, maxItems: 200, items: {type: object}}
        shippingAddress: {type: object}
    OrderLine:
      type: object
      properties:
        id: {type: string}
        sku: {type: string}
        quantity: {type: integer, minimum: 1}
        unitPrice: {$ref: '#/components/schemas/Money'}
    OrderPage:
      type: object
      properties:
        data: {type: array, items: {$ref: '#/components/schemas/Order'}}
        nextCursor: {type: [string, 'null']}
        hasMore: {type: boolean}
    Money:
      type: object
      required: [amount, currency]
      properties:
        amount: {type: string, pattern: '^-?\d+(\.\d{1,4})?$'}
        currency: {type: string, minLength: 3, maxLength: 3}
```

### 12.2 OpenAPI 3.2 features used

Version 3.2.0 adds the features below. Record which ones the API uses so
that consumers can check their tooling supports them.

| Feature | Field | Used here | Notes |
|---|---|---|---|
| QUERY method | `query` on the Path Item | *Example:* Yes, `searchOrders` (beta) | Keep a `POST` fallback until consumers confirm support |
| Other HTTP methods | `additionalOperations` | *Example:* Yes, `PURGE` on the cache path | Keys are the method name as sent on the wire; standard methods stay in their own fields |
| Streaming and sequential media types | `itemSchema`, `itemEncoding` | *Example:* Yes, JSON Lines export | Also covers `text/event-stream`, `application/json-seq` and `multipart/mixed` |
| Whole query string as one parameter | `in: querystring` | [No] | For APIs that accept a structured query string as one object |
| Device authorization flow | `flows.deviceAuthorization` | *Example:* Yes, handheld scanners | Requires `deviceAuthorizationUrl` and `tokenUrl` |
| Authorization server metadata | `oauth2MetadataUrl` | *Example:* Yes | Points to RFC 8414 metadata; TLS required |
| Deprecated security schemes | `deprecated: true` on a Security Scheme | *Example:* Yes, `apiKey` | Pair with the sunset policy in section 5 |
| Hierarchical, typed tags | `summary`, `parent`, `kind` on Tag | *Example:* Yes | `kind` values such as `nav` and `badge` drive documentation navigation |
| Document identity | `$self` | *Example:* Yes | Base URI for resolving references across files |
| [Feature] | [Field] | [Yes / No] | [Notes] |

### 12.3 Validation rules

Validation rules for the OpenAPI file:

| Check | Tool or method | Gate |
|---|---|---|
| File is valid OpenAPI 3.2 | [Linter, for example Spectral or Redocly CLI] | CI blocks merge |
| Style rules (casing, problem responses, operationId present) | Linter ruleset `[path]` | CI blocks merge |
| Breaking-change detection against the published version | [OpenAPI diff tool] | Requires API owner approval |
| Examples validate against schemas | Contract tests | CI blocks merge |

## 13. Changelog

<!-- Guidance: Public, consumer-facing record of contract changes. Newest first. Link each entry to the migration notes when action is needed. -->

| Date | API version | Type | Change | Consumer action |
|---|---|---|---|---|
| YYYY-MM-DD | 1.4.0 | Added | *Example:* `refund.succeeded` webhook | None |
| YYYY-MM-DD | 1.3.0 | Added | *Example:* `sort=totalAmount` on `GET /orders` | None |
| YYYY-MM-DD | 1.2.0 | Deprecated | *Example:* `GET /orders?page=` offset pagination; sunset YYYY-MM-DD | Move to `cursor` |
| [Date] | [x.y.z] | [Added / Changed / Deprecated / Removed / Fixed / Security] | [Change] | [Action] |

## 14. Service levels and support

<!-- Guidance: State what consumers can rely on, how it is measured and what happens when it is missed. Align with any contractual SLA. -->

### 14.1 Service level objectives

| Indicator | Objective | Measurement window | Measured as |
|---|---|---|---|
| Availability | *Example:* 99.95% | Calendar month | Share of non-5xx responses at the gateway, excluding planned maintenance |
| Latency, reads | p95 at or below 200 ms | Rolling 28 days | Gateway timing, `GET` endpoints |
| Latency, writes | p95 at or below 500 ms | Rolling 28 days | Gateway timing, `POST`, `PATCH` |
| Webhook delivery | 99% delivered within 60 s | Calendar month | Time from event to first attempt |
| [Indicator] | [Objective] | [Window] | [Method] |

Availability is computed as:

$$
A = \frac{\text{requests} - \text{5xx responses}}{\text{requests}} \times 100\%
$$

At 99.95% the monthly error budget is $0.0005 \times 30 \times 24 \times 60 \approx 21.6$ minutes of full outage.

### 14.2 Maintenance and support

| Item | Commitment |
|---|---|
| Planned maintenance | Announced 7 days ahead; [Sunday 02:00 to 04:00 UTC] |
| Status page | `[https://status.example.com]` |
| Support channel | [Developer portal ticket / email] |
| Response time, P1 (API unavailable) | [30 minutes, 24x7] |
| Response time, P3 (question) | [2 business days] |

## 15. Glossary and references

<!-- Guidance: Define domain terms and cite every standard the contract relies on. -->

### 15.1 Glossary

| Term | Meaning |
|---|---|
| Consumer | An application that calls this API |
| Idempotency key | Client-generated value that makes a retried request safe |
| Problem details | Standard JSON error body defined by RFC 9457 |
| Cursor | Opaque pointer to a position in a result set |
| ETag | Version tag of a resource representation used for concurrency control |
| [Term] | [Meaning] |

### 15.2 References

| Reference | Used for |
|---|---|
| RFC 9110, HTTP Semantics | Methods, status codes, conditional requests |
| RFC 9457, Problem Details for HTTP APIs | Error format in section 9 |
| OpenAPI Specification 3.2.0 (spec.openapis.org/oas/v3.2.0) | Machine-readable contract in section 12 |
| RFC 8628, OAuth 2.0 Device Authorization Grant | Device flow in section 3.1 |
| RFC 8414, OAuth 2.0 Authorization Server Metadata | `oauth2MetadataUrl` |
| IETF HTTP QUERY method draft (safe method with body) | `QUERY` in section 4.2 |
| RFC 3339, Date and Time on the Internet | Timestamp format |
| RFC 7396, JSON Merge Patch | `PATCH` semantics |
| RFC 8594, The Sunset HTTP Header Field | Removal date signalling |
| RFC 6749 and RFC 7636, OAuth 2.0 and PKCE | Authentication flows |
| [Internal API style guide] | [Use] |

## Standard and licence note

Based on the IETF specifications RFC 9110 (HTTP Semantics) and RFC 9457
(Problem Details for HTTP APIs) and on the OpenAPI Specification 3.2.0
(September 2025, Apache License 2.0), together with common industry practice for API
design. This template is original text written for this collection and
reproduces no text from those specifications; read them for their
normative requirements. Example values are illustrative. Template
licence: MIT.
