---
source: https://threa.io/developers/versioning
notes: Markdown mirror generated from the page above. YOUR_WORKSPACE_ID is the ws_… id in the app URL after /w/; YOUR_API_KEY is a key from Settings > API keys.
---

# API versions.

Threa versions the API by date. Each key is pinned to a version when you create it, so your integration keeps seeing the same shapes until you decide to move. When a version introduces a breaking change, your key stays on the older one and you opt in when you are ready.

## How it works

A version is a date. The current version is `2026-07-12`. When you create an API key it is pinned to whatever version is current at that moment, and that pin decides which shapes the key sees on every request. You do not have to send anything for this to work.

When we later ship a version with a breaking change, existing keys are unaffected. You test the new version by sending the `Threa-Version` header on individual requests, and when your integration is ready you either send that header everywhere or create a new key pinned to the newer version.

## The header

Send `Threa-Version` with a version date to override your key's pin for a single request. A valid header takes precedence over the pin.

*override the pin for one request:*

```bash
curl https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/streams \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Threa-Version: 2026-07-12"
```

The value must be one of the known versions exactly. An unknown or malformed value returns `400` with the code `INVALID_API_VERSION` and the list of known versions in the error, so a typo fails loudly instead of being treated as the latest. Every response that resolved a version echoes it in a `Threa-Version` response header, so you can always confirm which shapes you received. The `400` for an unknown version has no resolved version, so it carries no echo header.

*400 on an unknown version:*

```json
{
  "error": "Unknown API version \"2020-01-01\". Known versions: 2026-07-12",
  "code": "INVALID_API_VERSION"
}
```

## Reading and changing the pin

`GET /api/v1/workspaces/{workspaceId}/me` reports the key's version state alongside its identity, so an agent can discover its pin with a call it already makes:

*apiVersion block in the /me response:*

```json
{
  "data": {
    "kind": "user",
    "workspaceId": "ws_...",
    "userId": "usr_...",
    "apiVersion": {
      "pinned": "2026-07-12",
      "resolved": "2026-07-12",
      "current": "2026-07-12",
      "supported": ["2026-07-12"]
    }
  }
}
```

`pinned` is the key's pin, or `null` when the key is unpinned. `resolved` is the version this request used.

*detect that a newer version exists:*

```ts
interface ApiVersionInfo {
  pinned: string | null
  resolved: string
  current: string
  supported: string[]
}

const res = await fetch("https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/me", {
  headers: { Authorization: "Bearer YOUR_API_KEY" },
})
const { data } = (await res.json()) as { data: { apiVersion: ApiVersionInfo } }

if (data.apiVersion.resolved !== data.apiVersion.current) {
  // A newer version exists. Test against it by sending
  // "Threa-Version": data.apiVersion.current on individual requests.
}
```

Pins belong to the key's owner and are managed in the app, not through the public API: `PATCH` the key in the workspace's key management with `{"apiVersion": "<date>"}` to re-pin it, or `{"apiVersion": null}` to unpin it. An unpinned key always uses the current version, including future breaking changes, so unpin only if you update your integration as versions ship. To adopt newer shapes without touching the pin, send the header or create a new key.

## Breaking vs additive changes

Additive changes do not get a new version and can arrive on your current one at any time. Plan for them. These are new endpoints, new optional request fields, new response fields, and new values in response enums that are documented as open sets. Write your client so an unexpected field or enum value is ignored rather than rejected.

Breaking changes get a new dated version and never reach a key pinned to an earlier one. A change is breaking when it removes or renames a field, changes a field's type or meaning, tightens request validation, changes a default, or changes an error code or status. This is the same split Stripe uses.

| Change | New version? |
| --- | --- |
| New endpoint | No |
| New optional request field | No |
| New response field | No |
| New value in an open response enum | No |
| Removing or renaming a field | Yes |
| Changing a field's type or meaning | Yes |
| Tightening request validation | Yes |
| Changing a default value | Yes |
| Changing an error code or status | Yes |

## Support window

A version stays supported for at least twelve months after it is succeeded, so a key pinned to an older version keeps working for that long once a newer version ships.

## The path prefix

The URL prefix does not change with versions. Every endpoint stays under `/api/v1`, and the `Threa-Version` header carries the version. The `v1` segment is a stable prefix, so paths you hardcode today keep working. Version negotiation happens entirely through the header and the key's pin.

## Changelog

Each dated version is listed here with what changed. The current version is `2026-07-12`.

Each version has its own OpenAPI spec at `/openapi/<version>.json`, and [/openapi.json](https://threa.io/openapi.json) is the current one. The [reference](https://threa.io/developers/reference.md) renders each version at `/developers/reference/<version>`, with a version picker at the top.

### 2026-07-12

Initial versioned API.
