Plum / developers

Authentication

OAuth access tokens, app sessions, personal access tokens, scopes, and sign-out.

Optima account REST requests accept three kinds of bearer token:

  • An OAuth access token from Optima's authorization server. Its scopes are the permissions it carries. See OAuth access tokens.
  • An app session from the email-code routes under /auth. It carries every account permission. See Sign in with an email code.
  • A personal access token (optima_pat_...) that you create for your own scripts. It carries the permissions it lists, which never include the workspace permissions. It cannot create or list tokens, revoke any token but itself, sign out, or delete the account. See Personal access tokens.

MCP clients use OAuth, described in MCP authorization, or a personal access token.

Send X-API-Version: 1 on every REST request in this guide, including sign-in and refresh. See API conventions for version and error behavior.

Sign in with an email code

Signing in takes two requests.

  1. Request a code with POST /auth/otp:

    { "email": "you@example.com" }

    The response confirms the request was submitted. It does not reveal whether an account already exists for that address.

    The body also accepts an optional captcha_token string. Optima forwards it to its sign-in provider. If CAPTCHA verification fails, the request returns 400 BAD_REQUEST.

  2. Submit the code with POST /auth/verify-otp:

    { "email": "you@example.com", "token": "123456" }

    A successful verification returns a session in data.

Session fields

FieldDescription
access_tokenBearer token for account routes
refresh_tokenToken used to obtain a new session
token_typeAlways bearer
expires_inAccess token lifetime in seconds
expires_atAccess token expiry as a Unix timestamp in seconds
userThe signed-in account's id and email

Call account routes

Send the access token in the Authorization header. For example, GET /me returns the signed-in account:

curl https://api.getoptima.com/me \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  -H 'X-API-Version: 1'
{
  "success": true,
  "data": {
    "id": "ACCOUNT_ID",
    "email": "you@example.com",
    "name": "Home",
    "developer_enabled": false,
    "permissions": ["account:manage", "turns:read", "insights:read"],
    "sharing_enabled": false,
    "agent_enabled": false,
    "workspaces": [
      { "id": "WORKSPACE_ID", "name": "My workspace", "private": true, "role": "owner" }
    ]
  }
}
FieldMeaning
developer_enabledWhether the account has developer access
permissionsPermissions the calling credential can use: everything the account holds for an app session, narrowed to the token's scopes for an OAuth token (the example is abbreviated)
sharing_enabledWhether the account can use workspace sharing
agent_enabledWhether the account can use the experimental agent
workspacesWorkspaces the account belongs to, with its role there and whether it is the account's private workspace: its private workspace first, then the others by name

Every account has one private workspace, created with the account, which only it belongs to. Accounts with workspace sharing create more workspaces and manage their members and invitations through the workspace API. Treat unknown permission names as not applicable to your client; new permissions may appear.

Keep tokens in secure storage and never put them in URLs.

To name the account, call PATCH /me with { "name": "Home" }, or null to clear it. A name is trimmed and must contain 1–80 characters. The response is the updated account. Permissions, workspaces, and developer access cannot be changed through PATCH /me.

Developer access

Developer access is an account setting (the developer.tools entitlement) that Optima staff turn on. It adds the debug:read, debug:write, and pipelines:experimental permissions, which pipeline diagnostics and experimental pipelines require. An account cannot grant it to itself, and an OAuth scope alone never grants it: an OAuth token can use one of these permissions only when its scopes include it and the account has developer access. Use developer_enabled or permissions to show developer controls, while relying on API authorization for access checks. A change to developer access emits account.updated with permissions and developer_enabled in its changed fields.

Content layers

Recordings, transcripts, and insights are three layers, each with its own scopes and an account setting: the audio.access, transcripts.access, and insights.access entitlements. Every plan includes all three, so they are on unless Optima staff turn one off for an account. While a layer is off, its two permissions are missing from permissions and every credential, including an app session, receives 403 FORBIDDEN on that layer's routes; the other layers keep working. A change emits account.updated with permissions in its changed fields.

Workspace sharing

