Skip to content

Draft. This text can change at any time, and there's no version history until the draft is frozen. Comments stay attached to the passage they were made on; if it changes, they move to its section, marked as on an earlier version.

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 homeserver
  • s2s-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 with 415 bad-content-type. The exceptions are raw media uploads and downloads: application/octet-stream for 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's b text 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) gets 400 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/fetch responses: 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)
  • S2S deliver batches 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 get 413 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 POST MUST be safe. Every endpoint that creates something takes a client-chosen ID or is naturally idempotent, and each endpoint reference says which. The exception is invites/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 sends Authorization: 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 get 403 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 no after, the list starts at the beginning. limit defaults to 100, with a maximum of 500.
  • Response: { "items": [...], ? "next": tstr }. If next is present, there may be more, so request again with after=<next>.
  • Cursors are opaque tstr values, 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 now in auth/challenge (or in a clock-skew error, 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 HTTP Date header, 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 GET with a WebSocket upgrade, subprotocol tieroute.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 same n. 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 }, where ref echoes 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.

Versioning

  • The wire version is in the server document's apis entries and in the WebSocket subprotocol (tieroute.v1), not in any fixed path (§8.1, §19.1).
  • Version 1 can still change until the outside review is done (§19.1). Once version 1 is frozen, changes are logged in the specification's changelog (§22).

© 2026 Reroute Holdings LLC

A public draft of Tieroute v1, open for comments.