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.
| Service | Base | What lives there |
|---|---|---|
| Directory | https://dir.<domain> | sign-in and platform tokens (OpenID Connect), people and groups |
| Coordinator | https://s2.<domain>/v1 | access policy, meetings, the node registry, the console |
| Messaging | https://chat.s2.<domain>/v1 | conversations and messages, calls (the call token included) and signalling, presence, bots, attachments |
| Calendar | https://dir.<domain>/cal/v1 | calendars, 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.
| Token | Issued to | How to get it | Lives |
|---|---|---|---|
| platform token | a person in an app | from the directory: /oauth/authorize (code + PKCE) and the exchange at /oauth/token; a local account's password — /oauth/signin/password | minutes; renewed with the refresh token at the directory's /oauth/token |
bot (bot_…) | a bot or an integration | by an administrator in the console: Bots | until revoked; every request is audited, reads included |
| service | a product service (messaging → coordinator) | from configuration when the role is installed | until reissued |
node (X-Node-Token, not Bearer) | a host holding a role | when the host is enrolled | until the host is revoked |
| enrolment | one-shot, in the URL path | POST /admin/api/enroll | one exchange, then 410 |
Three rules integrations trip over:
- 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.
- 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. - 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
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.
| Header | Direction | Means |
|---|---|---|
X-SimpleTwo-Protocol | server → client | what this service speaks |
X-SimpleTwo-Protocol-Min | server → client | the oldest client it still serves |
X-SimpleTwo-Client-Protocol | client → server | what 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 server | Server | Client |
|---|---|---|
| current | serves | nothing |
behind, ≥ Min | serves, and counts by version | a non-blocking "update available" |
below Min | refuses: client_too_old | a 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." }
codeis 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.erroris 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:
| Code | Means | What to do |
|---|---|---|
| 400 | the request is malformed or names something unknown | a bug: report it, do not retry |
| 401 | the token is absent, expired or revoked | renew and retry once |
| 403 | authenticated and not allowed | do not retry — this is an answer |
| 404 | no such thing, or none this caller may see | treat as absent |
| 409 | conflicting state (duplicate join, taken name) | resolve and retry |
| 410 | the shape is gone; the message names its replacement | fix the client |
| 429 | rate limited | back 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
- Get a bot token in the console (Settings → Bots) with the capabilities you need.
- 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.
- Check
X-SimpleTwo-Protocol-Minin responses and send your own version: an integration is a client like any other, and theMinfloor applies to it exactly as it does to the mobile app. - 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.