Skip to main content

API overview

Applies to: SimpleTwo 0.9.x · Checked: 20.09.2026

The platform has more than one API — one per service — and that is not fragmentation but a consequence of splitting the planes: communications and management have different availability requirements, so they have different addresses and different token lifetimes.

ServiceBaseWhat lives there
Directoryhttps://dir.<domain>sign-in and platform tokens (OpenID Connect), people and groups
Coordinatorhttps://s2.<domain>/v1access policy, meetings, the node registry, the console
Messaginghttps://chat.s2.<domain>/v1conversations and messages, calls (the call token included) and signalling, presence, bots, attachments
Calendarhttps://dir.<domain>/cal/v1calendars, entries, invitations, ICS and CalDAV

Only the directory lets a person in; the coordinator signs nobody in and issues no person a token. A conversation's call takes its pass from messaging (POST /v1/calls/token with room = conv-<id> and conv = <id>): only messaging knows the conversation's members and their roles. The coordinator keeps the join into a calendar meeting (POST /v1/meetings/{id}/join) — it knows the invitees and the host.

The full endpoint map of each service is in the reference section; it is generated from the code, so it does not lag behind a release.

Authentication: five different things​

All five travel in one Authorization: Bearer <token> header — and that is the only thing they have in common. Rights, lifetime and how you get one all differ.

TokenIssued toHow to get itLives
platform tokena person in an appfrom the directory: /oauth/authorize (code + PKCE) and the exchange at /oauth/token; a local account's password — /oauth/signin/passwordminutes; renewed with the refresh token at the directory's /oauth/token
bot (bot_…)a bot or an integrationby an administrator in the console: Botsuntil revoked; every request is audited, reads included
servicea product service (messaging → coordinator)from configuration when the role is installeduntil reissued
node (X-Node-Token, not Bearer)a host holding a rolewhen the host is enrolleduntil the host is revoked
enrolmentone-shot, in the URL pathPOST /admin/api/enrollone exchange, then 410

Three rules integrations trip over:

  1. A bot token is not a person's token. A bot is bounded by capabilities and by a conversation allowlist: what an employee sees may be closed to the bot, and the other way round. Testing under a human session and running under a bot is a reliable way to meet a 403 in production.
  2. 401 means "renew", not "retry". On a 401, exchange the refresh token once at the directory's /oauth/token (grant_type=refresh_token) and repeat the request; repeating with the same token produces the same 401.
  3. 403 deliberately covers both "no such thing" and "not yours" wherever telling them apart would disclose a directory the caller may not read.

Protocol version​

The full contract is a page of its own

What follows is a summary. The obligations of both sides are fixed in the client–server protocol: the surfaces, the request shapes, socket events, capabilities, time on the wire, and the rules by which any of it may change at all.

One monotonic integer, PROTOCOL_VERSION, covers both the HTTP shapes and the socket frames: it is one contract, and the day they version apart is the day a client can satisfy half of it. Not semver — the only question anyone asks is "is this client older than the server requires", and that is a comparison.

HeaderDirectionMeans
X-SimpleTwo-Protocolserver → clientwhat this service speaks
X-SimpleTwo-Protocol-Minserver → clientthe oldest client it still serves
X-SimpleTwo-Client-Protocolclient → serverwhat this client speaks

Headers rather than body fields: a client has to be able to learn it is behind from a call it was already making — including one that failed — and a header survives a 4xx whose body the client cannot parse.

Three states, and all three exist:

Client vs serverServerClient
currentservesnothing
behind, ≥ Minserves, and counts by versiona non-blocking "update available"
below Minrefuses: client_too_olda blocking screen: this build cannot talk to this server

The middle row matters most. Without it every contract change is a choice between breaking people silently and carrying an alias forever, and an alias can never be retired: "the last old client is gone" is not provable. With it a deprecation has an end — raise Min on a date, having watched the population report, after the app has warned its users for weeks.

Min is 4 today: a client that names an older version gets 426 with the code client_too_old. A request without the header counts as version 0 and is served — it claims nothing to be judged by — but such a client never learns it is behind either. When planning an integration, send X-SimpleTwo-Client-Protocol.

Errors​

{ "code": "call_step_is_signalling",
"error": "This version of the app can no longer place calls — please update it." }
  • code is machine-readable, lower case with underscores, and is never renamed and never reused. It is an identifier: renaming one changes what an older client displays.
  • error is an English sentence and stays forever as the fallback for a client that does not know the code. It is shown to users whatever we might prefer: installed clients display it verbatim.

The full catalogue is on the error codes page; it is generated from the declarations in the service.

Status codes carry meaning, and distinguishing them is mandatory:

CodeMeansWhat to do
400the request is malformed or names something unknowna bug: report it, do not retry
401the token is absent, expired or revokedrenew and retry once
403authenticated and not alloweddo not retry — this is an answer
404no such thing, or none this caller may seetreat as absent
409conflicting state (duplicate join, taken name)resolve and retry
410the shape is gone; the message names its replacementfix the client
429rate limitedback off; the socket is unaffected

Events: one socket​

GET /v1/ws?token=…&device=… on the messaging host. One socket per device, not one per conversation: event order is guaranteed only within a connection, and several connections from one device are a race that cannot be fixed on the client.

Frame shapes, delivery order and what to do on a disconnect are in the events section of the protocol contract.

Where to start​

  1. Get a bot token in the console (Settings → Bots) with the capabilities you need.
  2. Look at what automations already do: outbound webhooks and event-driven rules cover most tasks without polling the API, and an administrator configures them in the console without code.
  3. Check X-SimpleTwo-Protocol-Min in responses and send your own version: an integration is a client like any other, and the Min floor applies to it exactly as it does to the mobile app.
  4. Read the contract — above all §1 (the server stamps identity), §5 (three surfaces) and §14 (how it is extended). Most integration mistakes are an attempt to tell the server something it already knows about you.