Workspace sharing is an account setting (the workspace.sharing entitlement) that Optima staff turn on; it is off by default. Without it, the workspace API serves the account's own workspaces and its private workspace's settings, and still lets the account leave a shared workspace, transfer its ownership, and delete one it owns; other sharing operations return 403 WORKSPACE_SHARING_REQUIRED. It adds no permission and no OAuth scope, so permissions does not change; use sharing_enabled to show sharing features. A change emits account.updated with sharing_enabled in its changed fields.

Experimental agent

The experimental agent (chat, automations, and the agent's memory in the account app) is an account setting (the agent.experimental entitlement) that Optima staff turn on; it is off by default and no plan includes it. It adds no permission and no OAuth scope, so permissions does not change; use agent_enabled to show or hide the agent. A change emits account.updated with agent_enabled in its changed fields.

Refresh a session

To obtain a new session, call POST /auth/refresh with the stored refresh token:

{ "refresh_token": "YOUR_REFRESH_TOKEN" }

The response contains a new session with the same fields as sign-in. Replace both stored tokens with the returned values.

Sign out

Call POST /auth/logout with the current bearer token. It returns 204 No Content and signs out only this session; the account's other sessions stay signed in. Delete both stored tokens. Sign-out revokes the current session's refresh capability, but an already-issued access token may remain valid until its expires_at time.

Sign out with /me/sign-out

POST /me/sign-out is the sign-out endpoint for Optima's own apps. It accepts an OAuth access token or an app session, not a personal access token, works for a deleted account, and returns 204 No Content. If the access token has expired, refresh it first. Then delete stored tokens.

With no body (or {}), it signs out only the calling app. For an OAuth token it revokes that token's grant, meaning its access and refresh tokens; for an app session it ends that session. Other apps and the browser sign-in session stay signed in, so signing in to an app again completes without a new code.

curl -X POST https://api.getoptima.com/me/sign-out \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "X-API-Version: 1"

Sign out everywhere

POST /me/sign-out with {"everywhere": true} signs the account out of every client. It requires account:manage. It:

  • revokes every OAuth grant for the account, across all clients and installations, including MCP connections;
  • ends the browser sign-in session, so the next authorization asks for a new email code;
  • revokes every personal access token;
  • when called with an app session, also signs out every app session for the account.

Revoked OAuth tokens stop working within about a minute everywhere. Treat the next 401 as signed out.

People can also sign out everywhere themselves from their Optima account settings, which does the same after a confirmation.

Connected apps

Each OAuth grant is a connected app: one per installation of an Optima app, and one per third-party client such as an MCP assistant. GET /me/connections lists the account's connected apps, newest first, with the usual limit and cursor pagination. DELETE /me/connections/{id} disconnects one: it revokes that grant and its access and refresh tokens, so the app must sign in again, and returns 204 No Content. Other apps stay connected. Both require account:manage and a signed-in app; a personal access token receives 403 FORBIDDEN. An ID that is not one of your connected apps returns 404 NOT_FOUND.

curl https://api.getoptima.com/me/connections \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "X-API-Version: 1"
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "Ab3dE5gH7jK9mN1p",
        "client_id": "plum-mac",
        "name": "Plum for Mac — Studio",
        "scopes": ["audio:write", "devices:manage", "turns:read"],
        "created_at": "2026-09-18T09:30:00.000Z",
        "current": false
      }
    ],
    "cursor": null
  }
}

name is the installation's label or the client's name. current is true for the grant behind the access token making the request; disconnecting it ends that app's own access. scopes can include values added after your client was built; show an unrecognized one as a generic permission. To disconnect every app at once, sign out everywhere.

People see and disconnect their connected apps under Connected apps in their Optima account settings.

See list connected apps and disconnect an app.

Delete and restore an account

DELETE /me deletes the signed-in account and returns 204 No Content. The deletion takes effect at once; the account's data is erased 7 days later unless the account is restored first.

Sign in again before deleting

