# Conventions

These hold across the whole surface. Reading this once saves rediscovering it per resource.

## Versioning

The version is in the path: `/api/public/v1/`.

Additive changes — new fields, new endpoints, new optional parameters, new enum values — ship inside `v1`. Your client must tolerate them, which takes two habits: **ignore response fields you don't recognise**, and **don't fail closed on an unfamiliar enum value**.

Removals, renames, retypes, and anything that tightens validation bump the version, with an overlap window during which both are live.

## List responses

Lists are cursor-paginated and always come back in the same envelope:

```json
{
  "object": "list",
  "data": [ { "object": "shift", "id": "0194d1c0-…", "…": "…" } ],
  "has_more": true,
  "next": "https://app.shiftavo.com/api/public/v1/shifts/?cursor=…"
}
```

- `next` is an **opaque URL**. Follow it as-is; do not construct or parse cursors.
- Stop when `has_more` is `false`.
- `page_size` controls the page length.

## The `object` discriminator

Every resource carries an `"object"` field naming its type — `"shift"`, `"location"`, `"employee"`. Branch on it rather than on the shape of the payload, especially inside webhook handlers and mixed collections.

## Errors

One envelope, everywhere:

```json
{
  "error": {
    "type": "conflict_error",
    "code": "not_cancellable",
    "message": "…",
    "details": [],
    "param": "start_date"
  }
}
```

- `details` is **always present** — a list of field-level failures, empty when the error has no field breakdown.
- `param` appears only when exactly one field is at fault.

There are **seven** types. Each maps to one status except `invalid_request_error`, the catch-all for the non-tabulated 4xx, where the `code` carries the distinction:

| `type` | Status | Notes |
|---|---|---|
| `invalid_request_error` | **400** · 405 · 406 · 415 | `code` = `invalid_request` / `method_not_allowed` / `not_acceptable` / `unsupported_media_type` |
| `idempotency_error` | 400 | `Idempotency-Key` reused with a different body (`idempotency_key_reused`) |
| `authentication_error` | 401 | missing / invalid / expired token |
| `permission_error` | 403 | `insufficient_scope`, and friends |
| `not_found` | 404 | also returned instead of 403 for records outside your company — no existence leak |
| `conflict_error` | 409 | state conflicts: `duplicate`, `in_use`, `not_deletable`, an in-flight idempotent replay |
| `rate_limit_error` | 429 | carries `Retry-After` |

Branch on `type` for handling class, on `code` for the specific case. Both are stable; `message` is not — treat it as human-readable only.

## Idempotency

Send an **`Idempotency-Key`** header on creates. A retried request then replays the first result instead of creating a second record.

Reusing a key with a *different* body is a `400 idempotency_key_reused` — the key identifies one request, not one endpoint.

## Request tracing

Every response, success and error alike, carries an **`X-Request-Id`** header. Log it. Send your own `X-Request-Id` and it is echoed back, so a trace id can travel end to end. Quote it in any support request.

## Lifecycle transitions

A transition fires as a `POST` to an action sub-path: `POST …/<id>/publish/`, `…/cancel/`, `…/revoke/`.

A transition that cannot apply — cancelling a draft, publishing something already published — is a **`409`** with a specific code (`not_cancellable`, `not_publishable`, `not_unpublishable`). It is never a `200` that silently did nothing.

## Deleting

`DELETE` returns a stub, not a `204`:

```json
{ "id": "0194d1c0-…", "object": "shift", "deleted": true }
```

**A `DELETE` never takes a body.** Everything a delete needs is in the path or the query string, so a generated client can express all of them. (HTTP leaves a `DELETE` body's semantics undefined and OpenAPI 3.0 tells consumers to ignore one, so a body field here would silently disappear from your client and come back as a `400` the schema couldn't explain.)

### Delete is not the soft verb

Where a resource offers both, they are not synonyms, and the wrong choice is not recoverable:

| Resource | `DELETE …/<id>/` | The soft verb |
|---|---|---|
| `shifts` | removes the shift; assignees of a published shift are notified and any live marketplace offer is withdrawn first | `POST …/cancel/` — the shift stays, marked cancelled |
| `employees` | terminal removal: access revoked, the person leaves every collection and a later `GET` is a `404`. The record is retained internally so timesheets and leave keep their references | `POST …/deactivate/` (reversible) · `POST …/offboard/` (dated termination + settlement) |
| `leave_requests` | only while **pending** or **declined** — a confirmed or revoked request carries a ledger booking, so it is a `409 not_deletable` | `POST …/revoke/` — undoes the effect, keeps the record |

### Recurring records

Deleting one occurrence of a recurring `shifts` or `availability` record needs an explicit scope:

```
DELETE …/<id>/?scope=this|following|all        # shifts
DELETE …/<id>/?scope=single|following|all      # availability
```

Omit it on a series and you get a `409 delete_scope_required` rather than a guess about what you meant.

## Rate limits

A `429` carries a `Retry-After` header. Honour it, and back off exponentially on repeats.
