# API contract details

Versioning rules and worked examples for the pagination, idempotency, error-shape and webhook rules summarised in SKILL.md.

## Contents

- Evolving a contract
- Pagination
- Idempotency for retried writes
- One error shape everywhere
- Webhooks done properly

## Evolving a contract

Treat every published field as a promise. Additive change is almost always safe;
removal, renaming and changing meaning are not.

Safe without a new version:

- adding an optional request field or a new response field;
- adding a new endpoint, RPC or event type;
- adding an enum value, **only if** clients were told to tolerate unknown values.

Breaking, needs a migration plan:

- removing or renaming a field, or making an optional field required;
- changing a type, unit or format (`price` from cents to decimal string);
- changing default behaviour or the meaning of a status.

Versioning mechanics, from lightest to heaviest:

1. **No explicit version, additive only.** Works longer than people expect.
2. **Dated versions** pinned per client (`Api-Version: 2026-06-01`), with the
   server translating older shapes. Good for public APIs with many integrators.
3. **Major version in the path** (`/v2/orders`). Simple to route and reason
   about, costly to maintain two copies.

Whatever the scheme, publish a deprecation window, emit a `Deprecation` and
`Sunset` header (RFC 9745 / RFC 8594) on retiring endpoints, and measure who is
still calling before you switch anything off.

For events, the same rules apply to the payload schema. Keep a schema registry
or a versioned schema file and run a compatibility check in CI.

## Pagination

Offset pagination (`?page=7`) is easy and wrong for data that changes: rows
shift between requests and deep pages get slow. Prefer an opaque cursor.

```http
GET /products?limit=50&after=eyJpZCI6IjAxSjhRIn0

200 OK
{
  "data": [ ... ],
  "page": { "next": "eyJpZCI6IjAxSjk0In0", "hasMore": true }
}
```

- The cursor encodes the sort key plus a unique tiebreaker (`created_at`, `id`)
  and is base64 so clients treat it as opaque.
- Cap `limit` on the server; ignore or clamp values above it.
- Do not promise a total count unless it is cheap; counting a large table on
  every page request is a common production hotspot.
- Streaming or long-lived exports are a job, not a very large page.

## Idempotency for retried writes

Networks drop responses, not just requests. A client that times out on
`POST /orders` cannot know if the order exists. Give it a way to retry safely.

```http
POST /orders
Idempotency-Key: 5f1c2d0e-8b7a-4c77-9a61-0d3f6c1b2a90
Content-Type: application/json

{ "productId": "p_812", "quantity": 2 }
```

Server behaviour:

1. Look up the key scoped to the caller. If a completed result exists, return it
   unchanged, including the original status code.
2. If a request with that key is still running, return `409 Conflict` so the client backs off.
3. If the same key arrives with a different body, reject it with `422`.
4. Store the key and result in the same transaction as the side effect, and
   expire keys after a stated window (24 hours is typical).

`PUT` and `DELETE` are idempotent by definition, but only if your handler is.
Webhook and event consumers need the same property: deduplicate on the event id,
because at-least-once delivery means duplicates will arrive.

## One error shape everywhere

Use RFC 9457 problem details for HTTP APIs. Clients then write one error
handler instead of one per endpoint.

```json
{
  "type": "https://api.example.com/problems/insufficient-stock",
  "title": "Not enough stock",
  "status": 409,
  "detail": "Only 1 unit of product p_812 is available.",
  "instance": "/orders/req_01J9Z",
  "errors": [{ "pointer": "/quantity", "detail": "must be at most 1" }]
}
```

- `type` is a stable identifier clients can branch on; `title` is for humans.
- Put field-level validation failures in an extension member (`errors` above)
  using JSON Pointers into the request body.
- Never leak stack traces, SQL or internal hostnames in `detail`.
- Include a request or trace id so support can find the log line.
- Serve it as `application/problem+json`.

For gRPC, map to canonical status codes and attach `google.rpc` error details;
for events, a failed consumer moves the message to a dead-letter destination
with the reason attached.

## Webhooks done properly

- Sign every delivery (HMAC-SHA256 over timestamp plus body) and document how to
  verify it; reject deliveries older than a few minutes to stop replay.
- Retry with exponential backoff and jitter for a bounded period, then park the
  delivery and surface it in a dashboard.
- Send a thin payload (event type, id, resource URL) or a full snapshot, and say
  which; thin payloads avoid stale data at the cost of a follow-up call.
- Let integrators replay past deliveries; they will lose some.
