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.

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. The agent index is /docs/llms.txt. No access token is required for any /docs page. /docs.md redirects to /docs/index.md.

Contents

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:

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

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

{"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:

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:
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.

QueryMeaning
sinceOmit for the initial sync. Thereafter send the previous response's next_batch.
timeoutMilliseconds to long-poll when an incremental sync has nothing new. Clamped to 30000. 0 or omitted returns immediately.
typesOptional 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:

Sending a message:

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):

Authenticated equivalents, which require a bearer token:

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:

Cross-signing:

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):

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:

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

MethodPathAuthNotes
GET/healthnoPlain text ok.
GET/_matrix/client/versionsnoSupported spec versions.
GET/.well-known/matrix/servernoFederation delegation, port 443.
GET/.well-known/matrix/clientnom.homeserver.base_url.
GET/_matrix/key/v2/servernoThis server's signing keys.
GET/metricsmetrics tokenEmpty 404 when METRICS_TOKEN is unset.
POST/_matrix/client/v3/registernoRate-limited. Production mode is disabled.
GET/_matrix/client/v3/loginnoLists flows, including m.login.sso when OIDC is configured.
POST/_matrix/client/v3/loginnom.login.password or m.login.token. Rate-limited.
GET/_matrix/client/v3/login/sso/redirectnoOnly when exactly one tenant is active. Otherwise M_MISSING_PARAM. Rate-limited.
GET/_matrix/client/v3/login/sso/redirect/:idp_idnoTenant slug or brokered IdP id. Rate-limited.
GET/_matrix/client/v3/login/sso/callbacknoOIDC redirect URI. Not called by the embedded client directly.
POST/_matrix/client/v3/logoutaccess tokenRevokes the token and deletes the device. May return sso_logout_url.
POST/_matrix/client/v3/logout/allaccess tokenRevokes every device.
POST/_matrix/client/v3/account/deactivateaccess tokenDeactivates the account.
GET/_matrix/client/v3/syncaccess tokenInitial sync, or long-poll. Timeout clamped to 30000 ms.
POST/_matrix/client/v3/createRoomaccess tokenname, topic, preset. Private rooms are restricted to the tenant Space.
POST/_matrix/client/v3/rooms/:room_id/joinaccess tokenLocal join, or federation join when the room is remote.
POST/_matrix/client/v3/join/:room_id_or_aliasaccess tokenJoin by room id or alias.
POST/_matrix/client/v3/rooms/:room_id/leaveaccess token
POST/_matrix/client/v3/rooms/:room_id/inviteaccess tokenBody user_id.
GET/_matrix/client/v3/rooms/:room_id/membersaccess token
GET/_matrix/client/v3/rooms/:room_id/joined_membersaccess token
GET/_matrix/client/v3/rooms/:room_id/stateaccess tokenFull state.
GET/_matrix/client/v3/rooms/:room_id/state/:event_typeaccess tokenEmpty state key.
PUT/_matrix/client/v3/rooms/:room_id/state/:event_typeaccess tokenEmpty state key.
GET/_matrix/client/v3/rooms/:room_id/state/:event_type/access tokenSame handler as the no-slash empty state key.
PUT/_matrix/client/v3/rooms/:room_id/state/:event_type/access tokenSame handler as the no-slash empty state key.
GET/_matrix/client/v3/rooms/:room_id/state/:event_type/:state_keyaccess token
PUT/_matrix/client/v3/rooms/:room_id/state/:event_type/:state_keyaccess token
PUT/_matrix/client/v3/rooms/:room_id/send/:event_type/:txn_idaccess tokenIdempotent per device and txn_id.
GET/_matrix/client/v3/rooms/:room_id/messagesaccess tokenfrom, to, dir (b default), limit (max 100).
GET/_matrix/client/v3/rooms/:room_id/context/:event_idaccess tokenPermalink / jump-to-event.
GET/_matrix/client/v1/rooms/:room_id/relations/:event_idaccess tokenAll relations. v1, not v3.
GET/_matrix/client/v1/rooms/:room_id/relations/:event_id/:rel_typeaccess token
GET/_matrix/client/v1/rooms/:room_id/relations/:event_id/:rel_type/:event_typeaccess token
GET/_matrix/client/v1/register/m.login.registration_token/validitynoRate-limited probe.
GET/_matrix/client/v3/voip/turnServeraccess tokenTURN credentials. Anonymous issuance is refused.
GET/_matrix/client/v3/account/whoamiaccess token
GET/_matrix/client/v3/capabilitiesaccess token
GET/_matrix/client/v3/pushrules/access tokenRead-only. The trailing slash is the registered path.
GET/_matrix/client/v3/joined_roomsaccess token
POST/_matrix/client/v3/user/:user_id/filteraccess tokenStored, but /sync does not apply filter ids.
GET/_matrix/client/v3/user/:user_id/filter/:filter_idaccess token
GET/_matrix/client/v3/user/:user_id/account_data/:event_typeaccess tokenIncludes m.direct.
PUT/_matrix/client/v3/user/:user_id/account_data/:event_typeaccess token
GET/_matrix/client/v3/user/:user_id/rooms/:room_id/account_data/:event_typeaccess token
PUT/_matrix/client/v3/user/:user_id/rooms/:room_id/account_data/:event_typeaccess token
GET/_matrix/client/v3/profile/:user_idnoUnauthenticated. A user id on another server is M_FORBIDDEN; federation profile queries are not supported.
GET/_matrix/client/v3/profile/:user_id/displaynamenoUnauthenticated read.
PUT/_matrix/client/v3/profile/:user_id/displaynameaccess token
GET/_matrix/client/v3/profile/:user_id/avatar_urlnoUnauthenticated read.
PUT/_matrix/client/v3/profile/:user_id/avatar_urlaccess token
GET/_matrix/client/v3/devicesaccess token
GET/_matrix/client/v3/devices/:device_idaccess tokenNo DELETE route.
PUT/_matrix/client/v3/devices/:device_idaccess tokenRename.
POST/_matrix/client/v3/rooms/:room_id/kickaccess token
POST/_matrix/client/v3/rooms/:room_id/banaccess token
POST/_matrix/client/v3/rooms/:room_id/unbanaccess token
POST/_matrix/client/v3/rooms/:room_id/forgetaccess token
POST/_matrix/client/v3/rooms/:room_id/report/:event_idaccess token
PUT/_matrix/client/v3/rooms/:room_id/redact/:event_id/:txn_idaccess token
PUT/_matrix/client/v3/rooms/:room_id/typing/:user_idaccess tokenEphemeral, not a timeline event.
POST/_matrix/client/v3/rooms/:room_id/receipt/:receipt_type/:event_idaccess token
POST/_matrix/client/v3/rooms/:room_id/read_markersaccess token
GET/_matrix/client/v3/presence/:user_id/statusaccess token
PUT/_matrix/client/v3/presence/:user_id/statusaccess token
GET/_matrix/client/v3/rooms/:room_id/aliasesaccess token
GET/_matrix/client/v3/directory/room/:room_aliasnoAlias lookup. Spec allows this unauthenticated.
PUT/_matrix/client/v3/directory/room/:room_aliasaccess token
DELETE/_matrix/client/v3/directory/room/:room_aliasaccess token
GET/_matrix/client/v3/publicRoomsaccess tokenAuthenticated even though the spec marks it optional.
POST/_matrix/client/v3/publicRoomsaccess token
GET/_matrix/client/v3/directory/list/room/:room_idaccess token
PUT/_matrix/client/v3/directory/list/room/:room_idaccess token
POST/_matrix/client/v3/user_directory/searchaccess tokenResults limited to users who share a room with the caller.
GET/_matrix/client/v3/pushersaccess token
POST/_matrix/client/v3/pushers/setaccess tokenGateway URL must be on the operator allow-list.
POST/_matrix/client/v3/keys/uploadaccess tokenDevice and one-time keys.
POST/_matrix/client/v3/keys/queryaccess tokenIncludes cross-signing keys when present.
POST/_matrix/client/v3/keys/claimaccess token
POST/_matrix/client/v3/keys/device_signing/uploadaccess tokenFirst upload is not UIAA-gated.
POST/_matrix/client/v3/keys/signatures/uploadaccess token
PUT/_matrix/client/v3/sendToDevice/:event_type/:txn_idaccess token
POST/_matrix/client/v3/room_keys/versionaccess tokenCreate a backup version.
GET/_matrix/client/v3/room_keys/versionaccess tokenLatest version.
GET/_matrix/client/v3/room_keys/version/:versionaccess token
PUT/_matrix/client/v3/room_keys/version/:versionaccess token
DELETE/_matrix/client/v3/room_keys/version/:versionaccess token
PUT/_matrix/client/v3/room_keys/keysaccess token
GET/_matrix/client/v3/room_keys/keysaccess token
DELETE/_matrix/client/v3/room_keys/keysaccess token
PUT/_matrix/client/v3/room_keys/keys/:room_idaccess token
GET/_matrix/client/v3/room_keys/keys/:room_idaccess token
DELETE/_matrix/client/v3/room_keys/keys/:room_idaccess token
PUT/_matrix/client/v3/room_keys/keys/:room_id/:session_idaccess token
GET/_matrix/client/v3/room_keys/keys/:room_id/:session_idaccess token
DELETE/_matrix/client/v3/room_keys/keys/:room_id/:session_idaccess token
GET/_matrix/media/v3/confignom.upload.size.
POST/_matrix/media/v3/uploadaccess tokenReturns mxc:// content URI.
POST/_matrix/media/v1/createaccess tokenReserve an id, then PUT the bytes.
PUT/_matrix/media/v3/upload/:server_name/:media_idaccess token
GET/_matrix/media/v3/download/:server_name/:media_idnoUnauthenticated through v1.10. The id is the secret.
GET/_matrix/media/v3/download/:server_name/:media_id/:file_nameno
GET/_matrix/media/v3/thumbnail/:server_name/:media_idno
GET/_matrix/media/v3/preview_urlnoAlways M_UNRECOGNIZED. URL previews are not supported.
GET/_matrix/client/v1/media/configaccess tokenAuthenticated media config.
GET/_matrix/client/v1/media/download/:server_name/:media_idaccess token
GET/_matrix/client/v1/media/download/:server_name/:media_id/:file_nameaccess token
GET/_matrix/client/v1/media/thumbnail/:server_name/:media_idaccess token
GET/_matrix/federation/v1/versionnoUnauthenticated. 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_idfederationTransaction push.
GET/_matrix/federation/v1/make_join/:room_id/:user_idfederation
PUT/_matrix/federation/v2/send_join/:room_id/:event_idfederation
PUT/_matrix/federation/v1/invite/:room_id/:event_idfederation
PUT/_matrix/federation/v2/invite/:room_id/:event_idfederation
GET/_matrix/federation/v1/make_leave/:room_id/:user_idfederation
PUT/_matrix/federation/v2/send_leave/:room_id/:event_idfederation
GET/_matrix/federation/v1/state/:room_idfederation
GET/_matrix/federation/v1/backfill/:room_idfederation
POST/_matrix/federation/v1/user/keys/queryfederation
POST/_matrix/federation/v1/user/keys/claimfederation

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 familyBurstSustained rate
POST /_matrix/client/v3/login51 per 12 seconds
POST /_matrix/client/v3/register31 per 30 seconds
GET /_matrix/client/v1/register/m.login.registration_token/validity101 per 6 seconds
GET /_matrix/client/v3/login/sso/redirect and .../redirect/{idpId}101 per 3 seconds

