Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/api-reference/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ Called by Substratal's hosted sign-in page (or the future dashboard's "Launch" b

### `POST /auth/oauth/token`

Called by the Application. A public-client endpoint: there's no client secret, the PKCE verifier is the proof.
Called by the Application. A public-client endpoint: there's no client secret, the PKCE verifier is the proof. It's the one Auth endpoint that doesn't need `Substratal-Version`, because standard OAuth libraries call it without custom headers (see [Conventions → Versioning](../conventions/#versioning)). Sending the header is fine.

```json
// Request — exchange an authorization code
Expand Down
3 changes: 2 additions & 1 deletion docs/api-reference/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,8 @@ Versions are whole numbers, and `1` is the only one today. The URL never carries
- **Echoed.** Every response to a versioned request carries `Substratal-Version` set to the version that served it.
- **Exempt:** `GET /health`, so uptime monitors can call it as is; `POST /auth/oauth/token`, which standard OAuth libraries call without custom headers; and the `/.well-known` endpoints. They behave the same in every version. [MCP](../../mcp-server/) clients don't send it either: the MCP server calls the API with the version its tools describe.
- **Browsers:** `Substratal-Version` is allowed in CORS requests and exposed on CORS responses.
- **Changes.** Additive changes (new optional fields, new endpoints, new enum values) ship within a version. A breaking change ships as the next number, and the previous version keeps working for a deprecation window, with `Deprecation` and `Sunset` headers on every response it serves. See [Non-Functional Requirements → Versioning](../../non-functional-requirements/#versioning).
- **Changes.** Additive changes (new optional fields, new endpoints, new enum values) ship within a version. A breaking change ships as the next number, and the previous version keeps working for at least 6 months, with `Deprecation` and `Sunset` headers on every response to a request that names it. After its `Sunset` date it's retired, and requests naming it get `400 unsupported_api_version`. See [Non-Functional Requirements → Versioning](../../non-functional-requirements/#versioning) for what counts as breaking.
- **Not the webhook version.** A webhook subscription's `api_version` (a date, such as `2026-10-05`) fixes the shape of the payloads it receives, and is versioned separately. See [Webhooks → Payload versions](../webhooks/#payload-versions).
- **Generated clients:** `openapi.yaml` doesn't declare the header on each operation, so configure your client to send it as a default header.

## Timestamps
Expand Down
2 changes: 1 addition & 1 deletion docs/api-reference/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ During a [secret rotation](#post-webhooksidrotate-secret), deploy with both secr

## Payload versions

Each subscription's `api_version` fixes the shape of every payload it receives. Versions are dates, and a new one is published only for a breaking change to an existing event's payload. New event types and new optional fields in `data` are additive and ship to every version.
Each subscription's `api_version` fixes the shape of every payload it receives. It's separate from the `Substratal-Version` request header ([Conventions → Versioning](../conventions/#versioning)): a request's API version doesn't change which payload version a subscription gets, and a new API version doesn't imply a new payload version. Payload versions are dates, and a new one is published only for a breaking change to an existing event's payload. New event types and new optional fields in `data` are additive and ship to every version.

| `api_version` | Status | Changes from the previous version |
|---|---|---|
Expand Down
9 changes: 9 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ What's changed in this specification over time. Every decision is written into t

---

## 2026-10-21 — Versioning rules spelled out

Clarifications to the header-based versioning below; nothing about the API's behavior changed.

- **What counts as breaking** is now listed in [Non-Functional Requirements → Versioning](../non-functional-requirements/#versioning): removing or renaming a field, endpoint, or parameter; changing a type or format; a new or newly required request field; a different status or error code for an existing condition; or a changed default or behavior.
- **Retirement.** `Deprecation` (RFC 9745) and `Sunset` (RFC 8594) appear on responses to requests that name a deprecated version. After its `Sunset` date, requests naming it get `400 unsupported_api_version`.
- **Two separate versions.** The `Substratal-Version` request header and a webhook subscription's payload `api_version` are independent, now stated on [Conventions](../api-reference/conventions/#versioning), [Webhooks](../api-reference/webhooks/#payload-versions), and in the [Glossary](../glossary/#api-version).
- [OpenAPI Specification](../openapi/#versioning-and-change-policy) no longer describes versions as URL prefixes, and [Auth → `POST /auth/oauth/token`](../api-reference/auth/#post-authoauthtoken) notes it needs no version header.

## 2026-10-21 — The API version moves from the URL to a header

**Breaking.** The `/v1` path prefix is gone. Every endpoint is now at the host root, and every request names its API version in a header instead:
Expand Down
6 changes: 6 additions & 0 deletions docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ Which [Pricing](../pricing/) tier unlocks an [Application](#application): `free`
#### API Key
A long-lived service credential (`satk_live_…`/`satk_test_…`), scoped to the platform or to one Application. An app-scoped key can only ever touch its own Application's rows. See [API Keys](../api-reference/api-keys/).

#### API version
The version of the REST API a request is written against, sent as a whole number in the required `Substratal-Version` header. `1` is the only one so far. Unrelated to a webhook [payload version](#payload-version). See [Conventions → Versioning](../api-reference/conventions/#versioning).

#### App token
The short-lived (5-minute) JWT an Application verifies locally, with `aud` set to that Application. Carries `entitlement_status` and `effective_permissions`. See [Auth → The app token](../api-reference/auth/#the-app-token).

Expand Down Expand Up @@ -66,6 +69,9 @@ A domain/grouping object for Users — a company, or a group within a company
#### OrganizationMembership
The join record between a User and an Organization, carrying that User's standing *within* that one Organization (`org_admin` or `member`) and whether it's `pending` (invited, not yet accepted) or `active` — a User can hold this for any number of Organizations at once. See [Domain Model → Users & Organizations](../domain-model/users-and-organizations/#organizationmembership).

#### Payload version
The shape of a webhook's payloads, a date pinned on each subscription as `api_version` (currently `2026-10-05`) and moved forward only by its owner. Unrelated to the [API version](#api-version). See [Webhooks → Payload versions](../api-reference/webhooks/#payload-versions).

#### Permission
An atomic, checkable capability, written as a dotted key (`billing.manage`, `app.timetrack.export`). Never assigned directly to a user — always granted through a [Role](#role).

Expand Down
3 changes: 2 additions & 1 deletion docs/non-functional-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ Targets for the API itself, measured monthly at the API Gateway. They're publish
## Versioning

- The API version is a whole number sent in the `Substratal-Version` request header, never in the URL. `1` is the only version today, and the header is required (`400 api_version_required` without it), so no client is ever on a version it didn't ask for. See [Conventions → Versioning](../api-reference/conventions/#versioning). Additive, backward-compatible changes (new optional fields, new endpoints) ship without a version bump.
- A breaking change gets the next version number. The old version keeps working for a minimum 6-month deprecation window, announced in the [Changelog](../changelog/) the day the replacement ships, with a `Deprecation` and `Sunset` response header (RFC 8594) added to every response the old version serves from that point on.
- A breaking change gets the next version number. The old version keeps working for a minimum 6-month deprecation window, announced in the [Changelog](../changelog/) the day the replacement ships. From then on, every response to a request that names the old version carries a `Deprecation` header (RFC 9745) and a `Sunset` header (RFC 8594) giving the retirement date. Once that date passes, the old version is removed from the supported list, and requests naming it get `400 unsupported_api_version`.
- **What counts as breaking:** removing or renaming a field, endpoint, or query parameter; changing a field's type or format; making an optional request field required, or adding a new required one; changing the status code or error code for an existing condition; and changing a default or a behavior that existing callers depend on. Everything else, such as new endpoints, new optional fields, new enum values, and new error codes for new conditions, is additive.
- Webhook payloads are versioned independently of the API version, by a date string (`api_version`, currently `2026-10-05`) pinned on each subscription at creation. A breaking payload change ships as a new date. Existing subscriptions keep their pinned shape until their owner moves them forward with `PATCH`, and old payload versions are supported for at least 12 months. Additive payload fields don't get a new date.
- The [MCP Server](../mcp-server/)'s protocol version (negotiated per MCP's own date-based scheme during `initialize`) is likewise independent of the API version — a protocol-version bump there doesn't imply `Substratal-Version: 2` here, and vice versa.
- **Enums can grow new values without a version bump.** `EntitlementStatus`, `UserStatus`, and similar closed-looking lists in [openapi.yaml](../openapi.yaml) are allowed to gain new members as additive, non-breaking changes. Clients — and any implementation — must treat an unrecognized enum value as "handle generically / no special case," never as an error to reject the response over. This is a contract, not just a suggestion: don't write a `switch` with no default case against any enum in this spec.
Expand Down
4 changes: 2 additions & 2 deletions docs/openapi/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,6 @@ To explore by hand, import `openapi.yaml` into Postman, Insomnia, Bruno, or any

The API version is the `Substratal-Version` request header (currently `1`), not part of the base URL. The document doesn't declare it on each operation, so configure a generated client to send it as a default header; the Interactive Reference sends it for you. `info.version` in the document (currently a `1.0.0-draft` pre-release) is the version of the document itself.

- **Additive changes** (new optional fields, new operations, new enum values) ship without a new prefix. Write clients to ignore fields they don't know, and to handle enum values they don't recognize.
- **Breaking changes** get a new prefix and a deprecation window for the old one. See [Non-Functional Requirements → Versioning](../non-functional-requirements/#versioning).
- **Additive changes** (new optional fields, new operations, new enum values) ship within the current version. Write clients to ignore fields they don't know, and to handle enum values they don't recognize.
- **Breaking changes** ship as the next version number, and the previous version keeps working for at least 6 months. See [Non-Functional Requirements → Versioning](../non-functional-requirements/#versioning) for what counts as breaking.
- Every change is recorded in the [Changelog](../changelog/).
Loading
Loading