Plum / developers

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.

RequestResult
X-API-Version: 1Optima uses version 1 and echoes X-API-Version on the response.
Header omittedOptima selects the latest supported version.
Unsupported value, such as 2HTTP 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:

HeaderValue
X-Optima-ClientThe 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

CallerAuthenticationStructured bodies
Account routesBearer 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 DevicesVerified client certificateJSON 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 Content with 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:

StatusExample codeMeaning
400BAD_REQUEST, UNSUPPORTED_API_VERSION, SIZE_MISMATCHThe request is malformed, pins an unsupported version, or sends the wrong number of bytes.
401UNAUTHORIZED, UNAUTHENTICATEDThe request needs a valid token (revoked, expired, and /mcp-only OAuth tokens are rejected), or a device request lacks a verified certificate.
403FORBIDDEN, ACCOUNT_DELETED, REAUTHENTICATION_REQUIREDThe 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.
404NOT_FOUNDThe requested resource was not found.
409CONFLICTAn identity or state conflict, such as a retry with different metadata.
410CONTENT_EXPIRED, CONTENT_DELETED, INVITE_EXPIREDThe record's content was removed by retention or a deletion (see Removed content), or an invite expired. Do not retry.
413PAYLOAD_TOO_LARGE, FILE_TOO_LARGEThe request body exceeds the route's limit.
415UNSUPPORTED_MEDIA_TYPEThe request's Content-Type is not accepted by the route.
422VALIDATION_ERRORThe input failed validation. See error.details when present.
426CLIENT_UPDATE_REQUIREDAn Optima app reported a build Optima no longer supports. See Optima app versions.
429TOO_MANY_REQUESTSToo many requests. Wait before retrying.
503SERVICE_UNAVAILABLEA 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_EXPIRED means the workspace's retention removed it; CONTENT_DELETED means a deletion did. removed_at is 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's audio, a signal's payload_state, and the source of each piece of evidence or conversation member are { "state": "available" | "expired" | "deleted", "removed_at" }. When the state is not available, the content next to it is null (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:

  1. Make the first request without a cursor.
  2. If the response cursor is not null, repeat the request with that value as the cursor query parameter.
  3. Keep every other query parameter the same while paging.
  4. Stop when cursor is null. The page sequence is complete.
ParameterBehavior
limitDefaults to 25. The maximum is 100.
cursorOpaque 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.

On this page