Over the limit the response is 429:

{"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:

Statuserror stringWhen
400the handler's messageAppError::BadRequest
401unauthorizedmissing or unknown access token
403forbiddenpassword login failed, or a forbidden action that did not set its own errcode
404not foundAppError::NotFound
409the handler's messageconflict
500internal server errordatabase or internal failure; the detail is logged, not returned
503temporarily unable to verify; retrya 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:

CodeWhen
M_LIMIT_EXCEEDEDrate limit, or too many unfinished media reservations
M_MISSING_PARAMa required SSO or login field was absent; bare SSO redirect when more than one tenant is active
M_INVALID_PARAMmalformed id, bad redirectUrl, bad profile or state value
M_FORBIDDENSSO token rejected, cross-signing replacement refused, profile change for someone else
M_NOT_FOUNDunknown IdP, unknown alias, SSO not configured, unknown backup version
M_USER_DEACTIVATEDpassword or SSO login for a deactivated account, after the credential checked out
M_USER_IN_USEregistration localpart taken
M_UNAUTHORIZEDUIAA stage failed (registration)
M_UNRECOGNIZEDURL preview, content repository disabled, unknown UIAA type, push gateway not configured
M_EXCLUSIVEapplication-service user_id outside the bot's namespace
M_TOO_LARGEupload over the size cap
M_NOT_JSON / M_BAD_JSONaccount-data or filter body
M_ROOM_IN_USEalias already points at a room
M_BAD_STATEmoderation or membership rejected by room state
M_WRONG_ROOM_KEYS_VERSIONkey backup version mismatch
M_CANNOT_OVERWRITE_MEDIAupload onto an id that already has bytes
M_NOT_YET_UPLOADEDdownload of a reserved id that has no bytes yet
M_UNABLE_TO_AUTHORISE_JOINfederation join the local server will not authorise
M_INCOMPATIBLE_ROOM_VERSIONfederation 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:

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.

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):

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.

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:

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:

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

matrix-js-sdk (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.