Deleting needs a recent sign-in: the credential must come from an email code entered in the last 10 minutes. Otherwise DELETE /me returns 403 REAUTHENTICATION_REQUIRED and changes nothing. Ask the person to sign in again, then retry with the new credential:

  • The iPhone and Mac apps run native first-party sign-in again, which always asks for a code.
  • Browser clients send the person to /oauth/authorize with prompt=login, which asks for a code even when the browser is already signed in to Optima. Without it, an authorization completed from the browser's sign-in session counts from the code that started that session.
  • App-session clients sign in again with POST /auth/otp and POST /auth/verify-otp.

Refreshing a token does not renew the sign-in. Personal access tokens cannot delete the account.

What deletion does at once

  • Every account route, MCP request, and request from a device the account owns returns 403 ACCOUNT_DELETED (see below).
  • Every OAuth grant and personal access token is revoked and the browser sign-in session ends, as sign out everywhere does. An app session used for the deletion stays signed in so it can sign out or restore.
  • The account's active event endpoints are disabled and their pending deliveries cancelled. Endpoints you paused or disabled stay as they were.
  • In shared workspaces that have other members, its membership and role end and the workspace stays; pending invitations to shared workspaces nobody else belongs to are revoked. If the account is the only owner of a shared workspace that has other members, deletion is refused with 409 WORKSPACE_OWNER_REQUIRED before anything changes; make another member an owner first.
  • account.deleted is recorded, and Optima emails the account's address with the date its data will be erased and a link to restore it.

The account's private workspace, the shared workspaces nobody else belongs to, and all its data are kept, unreadable, until the erase time.

Erasure after 7 days

At the erase time every workspace where the account is still the only member is deleted: its private workspace, and any shared workspace nobody else belongs to. Each records workspace.deleted in event history. Deleted workspaces are permanent: they are never restored or reused. The data is then erased in the background; erasing it emits no further events. The account's recordings, transcripts, insights, voice profiles, contacts, sources, uploaded files, and webhook endpoints are erased, and its recorders are released, so they must be claimed again. Erasure does not yet cover chats with the Optima agent (its threads and assistant chat history) or an Omi connection (its link to the account and any audio received from Omi but not yet uploaded). The account itself, its name, its email preferences, the names of its revoked personal access tokens, its event history, its declared memory context, and its pipeline profiles are kept.

While an account is deleted

While an account is deleted, every account route, MCP request, and request from a device the account owns returns 403 ACCOUNT_DELETED. The error's erase_at says when the account and its data are erased; it is null once they are erased or being erased:

{
  "success": false,
  "error": {
    "code": "ACCOUNT_DELETED",
    "message": "Account deleted",
    "erase_at": "2026-10-07T18:00:00.000Z"
  }
}

Three routes still accept the caller's credential:

RouteUse
POST /auth/logoutSign out this app session.
POST /me/sign-outSign out this app, or every app with {"everywhere": true}.
POST /me/restoreRestore the account. Returns 204 No Content; restoring an active account has no effect. Returns 409 ACCOUNT_DELETION_IN_PROGRESS while the data is being erased after the erase time; retry later.

Sign-in does not check account state: a deleted account signs in through any flow and gets tokens, and its requests then return ACCOUNT_DELETED. A client that receives it offers to restore the account (POST /me/restore) or sign out, and shows erase_at as the deadline for getting the data back. People can also restore a deleted account themselves at auth.getoptima.com/restore after an email code; clients without account:manage send people there. MCP errors for a deleted account include that address.

Restoring before erase_at brings the account back as it was: its private workspace and all its data, the shared workspaces nobody else belonged to, its recorders, and the event endpoints the deletion disabled, which receive the account.restored event. Revoked OAuth grants, sessions, and tokens stay revoked, and ended memberships of shared workspaces and revoked invitations stay ended. Restoring after the erase time starts the account fresh: sign-in works again and it gets a new, empty private workspace, but the erased data, its recorders, its former workspaces and memberships, and its tokens and endpoints do not come back. Restoring is the only way back: signing in or being invited to a workspace does not restore a deleted account, and an invitation stays pending until the restored account accepts it.

See get your account, update your account, delete your account, restore your account, and sign out everywhere.

Browser integrations

Browser clients use the same bearer session. Their requests are subject to the API's configured origin allowlist.

