API conventions
This page answers one question: what rules do all Tieroute HTTP and WebSocket APIs share? Status: draft, wire version 1. The rules apply to the client API and the server-to-server (S2S) API. The endpoint references only describe what's specific to each endpoint:
client-public.md: the client API, every endpoint a client calls on its homeservers2s-public.md: the S2S API, every endpoint one homeserver calls on another
Together they cover every endpoint in Appendix A of the Tieroute protocol specification. Section references (§) are to that specification.
Schemas are written in the Concise Data Definition Language (CDDL, RFC 8610), and reuse the types defined in the specification: identifier, device-key, signed, hash, msg-id, group-id, timestamp, and so on. Requirement keywords (MUST, SHOULD) are as in §1.1. That section also sets the default consequence of violating a MUST: a server rejects the request with 400 bad-request or the more specific code from the registry below, and a client drops what it received. This page states a consequence only where it differs from that default or needs a specific code.
Base URLs
| API | Base | Defined in |
|---|---|---|
| Client API | the client URL in the server document's apis entry for version 1 |
§8.1 |
| S2S API | the s2s URL in the same entry |
§8.1, §8.3 |
| Well-known | https://{name}/.well-known/tieroute/server and /handle |
§5 |
The server document is the only fixed address. Paths in the endpoint references are relative to the base: with a client base of https://chat.example/.well-known/tieroute/v1/client/ (the default in rerouted, the reference server), POST register means POST https://chat.example/.well-known/tieroute/v1/client/register. Clients and servers MUST NOT assume any other path. One that does fails against every server whose base differs.
Encoding
- Bodies are JSON (I-JSON, the Internet JSON profile, RFC 7493), with
Content-Type: application/tieroute+json. Servers MUST reject other content types on JSON endpoints with415 bad-content-type. The exceptions are raw media uploads and downloads:application/octet-streamfor files, and the image's own type for profile pictures (POST media/upload-public). - Write canonically where it's signed, and read leniently everywhere else (§4.1, §4.3). Receivers MUST accept any valid I-JSON body, in any member order and with any whitespace, except duplicate member names, which are always rejected (
bad-request). Byte-exact rules apply only to a signed object'sbtext and to hash inputs, which are defined with the JSON Canonicalization Scheme (JCS, RFC 8785). - Byte strings are base64url without padding (RFC 4648 §5), in JSON strings and in URLs. Non-canonical encodings are rejected (
bad-request). - Numbers are integers within ±(2⁵³ − 1). No floats.
- Every request and response body is a single JSON object. Even a list result is wrapped in an object (
{ "items": [...] }), so members can be added later without breaking anyone. - Unknown members are ignored in bodies (§19.2). An unknown value of a closed set (a delivery
kind, a chain op, a signed type tag) gets400 unknown-kind, so the sender knows the receiver may be on an older version. - Identifiers are already text and appear as-is in URLs:
/chain/tr:7f3ka7qmx2vbn4rtd6wycs5e. - Bodies are at most 1 MiB of JSON text, with these exceptions:
- media uploads, media downloads, and S2S
media/fetchresponses: up to 64 MiB of raw bytes (§14) backup/put: up to 2 MiB, since a segment holds up to 1 MiB before base64url (§7.5)
- media uploads, media downloads, and S2S
- S2S
deliverbatches MUST be split to fit under 1 MiB. A single delivery MUST fit on its own; base64url makes Messaging Layer Security (MLS) data about a third larger than its raw size. Larger bodies get413 too-large.
Methods
GET: reads. No body. Parameters go in the path or query string. Safe to retry.POST: everything else. The body is a JSON object, possibly empty ({}).- Retrying a
POSTMUST be safe. Every endpoint that creates something takes a client-chosen ID or is naturally idempotent, and each endpoint reference says which. The exception isinvites/create: a retry makes a second code, which is harmless, and the client can revoke the one it never showed.
Authentication
Client API
- After
POST auth/session(see the endpoint reference), the client sendsAuthorization: Bearer <token>on every request. - Tokens are opaque strings, at most 256 bytes. A token is bound to one device key.
- Tokens expire after 30 days without use, and each use extends them. The homeserver MUST revoke a device's tokens immediately when the device is removed from the chain (§7.6). After that, requests with those tokens get
401 unauthenticated. - Missing or bad tokens get
401 unauthenticated. Valid tokens without permission for the request get403 not-allowed.
S2S API
No headers. The request body is the signed s2s-request envelope (§8.3), and the endpoint-specific request goes in its body field. A request whose envelope fails verification gets 401, with bad-signature, or clock-skew when ts is outside the window. Responses are plain JSON, authenticated by TLS.
Well-known
Unauthenticated GETs (§5).
Responses and status codes
| Status | Meaning | Body |
|---|---|---|
200 OK |
Success | The endpoint's response map |
202 Accepted |
Accepted for processing; nothing further to report. Also used for deliberately silent results (§10.3) | {} |
204 No Content |
Success, nothing to return | none |
400 |
Malformed, or invalid against the schema or the spec's rules | error |
401 |
Not authenticated | error |
403 |
Authenticated, but not allowed | error |
404 |
No such resource | error |
409 |
Conflict with current state (stale head, stale epoch, and so on) | error |
410 |
Gone (expired, removed) | error |
413 |
Too large, or over quota | error |
415 |
Wrong content type | error |
429 |
Rate-limited | error, with retry |
501 |
The spec defines this endpoint, but this server doesn't implement it yet | error |
500, 503 |
Server trouble. Retry with backoff | error if possible |
Every error body is the error map from §4.4:
error = {
"error": tstr, ; a code from the registry below
? "msg": tstr, ; human-readable, for logs only; never shown to users as-is
? "retry": uint, ; seconds to wait, for 429 and 503
? "ids": [* msg-id], ; the deliveries an error is about, when a batch partly fails (wrong-home)
? "now": timestamp, ; with clock-skew: the server's clock (§8.4)
}
Clients MUST branch on error, never on msg. msg is free text that any server can word differently or change at any time, so a client that branches on it breaks without warning.
Silent rejections stay silent. S2S deliver and contacts/request never return not-allowed: allowlist, policy, and block rejections are reported as 202 Accepted, so nobody can probe them (§10.3).
Error codes
The registry of error codes. New endpoint references add to it.
| Code | Status | Meaning |
|---|---|---|
bad-request |
400 | Malformed JSON, duplicate member names, bad base64url, or a schema mismatch |
unknown-kind |
400 | An unknown value of a closed set, such as a delivery kind. The receiver may be on an older version |
bad-content-type |
415 | Not application/tieroute+json |
bad-signature |
400, 401 | A signed object failed verification: 400. An S2S request's own envelope failed verification: 401 |
invalid-chain |
400, 409 | A chain entry breaks a validation rule (§6.4): 400. A pushed or fetched chain conflicts with the cached one and isn't a valid recovery fork (§6.5): 409. msg says which |
clock-skew |
400, 401 | A timestamp is outside the allowed window (§6.4 rule 5): 400. An S2S request's ts is outside the window (§8.3): 401. The error carries the server's clock in now, and its msg says how far off the timestamp was and in which direction (§8.4) |
unauthenticated |
401 | Missing, expired, or revoked token |
not-allowed |
403 | Not permitted: policy, allowlist, or role |
registration-closed |
403 | This homeserver isn't registering new identities |
not-found |
404 | No such identity, group, or resource |
handle-taken |
409 | The requested handle belongs to someone else |
stale-head |
409 | The chain head moved. Re-fetch and retry (§6.4) |
stale-epoch |
409 | Another commit won this epoch (§11.6) |
wrong-home |
409 | S2S deliver: the receiver's copy of a sender's chain doesn't name the calling server as home, even after re-fetching. ids lists the deliveries. A refusal: don't retry (§10.2) |
awaiting-key |
409 | The hub is waiting for this epoch's key after a reg commit (§11.6). Retry shortly |
stale-version |
409 | Account data changed. Merge and retry (§7.4) |
bad-invite |
403 | A registration code is missing, wrong, used up, or revoked (§8.2). Contact requests never return it: they're silent (§9.2) |
already-exists |
409 | The resource exists, and the request doesn't match it |
expired |
410 | Past retention (inbox, media, hub commits), or a sign-in nonce that's unknown, used, or expired |
removed |
410 | Deleted after a report (§16.2) |
too-large |
413 | Body or file over the limit |
over-quota |
413 | Over the user's storage quota |
rate-limited |
429 | Slow down. See retry |
unavailable |
503 | Temporarily unavailable. See retry |
not-implemented |
501 | The endpoint exists in the spec but this server doesn't support it yet |
Pagination
Lists that can grow use opaque cursors:
- Request:
?after=<cursor>&limit=<n>. With noafter, the list starts at the beginning.limitdefaults to 100, with a maximum of 500. - Response:
{ "items": [...], ? "next": tstr }. Ifnextis present, there may be more, so request again withafter=<next>. - Cursors are opaque
tstrvalues, at most 128 bytes. Clients MUST NOT parse them. Servers can change their format at any time, so a client that parses them breaks without warning.
Time
- Timestamps are Unix seconds (
uint). - The homeserver is the clock (§8.4). Clients SHOULD use their homeserver's time for every timestamp they sign, from
nowinauth/challenge(or in aclock-skewerror, before they've signed in), and warn the user when the device's clock is more than a minute off. Every response also includes an HTTPDateheader, which servers use to notice a peer's clock drifting before it's far enough off to break federation. - The chain clock rule is split (§6.4 rule 5). Everyone rejects chain entries more than 10 minutes in the future. Only the homeserver accepting an append (
chain/append) also rejects entries more than 10 minutes in the past. Everyone else MUST accept old entries, since honest ones can arrive days late. A receiver that rejects them loses honest chain updates.
Rate limiting
Servers MAY rate-limit any endpoint, and return 429 rate-limited with retry. Clients MUST honor retry and back off exponentially on repeated 429s and 503s. A server MAY keep answering 429 rate-limited to a client that doesn't.
WebSocket streams
Used by the client API's GET stream.
- Connect: an ordinary HTTPS
GETwith a WebSocket upgrade, subprotocoltieroute.v1(Sec-WebSocket-Protocol). Servers MUST refuse other subprotocols. - Frames are text. Each frame is exactly one JSON object with a
"t"member naming the message type. Binary frames are a protocol error (close code 4400). - Authenticate in the first frame, not with HTTP headers, because some WebSocket clients can't set headers. The client's first frame MUST be
{ "t": "auth", "token": tstr }. The server closes the connection with code 4401 if it isn't. - Keepalive: either side MAY send
{ "t": "ping", "n": uint }, and the other replies{ "t": "pong", "n": uint }with the samen. Clients SHOULD ping every 30 seconds. A client MAY ping less often while it's sending other frames at least that often. A client that stays silent for 90 seconds risks having the server close the connection. - Errors that don't close the stream:
{ "t": "error", "error": tstr, ? "msg": tstr, ? "ref": any }, whererefechoes the request it's about, if any. - Close codes: 4400 protocol error, 4401 unauthenticated, 4403 not allowed (for example, the device was removed), 4429 rate-limited, 1001 server going away. Clients reconnect with exponential backoff, except after 4401 and 4403.
- Frames are at most 1 MiB.