# gdna-matrix

Public reference for people and agents building an embedded Matrix client against **this** homeserver. It describes what this build implements. An endpoint that is absent here is not implemented, even if the Matrix specification defines it.

- Server name: `matrix.gdna.ai`
- Public base URL: `https://matrix.gdna.ai`
- User IDs: `@localpart:matrix.gdna.ai`

Send Client-Server requests to `https://matrix.gdna.ai`. `GET /.well-known/matrix/client` advertises `https://matrix.gdna.ai` as `m.homeserver.base_url`. On the deployed homeservers the public host and the server name are the same, so those two URLs match.

Same-origin Markdown for this page is [`/docs/index.md`](/docs/index.md). The agent index is [`/docs/llms.txt`](/docs/llms.txt). No access token is required for any `/docs` page. `/docs.md` redirects to `/docs/index.md`.

## Contents

- [Server discovery](#server-discovery)
- [Authentication](#authentication)
- [Sync](#sync)
- [Sending and receiving](#sending-and-receiving)
- [Rooms](#rooms)
- [Media](#media)
- [End-to-end encryption](#end-to-end-encryption)
- [Endpoint reference](#endpoint-reference)
- [Rate limits and errors](#rate-limits-and-errors)
- [Federation](#federation)
- [Quickstart](#quickstart)
- [Not supported](#not-supported)


# Server discovery

A client should learn the base URL from discovery, then ask which specification versions this process speaks, before it tries to log in.

`GET /.well-known/matrix/client` is unauthenticated and returns:

```json
{"m.homeserver": {"base_url": "https://matrix.gdna.ai"}}
```

`GET /.well-known/matrix/server` is unauthenticated and returns:

```json
{"m.server": "matrix.gdna.ai:443"}
```

Federation traffic for this deployment is also accepted on port `8448`. The well-known delegation above names port `443`. Both listeners forward to this process. Do not hard-code `8448` in a client: follow `m.server`.

`GET /_matrix/client/versions` is unauthenticated. It is the first call a Client-Server client makes. This build advertises `r0.6.1` and `v1.1` through `v1.11`, and an empty `unstable_features` object. No unstable MSC is implemented.

`v1.11` is advertised so clients will use authenticated media (`/_matrix/client/v1/media/...`). The federation half of v1.11 media — `multipart/mixed` fetches under `/_matrix/federation/v1/media/` — is **not supported**. Remote media is still fetched over the legacy download path.

`GET /_matrix/key/v2/server` returns this server's federation signing keys. `GET /health` returns the plain text `ok` and is the load-balancer health check.

`GET /metrics` is a Prometheus scrape. It is not a Client-Server endpoint. With no `METRICS_TOKEN` configured it answers `404` and does not describe itself. With a token configured, the scraper must send `Authorization: Bearer <METRICS_TOKEN>`.


# Authentication

Embedded clients log in with **SSO**. The identity provider is Keycloak (OIDC) at `auth.gdna.io`. This deployment's issuer for the default realm is `https://auth.gdna.io/realms/gdna`. Additional tenants carry their own issuer. The homeserver redirects the browser to that realm; the client never talks to Keycloak directly except by following the redirect.

Password login (`m.login.password`) exists for accounts that have a password hash. Production registration is `disabled`, so new accounts are created by SSO (or the admin CLI), not by `POST /register`. Prefer SSO.

## Flows

`GET https://matrix.gdna.ai/_matrix/client/v3/login` lists flows. When OIDC is configured it includes:

- `m.login.password`
- `m.login.sso`, with one `identity_providers` entry per active tenant (`id` is the tenant slug) and any brokered upstream provider (this deployment advertises `google`)
- `m.login.token`, which is how the SSO dance finishes

The unsuffixed `GET /_matrix/client/v3/login/sso/redirect` works only when exactly one tenant is active. These deployments also run the `system` sentinel tenant, so that URL returns `400` `M_MISSING_PARAM`. Always call the suffixed form with an id from `identity_providers`.

## Redirect and loginToken

1. Send the browser to `GET https://matrix.gdna.ai/_matrix/client/v3/login/sso/redirect/{idpId}?redirectUrl={urlencoded callback}`.
2. `redirectUrl` is required. It must be same-origin with this homeserver, or on the operator allow-list `SSO_CLIENT_REDIRECT_ALLOWLIST`. The homeserver checks it before the flow starts and again before it attaches the login token. A rejected URL is `400` `M_INVALID_PARAM`. These deployments allow `https://matrix.gdna.io`, `https://matrix.gdna.ai`, `https://aeos.gdna.io`, `https://aeosdev.gdna.io`, `https://chat.gdna.io`, and `https://chat.gdna.ai`. An embedded client on any other origin has to be added to that list or the login token will not be delivered.
3. The browser is sent to the Keycloak authorization endpoint. The homeserver's own OIDC redirect URI is `https://matrix.gdna.ai/_matrix/client/v3/login/sso/callback`. A brokered id such as `google` adds Keycloak's `kc_idp_hint` so the realm skips its own chooser.
4. On success the browser lands on `redirectUrl?loginToken=...`. The login token is single-use and lives **60 seconds**. It is a credential: do not log it or put it in a `Referer`.
5. Exchange it immediately:

```http
POST https://matrix.gdna.ai/_matrix/client/v3/login
Content-Type: application/json

{"type":"m.login.token","token":"<loginToken>"}
```

The JSON response is `user_id`, `access_token`, and `device_id`. Optional fields you may send on that POST: `device_id`, `initial_device_display_name`.

## Access tokens

Send the access token as `Authorization: Bearer <token>`. The query-parameter form (`?access_token=`) is **not supported**.

Tokens last **24 hours** (`DEFAULT_TOKEN_LIFETIME_MS`). The login response does **not** include `refresh_token`. `POST /_matrix/client/v3/refresh` and `m.login.refresh` are **not supported**. When the token expires, run SSO again.

A missing or unknown bearer token is `401` with `errcode` **`M_UNKNOWN`** and `error` `"unauthorized"`. It is not `M_MISSING_TOKEN` or `M_UNKNOWN_TOKEN`. Treat `401` as "log in again". There is nothing to refresh.

`user_id` is `@localpart:matrix.gdna.ai`. SSO derives the localpart from the Keycloak `preferred_username` (lowercased; `@` and spaces become `-`; other characters outside the Matrix localpart grammar are dropped). The account is keyed on the OIDC `(issuer, sub)` pair. A preferred username that is already taken does **not** adopt that account.

## Logout

`POST https://matrix.gdna.ai/_matrix/client/v3/logout` with the bearer token and an empty JSON object revokes that token and deletes the device (and its published device keys). The response may include `sso_logout_url`, the Keycloak end-session URL. Open it if you also need the browser's IdP session to end. A client that ignores the field is still logged out of Matrix.

`POST https://matrix.gdna.ai/_matrix/client/v3/logout/all` revokes every device for the user, including the caller. It is not UIAA-gated.

`POST /_matrix/client/v3/account/deactivate` deactivates the authenticated account.

`DELETE /_matrix/client/v3/devices/{deviceId}` is **not supported**. Logout is how the current device is removed. `GET /devices` and `GET`/`PUT /devices/{deviceId}` exist (list, fetch, rename).


# Sync

`GET https://matrix.gdna.ai/_matrix/client/v3/sync` requires an access token.

| Query | Meaning |
|---|---|
| `since` | Omit for the initial sync. Thereafter send the previous response's `next_batch`. |
| `timeout` | Milliseconds to long-poll when an incremental sync has nothing new. Clamped to **30000**. `0` or omitted returns immediately. |
| `types` | Optional comma-separated event types to keep. This is the only filter `/sync` applies. |

The initial sync (`since` absent) does not long-poll. It returns the joined rooms and their current state and a timeline slice.

An incremental sync with `timeout` waits until a joined room has new timeline events, a to-device event arrives, or a device-list change is recorded — or until the timeout. Wakeups are local to the write and are also delivered across tasks through Postgres `LISTEN`/`NOTIFY`.

The response always has `next_batch` and `rooms` (`join`, and `leave` when the user has left a room). It also includes `to_device`, `device_lists`, `account_data` (including `m.direct`), and `presence` when those sections are non-empty. Timeline events for a joined room are `rooms.join[roomId].timeline.events`. Ephemeral events (typing, receipts) are under `rooms.join[roomId].ephemeral`, not the timeline.

`POST /user/{userId}/filter` and `GET /user/{userId}/filter/{filterId}` store a filter. `/sync` does **not** read a `filter` query parameter or a filter id. Sending one is ignored. Use `types` if you need to narrow event types.

Sliding sync is **not supported**.


# Sending and receiving

Receiving is `/sync`, then `GET /_matrix/client/v3/rooms/{roomId}/messages` for history, `GET /_matrix/client/v3/rooms/{roomId}/context/{eventId}` to jump to an event, and the `/relations` endpoints for reactions, edits, and threads.

Relations are under **`/v1/`**, not `/v3/`. The specification added them after the v3 rename. Three concrete routes exist:

- `GET /_matrix/client/v1/rooms/{roomId}/relations/{eventId}`
- `GET /_matrix/client/v1/rooms/{roomId}/relations/{eventId}/{relType}`
- `GET /_matrix/client/v1/rooms/{roomId}/relations/{eventId}/{relType}/{eventType}`

Sending a message:

```http
PUT https://matrix.gdna.ai/_matrix/client/v3/rooms/{roomId}/send/m.room.message/{txnId}
Authorization: Bearer <token>
Content-Type: application/json

{"msgtype":"m.text","body":"hello"}
```

`txnId` is chosen by the client. Repeating the same `txnId` from the same device returns the original `event_id` and does not create a second event. The success body is `{"event_id":"$..."}`.

The caller must already be joined to the room. State events use `PUT /_matrix/client/v3/rooms/{roomId}/state/{eventType}` (empty state key) or `PUT .../state/{eventType}/{stateKey}`. The empty-state-key form is also registered with a trailing slash, because clients send both.

Redaction is `PUT /_matrix/client/v3/rooms/{roomId}/redact/{eventId}/{txnId}` and is power-level enforced.

`GET /_matrix/client/v3/rooms/{roomId}/messages` pages history. `from` is a prior batch token; direction is `dir=b` (backwards) or `dir=f`.


# Rooms

Create a room with `POST https://matrix.gdna.ai/_matrix/client/v3/createRoom`. The body accepts `name`, `topic`, and `preset`. `preset: "public_chat"` makes the room publicly joinable. Any other preset, including omitting it, creates a private room. Private rooms are given `join_rule: restricted` against the caller's tenant Space. `trusted_private_chat` auto-power-level behaviour is **not supported**. The response is `{"room_id":"!..."}`.

Join a room this server already knows with `POST /_matrix/client/v3/rooms/{roomId}/join`. Join by id or alias with `POST /_matrix/client/v3/join/{roomIdOrAlias}`. Leave with `POST /_matrix/client/v3/rooms/{roomId}/leave`. Invite with `POST /_matrix/client/v3/rooms/{roomId}/invite` and body `{"user_id":"@someone:matrix.gdna.ai"}`. An invite into an isolated room across tenants is refused unless a live sharing grant exists.

Membership reads: `GET /rooms/{roomId}/members`, `GET /rooms/{roomId}/joined_members`, `GET /joined_rooms`.

Aliases: `GET /rooms/{roomId}/aliases`, and `GET`, `PUT`, `DELETE /_matrix/client/v3/directory/room/{roomAlias}`.

Directory: `GET` and `POST /_matrix/client/v3/publicRooms` are both authenticated, including the GET the specification marks as optionally public. `GET` and `PUT /_matrix/client/v3/directory/list/room/{roomId}` publish or unpublish a room.

Moderation, all authenticated and power-level enforced: `POST /rooms/{roomId}/kick`, `/ban`, `/unban`, `/forget`, and `POST /rooms/{roomId}/report/{eventId}`.

`GET /_matrix/client/v1/rooms/{roomId}/hierarchy` is **not supported**. Spaces can be created and linked with `m.space.child` and `m.space.parent` state events; a client walks those events through `/state`.

Knock, third-party invites, and room upgrading are **not supported**.


# Media

The content repository is supported when `MEDIA_STORE_PATH` is set. These deployments set it. If it is unset, the media routes still exist and answer `404` `M_UNRECOGNIZED` ("the content repository is not configured").

`GET /_matrix/media/v3/config` (and the authenticated `GET /_matrix/client/v1/media/config`) returns `m.upload.size`. The default cap is 50 MiB (`MEDIA_MAX_UPLOAD_BYTES`). There is also a per-user byte quota.

Upload with `POST /_matrix/media/v3/upload` (raw body, `Content-Type` of the file, optional `?filename=`). The response is `{"content_uri":"mxc://matrix.gdna.ai/<media id>"}`. To reserve an id first, `POST /_matrix/media/v1/create`, then `PUT /_matrix/media/v3/upload/{serverName}/{mediaId}`. Too many unfinished reservations is `429` `M_LIMIT_EXCEEDED`. Overwriting an existing object is `M_CANNOT_OVERWRITE_MEDIA`. A download of a reservation that has not been uploaded yet is `M_NOT_YET_UPLOADED`.

Legacy download (unauthenticated through v1.10; the media id is the secret):

- `GET /_matrix/media/v3/download/{serverName}/{mediaId}`
- `GET /_matrix/media/v3/download/{serverName}/{mediaId}/{fileName}`
- `GET /_matrix/media/v3/thumbnail/{serverName}/{mediaId}`

Authenticated equivalents, which require a bearer token:

- `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}`
- `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}/{fileName}`
- `GET /_matrix/client/v1/media/thumbnail/{serverName}/{mediaId}`

Downloads are served with a sandbox `Content-Security-Policy` (`default-src 'none'; script-src 'none'`), `X-Content-Type-Options: nosniff`, and `Content-Disposition: attachment` for types that are not safe to render inline. That policy applies to media responses, not to this documentation page.

`GET /_matrix/media/v3/preview_url` is routed and always returns `M_UNRECOGNIZED`. URL previews are **not supported**.

Remote homeserver media is fetched and cached when `MEDIA_REMOTE_CACHE_BYTES` is non-zero (these deployments set 10 GiB). Federation `multipart/mixed` media is **not supported**.


# End-to-end encryption

The server stores and forwards keys. It does not encrypt or decrypt room contents. Olm and Megolm sessions live in the client.

Device keys:

- `POST /_matrix/client/v3/keys/upload` — upload this device's device keys and one-time keys
- `POST /_matrix/client/v3/keys/query` — query device keys, including cross-signing keys (master, self-signing, user-signing) when they have been uploaded
- `POST /_matrix/client/v3/keys/claim` — claim one-time keys

Cross-signing:

- `POST /_matrix/client/v3/keys/device_signing/upload`
- `POST /_matrix/client/v3/keys/signatures/upload`

The first cross-signing upload is **not** gated on user-interactive authentication. These accounts have no local password, and a UIAA gate would make cross-signing unreachable. Replacing an existing cross-signing identity is forbidden (`M_FORBIDDEN`) unless the caller is allowed to replace it. See `api::client::cross_signing`.

To-device (key gossip, including encrypted to-device events): `PUT /_matrix/client/v3/sendToDevice/{eventType}/{txnId}`. Delivered events show up on the next `/sync` under `to_device`. The transaction id makes a retry idempotent.

Server-side key backup (room key escrow):

- `POST` and `GET /_matrix/client/v3/room_keys/version`
- `GET`, `PUT`, and `DELETE /_matrix/client/v3/room_keys/version/{version}`
- `PUT`, `GET`, and `DELETE /_matrix/client/v3/room_keys/keys`
- `PUT`, `GET`, and `DELETE /_matrix/client/v3/room_keys/keys/{roomId}`
- `PUT`, `GET`, and `DELETE /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}`

A backup request that names the wrong version is `M_WRONG_ROOM_KEYS_VERSION`.

Dehydrated devices and secret storage are **not supported**. There is no `/keys/changes` endpoint; device-list changes arrive on `/sync` in `device_lists`. SAS and QR verification are to-device messages the client drives itself. This server has no separate verification route.

Federated key exchange, used by other homeservers rather than by your client:

- `POST /_matrix/federation/v1/user/keys/query`
- `POST /_matrix/federation/v1/user/keys/claim`

`device_id` on login is the device these keys are bound to. `POST /logout` deletes that device and its keys. There is no `DELETE /devices/{deviceId}` route.

Dehydrated devices, secret storage (`/unstable/org.matrix.msc3814` and the stable secret-storage endpoints), and the key backup `/_matrix/client/v3/room_keys/version/{version}` room-key endpoints beyond the five routes above are what this server ships. The following are **not supported**: SAS verification as a server endpoint (verification is to-device traffic the client drives itself), `/keys/changes` as a separate poll (device-list changes arrive on `/sync` in `device_lists`), and QR-code verification shortcuts.


# Endpoint reference

Every row is a route this binary registers. Paths use the router's `:parameter` names. Optional segments are separate rows, not brackets. Authentication is a user access token (`Authorization: Bearer`) unless the row says otherwise.

`GET /metrics` with no `METRICS_TOKEN` configured answers an empty `404` on purpose. The route is still registered.

## Endpoint reference table

| Method | Path | Auth | Notes |
|---|---|---|---|
| `GET` | `/health` | no | Plain text `ok`. |
| `GET` | `/_matrix/client/versions` | no | Supported spec versions. |
| `GET` | `/.well-known/matrix/server` | no | Federation delegation, port 443. |
| `GET` | `/.well-known/matrix/client` | no | `m.homeserver.base_url`. |
| `GET` | `/_matrix/key/v2/server` | no | This server's signing keys. |
| `GET` | `/metrics` | metrics token | Empty 404 when `METRICS_TOKEN` is unset. |
| `POST` | `/_matrix/client/v3/register` | no | Rate-limited. Production mode is `disabled`. |
| `GET` | `/_matrix/client/v3/login` | no | Lists flows, including `m.login.sso` when OIDC is configured. |
| `POST` | `/_matrix/client/v3/login` | no | `m.login.password` or `m.login.token`. Rate-limited. |
| `GET` | `/_matrix/client/v3/login/sso/redirect` | no | Only when exactly one tenant is active. Otherwise `M_MISSING_PARAM`. Rate-limited. |
| `GET` | `/_matrix/client/v3/login/sso/redirect/:idp_id` | no | Tenant slug or brokered IdP id. Rate-limited. |
| `GET` | `/_matrix/client/v3/login/sso/callback` | no | OIDC redirect URI. Not called by the embedded client directly. |
| `POST` | `/_matrix/client/v3/logout` | access token | Revokes the token and deletes the device. May return `sso_logout_url`. |
| `POST` | `/_matrix/client/v3/logout/all` | access token | Revokes every device. |
| `POST` | `/_matrix/client/v3/account/deactivate` | access token | Deactivates the account. |
| `GET` | `/_matrix/client/v3/sync` | access token | Initial sync, or long-poll. Timeout clamped to 30000 ms. |
| `POST` | `/_matrix/client/v3/createRoom` | access token | `name`, `topic`, `preset`. Private rooms are restricted to the tenant Space. |
| `POST` | `/_matrix/client/v3/rooms/:room_id/join` | access token | Local join, or federation join when the room is remote. |
| `POST` | `/_matrix/client/v3/join/:room_id_or_alias` | access token | Join by room id or alias. |
| `POST` | `/_matrix/client/v3/rooms/:room_id/leave` | access token | |
| `POST` | `/_matrix/client/v3/rooms/:room_id/invite` | access token | Body `user_id`. |
| `GET` | `/_matrix/client/v3/rooms/:room_id/members` | access token | |
| `GET` | `/_matrix/client/v3/rooms/:room_id/joined_members` | access token | |
| `GET` | `/_matrix/client/v3/rooms/:room_id/state` | access token | Full state. |
| `GET` | `/_matrix/client/v3/rooms/:room_id/state/:event_type` | access token | Empty state key. |
| `PUT` | `/_matrix/client/v3/rooms/:room_id/state/:event_type` | access token | Empty state key. |
| `GET` | `/_matrix/client/v3/rooms/:room_id/state/:event_type/` | access token | Same handler as the no-slash empty state key. |
| `PUT` | `/_matrix/client/v3/rooms/:room_id/state/:event_type/` | access token | Same handler as the no-slash empty state key. |
| `GET` | `/_matrix/client/v3/rooms/:room_id/state/:event_type/:state_key` | access token | |
| `PUT` | `/_matrix/client/v3/rooms/:room_id/state/:event_type/:state_key` | access token | |
| `PUT` | `/_matrix/client/v3/rooms/:room_id/send/:event_type/:txn_id` | access token | Idempotent per device and `txn_id`. |
| `GET` | `/_matrix/client/v3/rooms/:room_id/messages` | access token | `from`, `to`, `dir` (`b` default), `limit` (max 100). |
| `GET` | `/_matrix/client/v3/rooms/:room_id/context/:event_id` | access token | Permalink / jump-to-event. |
| `GET` | `/_matrix/client/v1/rooms/:room_id/relations/:event_id` | access token | All relations. `v1`, not `v3`. |
| `GET` | `/_matrix/client/v1/rooms/:room_id/relations/:event_id/:rel_type` | access token | |
| `GET` | `/_matrix/client/v1/rooms/:room_id/relations/:event_id/:rel_type/:event_type` | access token | |
| `GET` | `/_matrix/client/v1/register/m.login.registration_token/validity` | no | Rate-limited probe. |
| `GET` | `/_matrix/client/v3/voip/turnServer` | access token | TURN credentials. Anonymous issuance is refused. |
| `GET` | `/_matrix/client/v3/account/whoami` | access token | |
| `GET` | `/_matrix/client/v3/capabilities` | access token | |
| `GET` | `/_matrix/client/v3/pushrules/` | access token | Read-only. The trailing slash is the registered path. |
| `GET` | `/_matrix/client/v3/joined_rooms` | access token | |
| `POST` | `/_matrix/client/v3/user/:user_id/filter` | access token | Stored, but `/sync` does not apply filter ids. |
| `GET` | `/_matrix/client/v3/user/:user_id/filter/:filter_id` | access token | |
| `GET` | `/_matrix/client/v3/user/:user_id/account_data/:event_type` | access token | Includes `m.direct`. |
| `PUT` | `/_matrix/client/v3/user/:user_id/account_data/:event_type` | access token | |
| `GET` | `/_matrix/client/v3/user/:user_id/rooms/:room_id/account_data/:event_type` | access token | |
| `PUT` | `/_matrix/client/v3/user/:user_id/rooms/:room_id/account_data/:event_type` | access token | |
| `GET` | `/_matrix/client/v3/profile/:user_id` | no | Unauthenticated. A user id on another server is `M_FORBIDDEN`; federation profile queries are not supported. |
| `GET` | `/_matrix/client/v3/profile/:user_id/displayname` | no | Unauthenticated read. |
| `PUT` | `/_matrix/client/v3/profile/:user_id/displayname` | access token | |
| `GET` | `/_matrix/client/v3/profile/:user_id/avatar_url` | no | Unauthenticated read. |
| `PUT` | `/_matrix/client/v3/profile/:user_id/avatar_url` | access token | |
| `GET` | `/_matrix/client/v3/devices` | access token | |
| `GET` | `/_matrix/client/v3/devices/:device_id` | access token | No DELETE route. |
| `PUT` | `/_matrix/client/v3/devices/:device_id` | access token | Rename. |
| `POST` | `/_matrix/client/v3/rooms/:room_id/kick` | access token | |
| `POST` | `/_matrix/client/v3/rooms/:room_id/ban` | access token | |
| `POST` | `/_matrix/client/v3/rooms/:room_id/unban` | access token | |
| `POST` | `/_matrix/client/v3/rooms/:room_id/forget` | access token | |
| `POST` | `/_matrix/client/v3/rooms/:room_id/report/:event_id` | access token | |
| `PUT` | `/_matrix/client/v3/rooms/:room_id/redact/:event_id/:txn_id` | access token | |
| `PUT` | `/_matrix/client/v3/rooms/:room_id/typing/:user_id` | access token | Ephemeral, not a timeline event. |
| `POST` | `/_matrix/client/v3/rooms/:room_id/receipt/:receipt_type/:event_id` | access token | |
| `POST` | `/_matrix/client/v3/rooms/:room_id/read_markers` | access token | |
| `GET` | `/_matrix/client/v3/presence/:user_id/status` | access token | |
| `PUT` | `/_matrix/client/v3/presence/:user_id/status` | access token | |
| `GET` | `/_matrix/client/v3/rooms/:room_id/aliases` | access token | |
| `GET` | `/_matrix/client/v3/directory/room/:room_alias` | no | Alias lookup. Spec allows this unauthenticated. |
| `PUT` | `/_matrix/client/v3/directory/room/:room_alias` | access token | |
| `DELETE` | `/_matrix/client/v3/directory/room/:room_alias` | access token | |
| `GET` | `/_matrix/client/v3/publicRooms` | access token | Authenticated even though the spec marks it optional. |
| `POST` | `/_matrix/client/v3/publicRooms` | access token | |
| `GET` | `/_matrix/client/v3/directory/list/room/:room_id` | access token | |
| `PUT` | `/_matrix/client/v3/directory/list/room/:room_id` | access token | |
| `POST` | `/_matrix/client/v3/user_directory/search` | access token | Results limited to users who share a room with the caller. |
| `GET` | `/_matrix/client/v3/pushers` | access token | |
| `POST` | `/_matrix/client/v3/pushers/set` | access token | Gateway URL must be on the operator allow-list. |
| `POST` | `/_matrix/client/v3/keys/upload` | access token | Device and one-time keys. |
| `POST` | `/_matrix/client/v3/keys/query` | access token | Includes cross-signing keys when present. |
| `POST` | `/_matrix/client/v3/keys/claim` | access token | |
| `POST` | `/_matrix/client/v3/keys/device_signing/upload` | access token | First upload is not UIAA-gated. |
| `POST` | `/_matrix/client/v3/keys/signatures/upload` | access token | |
| `PUT` | `/_matrix/client/v3/sendToDevice/:event_type/:txn_id` | access token | |
| `POST` | `/_matrix/client/v3/room_keys/version` | access token | Create a backup version. |
| `GET` | `/_matrix/client/v3/room_keys/version` | access token | Latest version. |
| `GET` | `/_matrix/client/v3/room_keys/version/:version` | access token | |
| `PUT` | `/_matrix/client/v3/room_keys/version/:version` | access token | |
| `DELETE` | `/_matrix/client/v3/room_keys/version/:version` | access token | |
| `PUT` | `/_matrix/client/v3/room_keys/keys` | access token | |
| `GET` | `/_matrix/client/v3/room_keys/keys` | access token | |
| `DELETE` | `/_matrix/client/v3/room_keys/keys` | access token | |
| `PUT` | `/_matrix/client/v3/room_keys/keys/:room_id` | access token | |
| `GET` | `/_matrix/client/v3/room_keys/keys/:room_id` | access token | |
| `DELETE` | `/_matrix/client/v3/room_keys/keys/:room_id` | access token | |
| `PUT` | `/_matrix/client/v3/room_keys/keys/:room_id/:session_id` | access token | |
| `GET` | `/_matrix/client/v3/room_keys/keys/:room_id/:session_id` | access token | |
| `DELETE` | `/_matrix/client/v3/room_keys/keys/:room_id/:session_id` | access token | |
| `GET` | `/_matrix/media/v3/config` | no | `m.upload.size`. |
| `POST` | `/_matrix/media/v3/upload` | access token | Returns `mxc://` content URI. |
| `POST` | `/_matrix/media/v1/create` | access token | Reserve an id, then PUT the bytes. |
| `PUT` | `/_matrix/media/v3/upload/:server_name/:media_id` | access token | |
| `GET` | `/_matrix/media/v3/download/:server_name/:media_id` | no | Unauthenticated through v1.10. The id is the secret. |
| `GET` | `/_matrix/media/v3/download/:server_name/:media_id/:file_name` | no | |
| `GET` | `/_matrix/media/v3/thumbnail/:server_name/:media_id` | no | |
| `GET` | `/_matrix/media/v3/preview_url` | no | Always `M_UNRECOGNIZED`. URL previews are not supported. |
| `GET` | `/_matrix/client/v1/media/config` | access token | Authenticated media config. |
| `GET` | `/_matrix/client/v1/media/download/:server_name/:media_id` | access token | |
| `GET` | `/_matrix/client/v1/media/download/:server_name/:media_id/:file_name` | access token | |
| `GET` | `/_matrix/client/v1/media/thumbnail/:server_name/:media_id` | access token | |
| `GET` | `/_matrix/federation/v1/version` | no | Unauthenticated. Body is `{"server":{"name":"gdna-matrix","version":"<crate version>"}}`. Federation testers treat a 404 here as the server being down. |
| `PUT` | `/_matrix/federation/v1/send/:txn_id` | federation | Transaction push. |
| `GET` | `/_matrix/federation/v1/make_join/:room_id/:user_id` | federation | |
| `PUT` | `/_matrix/federation/v2/send_join/:room_id/:event_id` | federation | |
| `PUT` | `/_matrix/federation/v1/invite/:room_id/:event_id` | federation | |
| `PUT` | `/_matrix/federation/v2/invite/:room_id/:event_id` | federation | |
| `GET` | `/_matrix/federation/v1/make_leave/:room_id/:user_id` | federation | |
| `PUT` | `/_matrix/federation/v2/send_leave/:room_id/:event_id` | federation | |
| `GET` | `/_matrix/federation/v1/state/:room_id` | federation | |
| `GET` | `/_matrix/federation/v1/backfill/:room_id` | federation | |
| `POST` | `/_matrix/federation/v1/user/keys/query` | federation | |
| `POST` | `/_matrix/federation/v1/user/keys/claim` | federation | |


# Rate limits and errors

## Rate limits

Limits apply to unauthenticated, credential-adjacent routes. They are **per process**. Each of these deployments runs one task and does not autoscale, so the numbers below are the deployment's limits. Set `RATE_LIMIT_DISABLED=1` only to turn them off; the server binary enables them.

The limiter keys on the TCP peer address. `X-Forwarded-For` is ignored unless `TRUST_FORWARDED_FOR=1`. These deployments set that, because the load balancer is the only path to the task and overwrites the header. A client that can reach the process directly must not be trusted to set it.

| Route family | Burst | Sustained rate |
|---|---|---|
| `POST /_matrix/client/v3/login` | 5 | 1 per 12 seconds |
| `POST /_matrix/client/v3/register` | 3 | 1 per 30 seconds |
| `GET /_matrix/client/v1/register/m.login.registration_token/validity` | 10 | 1 per 6 seconds |
| `GET /_matrix/client/v3/login/sso/redirect` and `.../redirect/{idpId}` | 10 | 1 per 3 seconds |

Over the limit the response is `429`:

```json
{"errcode":"M_LIMIT_EXCEEDED","error":"Too many requests","retry_after_ms":1000}
```

`retry_after_ms` is at least 1. Wait that long. The shared load balancer also has a WAF rate-based rule of 2000 requests per IP on paths under `/_matrix/client/`, evaluated on the WAF's default window, which blocks rather than returning `M_LIMIT_EXCEEDED`. `/docs` is not under that path.

`/sync` is not rate-limited by this process. Its timeout is clamped to 30 seconds so one client cannot hold a connection longer than that.

## Error shape

Matrix errors are JSON: `{"errcode":"M_...","error":"..."}`. Handlers that return a plain `AppError` (no specific errcode) are wrapped as **`M_UNKNOWN`** with a fixed message:

| Status | `error` string | When |
|---|---|---|
| 400 | the handler's message | `AppError::BadRequest` |
| 401 | `unauthorized` | missing or unknown access token |
| 403 | `forbidden` | password login failed, or a forbidden action that did not set its own errcode |
| 404 | `not found` | `AppError::NotFound` |
| 409 | the handler's message | conflict |
| 500 | `internal server error` | database or internal failure; the detail is logged, not returned |
| 503 | `temporarily unable to verify; retry` | a dependency such as the OIDC provider was unreachable |

A client that only treats `M_MISSING_TOKEN` / `M_UNKNOWN_TOKEN` as "session expired" will not notice a dead access token. Branch on **HTTP 401**.

Errcodes this server sets explicitly, and that an embedded client will actually see:

| Code | When |
|---|---|
| `M_LIMIT_EXCEEDED` | rate limit, or too many unfinished media reservations |
| `M_MISSING_PARAM` | a required SSO or login field was absent; bare SSO redirect when more than one tenant is active |
| `M_INVALID_PARAM` | malformed id, bad `redirectUrl`, bad profile or state value |
| `M_FORBIDDEN` | SSO token rejected, cross-signing replacement refused, profile change for someone else |
| `M_NOT_FOUND` | unknown IdP, unknown alias, SSO not configured, unknown backup version |
| `M_USER_DEACTIVATED` | password or SSO login for a deactivated account, after the credential checked out |
| `M_USER_IN_USE` | registration localpart taken |
| `M_UNAUTHORIZED` | UIAA stage failed (registration) |
| `M_UNRECOGNIZED` | URL preview, content repository disabled, unknown UIAA type, push gateway not configured |
| `M_EXCLUSIVE` | application-service `user_id` outside the bot's namespace |
| `M_TOO_LARGE` | upload over the size cap |
| `M_NOT_JSON` / `M_BAD_JSON` | account-data or filter body |
| `M_ROOM_IN_USE` | alias already points at a room |
| `M_BAD_STATE` | moderation or membership rejected by room state |
| `M_WRONG_ROOM_KEYS_VERSION` | key backup version mismatch |
| `M_CANNOT_OVERWRITE_MEDIA` | upload onto an id that already has bytes |
| `M_NOT_YET_UPLOADED` | download of a reserved id that has no bytes yet |
| `M_UNABLE_TO_AUTHORISE_JOIN` | federation join the local server will not authorise |
| `M_INCOMPATIBLE_ROOM_VERSION` | federation invite for a room version this server refuses |

Password login uses the same `403` `M_UNKNOWN` for "no such user" and "wrong password". `M_USER_DEACTIVATED` is returned only after the password verifies.


# Federation

`matrix.gdna.ai` is one homeserver. `matrix.gdna.io` and `matrix.gdna.ai` are two deployments of this same binary, each with its own server name, database, and signing key. They are not two tasks of one server.

The code default is federation **disabled** (`FEDERATION_MODE` unset, `disabled`, or unrecognised). Nothing is sent or accepted until an operator opts in. These two deployments set `FEDERATION_MODE=allowlist`:

- `matrix.gdna.io` allows only `matrix.gdna.ai`
- `matrix.gdna.ai` allows only `matrix.gdna.io`

Any other server name is refused, inbound and outbound, after the caller has authenticated. The policy does not unsend events that were already shared. An allow-list that is empty allows nobody.

Clients do not call the federation endpoints. They join, invite, and leave through the Client-Server routes, and this process speaks Server-Server to the peer when the room has a member there. Joining a room that lives on the other homeserver uses `POST /_matrix/client/v3/join/{roomIdOrAlias}` or `POST /rooms/{roomId}/join`; this server then runs the federation join against the peer.

Server-Server routes this build implements are listed in the endpoint reference. `GET /_matrix/federation/v1/version` is the exception: it is unauthenticated and returns `{"server":{"name":"gdna-matrix","version":"<crate version>"}}`. A 404 on that path is what federation testers report as the server being down. Every other federation route requires a signed `Authorization: X-Matrix` header, not a user access token. Federation listens on the well-known port from `GET /.well-known/matrix/server` (`matrix.gdna.ai:443`) and, on this deployment, also on `8448`.

Not supported on the federation side: `multipart/mixed` media (`/_matrix/federation/v1/media/...`), `/_matrix/federation/v1/query` directory queries beyond what the client directory already does locally, and open federation with the public Matrix network. A room on `matrix.org` cannot be joined from these servers.


# Quickstart

Replace nothing in the URLs below: this page already substituted `https://matrix.gdna.ai` and `matrix.gdna.ai`. The examples assume an embedded web client whose origin is on the SSO redirect allow-list (see Authentication).

## curl

Discover, then start SSO. `{idpId}` comes from `GET /login` (`identity_providers[].id`), not from a guess. The callback below must be URL-encoded and allow-listed.

```bash
BASE=https://matrix.gdna.ai
curl -sS "$BASE/.well-known/matrix/client"
curl -sS "$BASE/_matrix/client/versions"
curl -sS "$BASE/_matrix/client/v3/login"
```

Open this in a browser (the `redirectUrl` is your app, not the homeserver):

```text
https://matrix.gdna.ai/_matrix/client/v3/login/sso/redirect/{idpId}?redirectUrl=https%3A%2F%2Fchat.gdna.io%2Fcallback
```

The browser returns to your callback with `?loginToken=`. Exchange it within 60 seconds. The response has `access_token`, `user_id`, and `device_id`, and no `refresh_token`.

```bash
curl -sS -X POST "$BASE/_matrix/client/v3/login" \
  -H 'content-type: application/json' \
  -d '{"type":"m.login.token","token":"LOGIN_TOKEN"}'
```

Initial sync (does not long-poll), create a room, send a message, then wait for the next event:

```bash
TOKEN=ACCESS_TOKEN
curl -sS "$BASE/_matrix/client/v3/sync" -H "authorization: Bearer $TOKEN"

ROOM=$(curl -sS -X POST "$BASE/_matrix/client/v3/createRoom" \
  -H "authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' \
  -d '{"name":"embedded"}' | jq -r .room_id)

TXN=$(uuidgen | tr '[:upper:]' '[:lower:]')
curl -sS -X PUT "$BASE/_matrix/client/v3/rooms/$ROOM/send/m.room.message/$TXN" \
  -H "authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' \
  -d '{"msgtype":"m.text","body":"hello"}'

NEXT=$(curl -sS "$BASE/_matrix/client/v3/sync" \
  -H "authorization: Bearer $TOKEN" | jq -r .next_batch)
curl -sS -G "$BASE/_matrix/client/v3/sync" \
  -H "authorization: Bearer $TOKEN" \
  --data-urlencode "since=$NEXT" \
  --data-urlencode "timeout=30000"
```

`timeout` larger than 30000 is clamped. A `401` with `errcode` `M_UNKNOWN` means the access token is missing or dead. Log in with SSO again. Refresh is not supported.

Logout revokes the token and deletes the device:

```bash
curl -sS -X POST "$BASE/_matrix/client/v3/logout" \
  -H "authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' \
  -d '{}'
```

## matrix-js-sdk (TypeScript)

```typescript
import * as sdk from "matrix-js-sdk";

const baseUrl = "https://matrix.gdna.ai";

// 1. SSO. idpId is identity_providers[].id from GET /_matrix/client/v3/login.
//    redirectUrl must be on this server's allow-list.
const idpId = "default";
const redirectUrl = window.location.origin + "/callback";
window.location.assign(
  `${baseUrl}/_matrix/client/v3/login/sso/redirect/${encodeURIComponent(idpId)}` +
    `?redirectUrl=${encodeURIComponent(redirectUrl)}`,
);

// 2. On the callback page, exchange loginToken. There is no refresh token.
const loginToken = new URLSearchParams(window.location.search).get("loginToken");
if (!loginToken) throw new Error("missing loginToken");

const session = await sdk.createClient({ baseUrl }).login("m.login.token", {
  token: loginToken,
  initial_device_display_name: "embedded",
});

const client = sdk.createClient({
  baseUrl,
  accessToken: session.access_token,
  userId: session.user_id,
  deviceId: session.device_id,
});

// 3. Initial sync, then live events. toStartOfTimeline is pagination, not new traffic.
client.on(
  "Room.timeline",
  (event: sdk.MatrixEvent, room: sdk.Room | undefined, toStartOfTimeline: boolean) => {
    if (toStartOfTimeline || !room) return;
    if (event.getType() !== "m.room.message") return;
    if (event.getContent().msgtype !== "m.text") return;
    console.log(room.roomId, event.getContent().body);
  },
);
await client.startClient({ initialSyncLimit: 20 });

// 4. Send. txnId is generated inside sendTextMessage.
const roomId = (await client.createRoom({ name: "embedded" })).room_id;
await client.sendTextMessage(roomId, "hello from an embedded client");
```

`client.createRoom` calls `POST /_matrix/client/v3/createRoom`. `sendTextMessage` calls `PUT /rooms/{roomId}/send/m.room.message/{txnId}`. `startClient` drives `GET /sync`. When a request fails with HTTP 401, call the SSO redirect again; do not call a refresh API. This server does not implement one.

User ids in events are `@localpart:matrix.gdna.ai`.


# Not supported

These are absent from the router, or present only as an explicit refusal. Do not call them and retry.

- **Access-token refresh.** No `refresh_token` field on `POST /login`. No `/_matrix/client/v3/refresh`. No `m.login.refresh`. Tokens last 24 hours; run SSO again.
- **`DELETE /_matrix/client/v3/devices/{deviceId}`.** Logout deletes the calling device. Listing and renaming devices is supported.
- **Bearer token in the query string.** Only `Authorization: Bearer`.
- **Sliding sync**, `/sync/v4`, and MSC4186.
- **`/sync` filter ids.** Filter CRUD exists. `/sync` honours `since`, `timeout` (max 30000 ms), and `types` only.
- **Space hierarchy** (`GET /_matrix/client/v1/rooms/{roomId}/hierarchy`). Walk `m.space.child` / `m.space.parent` via `/state`.
- **URL previews.** `GET /_matrix/media/v3/preview_url` is registered and always returns `M_UNRECOGNIZED`.
- **Push-rule writes and server-side push-rule evaluation.** `GET /_matrix/client/v3/pushrules/` is read-only. There is no PUT.
- **Push notifications.** `GET` and `POST /_matrix/client/v3/pushers` exist, and the gateway URL is checked against `PUSH_GATEWAY_ALLOWLIST`. These deployments set that list empty, so setting a pusher is refused (`M_UNRECOGNIZED` when push is not configured).
- **Registration** on the deployed homeservers (`REGISTRATION_MODE=disabled`). `POST /register` exists in the binary and is closed in production.
- **Knock, third-party invites, room upgrades, search** (`/search`), **notifications** (`/notifications`).
- **`trusted_private_chat` auto-power-levels** on `createRoom`.
- **Federation media** `multipart/mixed` under `/_matrix/federation/v1/media/`.
- **Open federation.** These deployments federate only with each other. See Federation.
- **Query-parameter access tokens, guest access, and `m.login.token` refresh.** Guest registration is not implemented. `m.login.token` is only the SSO login-token exchange.
- **Application-service registration files.** Bots are managed through `/peregrine`, which is an operator UI gated by the SSO session cookie. It is not part of the client-server surface and is not documented as a public client API.
