API conventions
Version headers, response envelopes, errors, and pagination.
These conventions apply to every Optima REST route. Route-specific request fields, responses, and status codes are listed in the API reference.
Versioning
Send X-API-Version: 1 on every REST request, including authentication, byte transfers, and device requests. The URL has no version prefix.
| Request | Result |
|---|---|
X-API-Version: 1 | Optima uses version 1 and echoes X-API-Version on the response. |
| Header omitted | Optima selects the latest supported version. |
Unsupported value, such as 2 | HTTP 400 with code UNSUPPORTED_API_VERSION. Optima does not fall back to another version. |
An installed client should always pin the version it implements. The omitted-header default can advance to a breaking contract when a newer version becomes current.
Within a version, responses can gain fields. Ignore fields your client does not recognize instead of rejecting the response. Existing fields keep their meaning and type. Requests are strict: an unrecognized field in a request body returns 422 VALIDATION_ERROR. Versioned documents inside a response, such as turn content, change only through their own schema_version.
Some response fields are open value sets that can gain values within a version, such as a source kind, a source activity status, an audio status, a device binding_kind, and an event type. The reference lists their known values. Handle an unrecognized value as a generic one instead of rejecting the response. Request fields and filters accept only the documented values.
OAuth and MCP use their own protocol contracts and do not use this REST header.
Optima app versions
X-API-Version selects the API contract. Optima's own installed apps also report which app build is calling, so Optima can retire builds it no longer supports:
| Header | Value |
|---|---|
X-Optima-Client | The app, such as macos or mobile. |
X-Optima-Client-Version | <major>.<minor>.<patch>+<build>, such as 0.4.0+10612. Development builds omit +<build>. |
When an Optima app reports a build below the minimum Optima still supports, every REST request returns HTTP 426 with code CLIENT_UPDATE_REQUIRED, and the app offers its update. Requests without these headers, including your own clients, requests without a build number, and apps released before the header existed, are never rejected for their version. Both headers are optional descriptions of the caller, not credentials. The API also accepts the earlier names X-Plum-Client and X-Plum-Client-Version, which apps released before the Optima name still send.
Authentication and request bodies
| Caller | Authentication | Structured bodies |
|---|---|---|
| Account routes | Bearer token: an OAuth access token for the API origin, limited to its scopes, or an app session. Sign-in, refresh, and the health check need none. | JSON with Content-Type: application/json |
| Hardware device routes listed in Devices | Verified client certificate | JSON or CBOR. See JSON and CBOR. |
Byte transfers are the exception to structured bodies. Audio chunk uploads and private file uploads send raw bytes, not JSON or multipart form data. See Upload audio and Private files.
Responses
A successful structured response wraps its payload in data:
{
"success": true,
"data": { "...": "..." }
}Some responses have no envelope:
- Successful deletes and logout return
204 No Contentwith no body. - Audio and file downloads return raw bytes.
Request IDs
Optima returns an X-Request-ID header on API responses. Include it when you report a problem. You can also send your own X-Request-ID of up to 255 letters, digits, _, -, or =; Optima reuses a valid value and generates one otherwise. The header is optional.
Errors
An error response has success: false and an error object with a stable code and a human-readable message:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": [{ "path": "token", "message": "Enter the six-digit email code" }]
}
}error.details is present only on some errors. When a request fails schema validation, each entry names the invalid field path and a message. error.erase_at comes only with ACCOUNT_DELETED: when the deleted account and its data are erased, or null once they are.
Handle errors by status and code rather than by message text. Each status has a different meaning:
| Status | Example code | Meaning |
|---|---|---|
| 400 | BAD_REQUEST, UNSUPPORTED_API_VERSION, SIZE_MISMATCH | The request is malformed, pins an unsupported version, or sends the wrong number of bytes. |
| 401 | UNAUTHORIZED, UNAUTHENTICATED | The request needs a valid token (revoked, expired, and /mcp-only OAuth tokens are rejected), or a device request lacks a verified certificate. |
| 403 | FORBIDDEN, ACCOUNT_DELETED, REAUTHENTICATION_REQUIRED | The caller is authenticated but lacks access, including an OAuth token without the route's scope; the account is deleted (error.erase_at says until when it can be restored with its data); or deleting the account needs a recent sign-in. See Delete and restore an account. |
| 404 | NOT_FOUND | The requested resource was not found. |
| 409 | CONFLICT | An identity or state conflict, such as a retry with different metadata. |
| 410 | CONTENT_EXPIRED, CONTENT_DELETED, INVITE_EXPIRED | The record's content was removed by retention or a deletion (see Removed content), or an invite expired. Do not retry. |
| 413 | PAYLOAD_TOO_LARGE, FILE_TOO_LARGE | The request body exceeds the route's limit. |
| 415 | UNSUPPORTED_MEDIA_TYPE | The request's Content-Type is not accepted by the route. |
| 422 | VALIDATION_ERROR | The input failed validation. See error.details when present. |
| 426 | CLIENT_UPDATE_REQUIRED | An Optima app reported a build Optima no longer supports. See Optima app versions. |
| 429 | TOO_MANY_REQUESTS | Too many requests. Wait before retrying. |
| 503 | SERVICE_UNAVAILABLE | A required service is temporarily unavailable. |
Not every route returns every status. Check the API reference for each route's responses.
Removed content
A workspace keeps audio, transcripts, and insights only for its retention settings, and a deletion removes a record's content at once. Optima keeps the removed record's identity, times, and relationships, and never returns an emptied value as if it were content. Removed content appears in one of three ways:
-
Lists leave removed records out. A turn whose transcript was removed, a memory, conversation, or suggestion that expired or was deleted, an audio chunk whose audio was removed, and a released file are not listed, searched, or recalled.
-
Reading or changing a removed record by ID returns 410. The error names why and when:
{ "success": false, "error": { "code": "CONTENT_EXPIRED", "message": "Turn expired under the workspace retention settings", "removed_at": "2026-09-21T23:10:00.123456+00:00" } }CONTENT_EXPIREDmeans the workspace's retention removed it;CONTENT_DELETEDmeans a deletion did.removed_atis when. Only the record's owner gets 410; anyone else gets 404, as for a record that never existed. Deleting a record is not refused: an expired record can still be deleted. -
Parts and references of live records carry a state. A turn's
audio, a stream'saudio, a signal'spayload_state, and thesourceof each piece of evidence or conversation member are{ "state": "available" | "expired" | "deleted", "removed_at" }. When the state is notavailable, the content next to it isnull(or an empty signal payload):{ "turn_id": "5f1c7d2e-8f6a-4b3c-9d2e-1a2b3c4d5e6f", "word_start": null, "word_end": null, "text": null, "source": { "state": "expired", "removed_at": "2026-09-21T23:10:00.123456+00:00" } }New states can appear; treat an unrecognized one like
expired. -
History leaves out removed revisions. A memory, conversation, or suggestion that expired and then received new content is live again, but its earlier revisions stay removed: revision lists skip them, and reading one by revision number returns 404.
In your client, hide removed items or label them "Expired" or "Deleted", disable playback
for audio that is not available, and never show an empty value as what was said. Treat a
410 as final: drop the item from your cache and do not retry it. Retention records one
workspace.retention_applied event per sweep instead of an event
per record; refetch or drop cached content when you receive it.
Pagination
List routes return a page inside data:
{
"success": true,
"data": {
"items": [],
"cursor": "opaque-cursor-value"
}
}To read every page:
- Make the first request without a
cursor. - If the response
cursoris not null, repeat the request with that value as thecursorquery parameter. - Keep every other query parameter the same while paging.
- Stop when
cursoris null. The page sequence is complete.
| Parameter | Behavior |
|---|---|
limit | Defaults to 25. The maximum is 100. |
cursor | Opaque value from the previous page. Omit it on the first request. |
Treat every cursor as opaque. Use it only with the same account, route, and query parameters that produced it, and do not parse, construct, or store it as a durable ID. Its format differs between routes and can change.
Optima rejects some misused cursors with 422 VALIDATION_ERROR. Turn and audio cursors are checked against the caller and the query that produced them. Other routes cannot detect every cursor reused with a different query, so following the rules above is your client's responsibility.
Turn lists also require absolute from and to timestamps. To refresh a time window whose contents may have changed, start again from its first page rather than reusing a cursor from an earlier snapshot.