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.
-
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_tokenstring. Optima forwards it to its sign-in provider. If CAPTCHA verification fails, the request returns 400BAD_REQUEST. -
Submit the code with
POST /auth/verify-otp:{ "email": "you@example.com", "token": "123456" }A successful verification returns a session in
data.
Session fields
| Field | Description |
|---|---|
access_token | Bearer token for account routes |
refresh_token | Token used to obtain a new session |
token_type | Always bearer |
expires_in | Access token lifetime in seconds |
expires_at | Access token expiry as a Unix timestamp in seconds |
user | The 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" }
]
}
}| Field | Meaning |
|---|---|
developer_enabled | Whether the account has developer access |
permissions | Permissions 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_enabled | Whether the account can use workspace sharing |
agent_enabled | Whether the account can use the experimental agent |
workspaces | Workspaces 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/authorizewithprompt=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/otpandPOST /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_REQUIREDbefore anything changes; make another member an owner first. account.deletedis 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:
| Route | Use |
|---|---|
POST /auth/logout | Sign out this app session. |
POST /me/sign-out | Sign out this app, or every app with {"everywhere": true}. |
POST /me/restore | Restore 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.
| Direction | Headers |
|---|---|
| Request headers a browser may send | Authorization, Content-Type, X-API-Version, X-Request-ID |
| Response headers a browser can read | X-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.
| Endpoint | Use |
|---|---|
GET /oauth/authorize | Start authorization in the browser. |
POST /oauth/token | Exchange a code, refresh tokens, or revoke a token (RFC 7009). |
POST /oauth/register | Register a third-party client (RFC 7591). |
POST /oauth/introspect | Look up the account behind an access token, for confidential Optima clients (RFC 7662). |
GET /oauth/end-session | End 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_namecontaining "Optima" or "Plum", withinvalid_client_metadata; - a redirect URI that is not
https,httponlocalhost,127.0.0.1, or[::1], or a private-use scheme in reverse-DNS form such ascom.example.app:/callback(RFC 8252), withinvalid_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:
| Resource | Accepted 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:
| Layer | Scopes | Allows |
|---|---|---|
| Recordings | audio:read, audio:write | Read audio, sources, and streams; register sources and upload audio |
| Transcripts | turns:read, turns:write | Read turns, contacts, sources, streams, source signals, and account events; correct attribution, create, rename, and delete contacts, and delete turns |
| Insights | insights:read, insights:write | Read suggestions and the session summary; edit, group, and give feedback on suggestions |
- Layers are separate.
insights:readdoes not includeturns:read: reading the turns an insight cites needsturns:readas well. - Recall reads two layers.
GET /searchandPOST /recall, and the MCPsearchandrecalltools, need bothturns:readandinsights:read. - Conversations and memories.
insights:readserves them only inside the session summary of a meeting you hosted. Reading them in general is a diagnostic that needsdebug:readand developer access. - Every plan includes the three layers. Optima staff can turn a layer off for an account (the
audio.access,transcripts.access, andinsights.accessentitlements); see content layers.
The other scopes control and manage; they are not content:
| Scope | Allows |
|---|---|
account:manage | Read, rename, delete, and restore the account; sign out everywhere |
files:read, files:write | Read and manage private files |
devices:manage | List, claim, and remove devices; start, join, extend, and end recorder sessions |
endpoints:manage | Manage event endpoints |
profile:read, profile:write | Read and update declared context |
pipelines:read, pipelines:write | Read or manage pipelines |
pipelines:experimental | Use 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:write | Diagnostics, with developer access |
workspace:read | List 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:members | Invite members, remove other members, and change members' roles, where your role is admin or owner |
workspace:manage | Create 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 scope | Acts as |
|---|---|
suggestions:read, suggestions:write | insights:read, insights:write |
contacts:read, contacts:write | turns:read, turns:write |
events:read | turns:read |
recall:read | Search and recall only. It reads no turns, contacts, events, or suggestions by itself |
sources:write | audio: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:
prompt | Behavior |
|---|---|
login | Always ask for a new email code, even with a sign-in session. Used before deleting the account. |
select_account | With 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. |
none | Never 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.
-
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.comscopeandinstallation_labelare optional. Sendcaptcha_tokenwhen CAPTCHA is enabled. Optima emails a six-digit code and answers403, 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 } -
Send the code with the
auth_session:curl https://api.getoptima.com/oauth/authorize-challenge \ -d auth_session=AUTH_SESSION \ -d otp=123456On success the response is
200with a short-lived authorization code:{ "authorization_code": "AUTHORIZATION_CODE" }An incorrect or expired code answers
403 insufficient_authorizationwith the sameauth_session, so the person can try again. -
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_VERIFIERThe 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.
| Status | error | Meaning |
|---|---|---|
| 400 | invalid_request | A 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. |
| 400 | unauthorized_client | The client cannot use native sign-in; use the browser flow. |
| 400 | invalid_scope | A scope is outside the client's registered scopes. |
| 400 | invalid_session | The auth_session is unknown, expired, already used, or ended after too many incorrect codes. Start again from step 1. |
| 403 | insufficient_authorization | Enter the emailed code; retry with the returned auth_session. |
| 429 | temporarily_unavailable | Too many requests; wait before trying again. |
| 503 | temporarily_unavailable | Sign-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_tokenRevoking 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.