DirectionHeaders
Request headers a browser may sendAuthorization, Content-Type, X-API-Version, X-Request-ID
Response headers a browser can readX-API-Version, X-Request-ID

X-Request-ID is optional. Optima returns one on API responses; include it when you report a problem. See API conventions.

OAuth access tokens

Optima's authorization server is the API itself. Clients discover it from https://api.getoptima.com/.well-known/oauth-authorization-server and use the authorization-code flow with PKCE (S256). Every authorization request needs a code_challenge with code_challenge_method=S256, including from confidential clients; without one, the redirect URI receives error=invalid_request. https://api.getoptima.com is the issuer and the MCP resource. Connections made earlier through https://api.plum.hnf.dev keep working there until they are reconnected.

EndpointUse
GET /oauth/authorizeStart authorization in the browser.
POST /oauth/tokenExchange a code, refresh tokens, or revoke a token (RFC 7009).
POST /oauth/registerRegister a third-party client (RFC 7591).
POST /oauth/introspectLook up the account behind an access token, for confidential Optima clients (RFC 7662).
GET /oauth/end-sessionEnd the browser's Optima sign-in session, for Optima's own browser apps (OpenID Connect RP-initiated logout).

Register a client

POST /oauth/register accepts standard RFC 7591 metadata. Optima refuses:

  • a client_name containing "Optima" or "Plum", with invalid_client_metadata;
  • a redirect URI that is not https, http on localhost, 127.0.0.1, or [::1], or a private-use scheme in reverse-DNS form such as com.example.app:/callback (RFC 8252), with invalid_redirect_uri.

The consent screen shows the redirect URI's host (or private-use scheme) as where the person returns after approving.

Attempt limits

Sign-in and registration are rate limited per network: sending and checking email codes, native sign-in, and registration. Over the limit, the sign-in website asks the person to wait a minute, and POST /oauth/register answers 429 with error=temporarily_unavailable. A browser sign-in ends after five incorrect codes; the person starts again from the app.

In the browser, the person signs in with a six-digit email code. Access tokens last one hour. Refresh tokens last 30 days and rotate on every use: store the new refresh token each time.

Send the access token as Authorization: Bearer ACCESS_TOKEN. OAuth requests to /oauth/* do not use X-API-Version; REST requests with an OAuth token still send it.

Token audience

A token names the resource it was issued for, chosen with the RFC 8707 resource parameter:

ResourceAccepted on
https://api.getoptima.com (the API origin)REST account routes and /mcp
https://api.getoptima.com/mcp/mcp only

Optima's own apps default to the API origin; other clients default to /mcp. Any other resource value returns an invalid_target error to the client's redirect URI. Staging uses https://staging.api.plum.hnf.dev in the same way.

Scopes

Scopes are Optima permission names. A token's scopes are exactly what it may do: a request outside them returns 403 FORBIDDEN, even though the same route works with an app session.

Content has three layers. Each has a read scope and a write scope:

LayerScopesAllows
Recordingsaudio:read, audio:writeRead audio, sources, and streams; register sources and upload audio
Transcriptsturns:read, turns:writeRead turns, contacts, sources, streams, source signals, and account events; correct attribution, create, rename, and delete contacts, and delete turns
Insightsinsights:read, insights:writeRead suggestions and the session summary; edit, group, and give feedback on suggestions
  • Layers are separate. insights:read does not include turns:read: reading the turns an insight cites needs turns:read as well.
  • Recall reads two layers. GET /search and POST /recall, and the MCP search and recall tools, need both turns:read and insights:read.
  • Conversations and memories. insights:read serves them only inside the session summary of a meeting you hosted. Reading them in general is a diagnostic that needs debug:read and developer access.
  • Every plan includes the three layers. Optima staff can turn a layer off for an account (the audio.access, transcripts.access, and insights.access entitlements); see content layers.

The other scopes control and manage; they are not content:

ScopeAllows
account:manageRead, rename, delete, and restore the account; sign out everywhere
files:read, files:writeRead and manage private files
devices:manageList, claim, and remove devices; start, join, extend, and end recorder sessions
endpoints:manageManage event endpoints
profile:read, profile:writeRead and update declared context
pipelines:read, pipelines:writeRead or manage pipelines
pipelines:experimentalUse and manage non-default pipelines, with developer access; pipelines:write includes it. With developer access, pipelines:read, or the scopes recall needs, also lists and selects non-default pipelines, but does not create or update them
debug:read, debug:writeDiagnostics, with developer access
workspace:readList workspaces and members, list and answer your invitations, and leave a shared workspace (POST /workspaces/{id}/leave, or removing yourself with DELETE /workspaces/{id}/members/{user_id})
workspace:membersInvite members, remove other members, and change members' roles, where your role is admin or owner
workspace:manageCreate workspaces; rename, delete, and transfer those you own

The workspace scopes are also workspace role permissions: a request needs the scope and a role in the target workspace that holds it (member: workspace:read; admin: adds workspace:members; owner: adds workspace:manage). See workspaces. Only Optima's web app, iPhone app, and account app at getoptima.com may request them; the Mac app, the agent, and third-party clients cannot, and a personal access token never carries them.

There are no staff scopes: Optima's staff console is a separate system, and a staff member's own app sessions and tokens carry only the permissions above.

Earlier scope names

Grants and personal access tokens that carry these names keep working, and a client may still request them, except sources:write, which new authorizations cannot request. Request the current names in a new client; the discovery document's scopes_supported lists only those.

Earlier scopeActs as
suggestions:read, suggestions:writeinsights:read, insights:write
contacts:read, contacts:writeturns:read, turns:write
events:readturns:read
recall:readSearch and recall only. It reads no turns, contacts, events, or suggestions by itself
sources:writeaudio:write and devices:manage, on existing MCP grants only

A grant keeps the scope names its client requested: the token response and connected apps list those names. GET /me, token introspection, and personal access tokens report current permission names.

Optima apps

Plum's own apps are registered clients: plum-web, plum-ios, plum-mac, plum-agent, and optima-app (the account, workspace, and meetings app at getoptima.com). They skip the consent screen and may request only their registered scopes; omitting scope requests all of them. Each installation keeps its own grant, so signing in on a second Mac does not sign out the first. An app can pass installation_label (up to 80 characters, such as "Plum for Mac — Studio"); connected apps show it as the name.

The web app, the account app, and the agent sign in through the browser at /oauth/authorize with fixed redirect URIs. The iPhone and Mac apps sign in with their own screens through native first-party sign-in.

After a successful email code in the browser, the browser keeps an Optima sign-in session for 30 days, extended while it is used, and never longer than 90 days after that code. While it lasts, authorizing another browser-based Optima app redirects straight back without another code. Native sign-in does not use or create this session. Third-party clients always ask for a code and consent.

GET /oauth/authorize supports these OpenID Connect prompt values:

promptBehavior
loginAlways ask for a new email code, even with a sign-in session. Used before deleting the account.
select_accountWith a sign-in session, ask the person to Continue as the signed-in account or Use a different email. Continuing counts from the code that started the session, like a silent authorization. login select_account asks for a code.
noneNever show a page: complete from the sign-in session, or redirect with error=login_required. Third-party clients always receive login_required. Combining none with login or select_account returns invalid_request.

Other prompt values are ignored.

To sign a browser out of its Optima session, an Optima browser app sends it to GET /oauth/end-session?client_id=CLIENT_ID&post_logout_redirect_uri=URL. The return URL must have the same origin as one of the client's registered redirect URIs; the API ends the session and redirects there. Other clients and return addresses receive 400. The request needs no token, so a link elsewhere could sign a browser out of its Optima session, but nothing more: app grants stay valid and the next sign-in asks for a code.

Token introspection

A confidential Optima client, such as the agent (plum-agent) or the account app (optima-app), holds a client secret and exchanges codes at /oauth/token with HTTP Basic client authentication (client_secret_basic). It learns which account signed in by introspecting the access token it received:

curl https://api.getoptima.com/oauth/introspect \
  -u plum-agent:CLIENT_SECRET \
  -d token=ACCESS_TOKEN
{
  "active": true,
  "token_type": "Bearer",
  "client_id": "plum-agent",
  "sub": "ACCOUNT_ID",
  "username": "you@example.com",
  "scope": "turns:read"
}

sub is the account ID. For the agent's own tokens the response also carries agent_enabled: whether the account has the experimental agent turned on. A token that is expired, revoked, a refresh token, or issued to another client returns { "active": false }. Missing or wrong client credentials return 401 with invalid_client; public clients cannot introspect.

Native first-party sign-in

The iPhone and Mac apps (plum-ios and plum-mac) collect the email address and code in their own screens, without a browser, through the authorization challenge endpoint defined by the IETF draft OAuth 2.0 for First-Party Applications. Authorization server metadata lists it as authorization_challenge_endpoint. Other clients receive unauthorized_client and use the browser flow.

Requests are POST with Content-Type: application/x-www-form-urlencoded. Responses are JSON with Cache-Control: no-store.

  1. Create a PKCE verifier and its S256 challenge, then send the email address:

    curl https://api.getoptima.com/oauth/authorize-challenge \
      -d client_id=plum-ios \
      -d response_type=code \
      -d code_challenge=CODE_CHALLENGE \
      -d code_challenge_method=S256 \
      -d scope='turns:read account:manage' \
      -d installation_label='Plum for iPhone — Ada' \
      -d email=you@example.com

    scope and installation_label are optional. Send captcha_token when CAPTCHA is enabled. Optima emails a six-digit code and answers 403, whether or not an account exists for the address:

    {
      "error": "insufficient_authorization",
      "error_description": "Enter the six-digit code sent to the email address",
      "auth_session": "AUTH_SESSION",
      "expires_in": 600
    }
  2. Send the code with the auth_session:

    curl https://api.getoptima.com/oauth/authorize-challenge \
      -d auth_session=AUTH_SESSION \
      -d otp=123456

    On success the response is 200 with a short-lived authorization code:

    { "authorization_code": "AUTHORIZATION_CODE" }

    An incorrect or expired code answers 403 insufficient_authorization with the same auth_session, so the person can try again.

  3. Exchange the code at the token endpoint with the verifier. Omit redirect_uri:

    curl https://api.getoptima.com/oauth/token \
      -d grant_type=authorization_code \
      -d client_id=plum-ios \
      -d code=AUTHORIZATION_CODE \
      -d code_verifier=CODE_VERIFIER

    The tokens are ordinary Optima OAuth tokens for the API origin: their scopes are their permissions, refresh tokens rotate, and signing out of one app revokes them.

An auth_session lasts 10 minutes and works for one successful sign-in. After five incorrect codes it ends.

StatuserrorMeaning
400invalid_requestA required parameter is missing or malformed, such as response_type, an S256 code_challenge, the email address, or a six-digit otp, or CAPTCHA verification failed.
400unauthorized_clientThe client cannot use native sign-in; use the browser flow.
400invalid_scopeA scope is outside the client's registered scopes.
400invalid_sessionThe auth_session is unknown, expired, already used, or ended after too many incorrect codes. Start again from step 1.
403insufficient_authorizationEnter the emailed code; retry with the returned auth_session.
429temporarily_unavailableToo many requests; wait before trying again.
503temporarily_unavailableSign-in is unavailable; try again later.

Sign out of one app

Optima's own apps call POST /me/sign-out. Any OAuth client can also revoke its refresh token at the token endpoint (RFC 7009), then delete stored tokens:

curl https://api.getoptima.com/oauth/token \
  -d client_id=plum-web \
  -d token=REFRESH_TOKEN \
  -d token_type_hint=refresh_token

Revoking the refresh token revokes that installation's grant and its access tokens. Other apps and the browser sign-in session stay signed in. To sign out of every app, use sign out everywhere.

MCP authorization

MCP clients use the same OAuth server. They request the /mcp resource, so their tokens work on /mcp but not on REST routes. They register dynamically and show a consent screen.

To connect a compatible client, follow MCP.

Plum for Mac needs insights:read and insights:write to display suggestions and save feedback. A Mac grant issued without them keeps its original permissions; sign out and sign in again to add them.

On this page