{
  "openapi": "3.1.0",
  "info": {
    "title": "Chatbox Backend API",
    "version": "1.0.0",
    "description": "Realtime communications backend: durable meeting chat, attachments, presence/receipts, SFU-based WebRTC. Public endpoints use short-lived room-scoped Ed25519 JWTs; /internal/v1/* uses HMAC-SHA256 request signing (X-Chatbox-* headers). The two WebSocket endpoints are specified separately in asyncapi.json, which references the wire shapes in ws-schemas.json.",
    "license": {
      "name": "Apache-2.0",
      "identifier": "Apache-2.0"
    }
  },
  "servers": [
    {
      "url": "https://api.ollacore.com",
      "description": "Ollacore hosted API"
    },
    {
      "url": "https://{host}",
      "description": "Self-hosted: the PUBLIC_HOST of your deployment",
      "variables": {
        "host": {
          "default": "api.example.com"
        }
      }
    }
  ],
  "tags": [
    {
      "name": "rtc-experimental",
      "description": "Experimental READ-ONLY own-session observation. Default off; not stable or GA."
    },
    {
      "name": "account",
      "description": "Self-serve customer accounts: signup/login and API-key management (session bearer)."
    },
    {
      "name": "directory",
      "description": "Turnkey phone-OTP end-user identity + contacts (the customer resells this to their users)."
    },
    {
      "name": "server",
      "description": "Customer server API: tenant-pinned ak_* credential; reuses the room/member/token/message engine."
    },
    {
      "name": "webhooks",
      "description": "Customer webhook endpoints and delivery history (scope webhooks:manage)."
    },
    {
      "name": "messages",
      "description": "Durable room chat: history, search, posting, edits and reactions."
    },
    {
      "name": "receipts",
      "description": "Delivery and read cursors, and per-room unread counts."
    },
    {
      "name": "attachments",
      "description": "Presigned single-shot and multipart uploads, and downloads."
    },
    {
      "name": "push",
      "description": "Web Push subscription lifecycle and the VAPID public key."
    },
    {
      "name": "e2ee",
      "description": "Olm/Megolm device keys, one-time keys and to-device delivery."
    },
    {
      "name": "keypackages",
      "description": "MLS key packages: publish, count and consume."
    },
    {
      "name": "mls",
      "description": "MLS Delivery Service state beyond key packages: the room's latest GroupInfo and ratchet tree, published by the committer so a Welcome need not carry the tree and external joins have something to join from."
    },
    {
      "name": "rooms",
      "description": "Room-scoped reads available to session-token holders."
    },
    {
      "name": "internal",
      "description": "Server-to-server control plane, authenticated by HMAC request signing."
    },
    {
      "name": "ops",
      "description": "Unauthenticated liveness, readiness and Prometheus metrics."
    }
  ],
  "components": {
    "securitySchemes": {
      "customer_key": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ak_<env>_<id>.<secret> (tenant-pinned customer server key)"
      },
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT (EdDSA, room-scoped)"
      },
      "account_session": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "as_<token> (customer account session)"
      },
      "directory_session": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "du_<token> (directory end-user session)"
      },
      "hmac": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Chatbox-Signature",
        "description": "With X-Chatbox-Key-Id, X-Chatbox-Timestamp, X-Chatbox-Nonce. Signature = hex HMAC-SHA256 over METHOD\\nPATH_AND_QUERY\\nSHA256_HEX_BODY\\nTIMESTAMP\\nNONCE."
      }
    },
    "schemas": {
      "RtcSessionRequest": {
        "type": "object",
        "required": [
          "handle"
        ],
        "additionalProperties": false,
        "description": "Experimental read-only request. Exactly one handle field; duplicate JSON keys are rejected by the Rust parser (not expressible after JSON parsing in this schema). Entire body including whitespace is limited to 256 bytes. Arrays, missing/unknown fields, null/non-string handles and non-hyphenated UUIDs are rejected.",
        "properties": {
          "handle": {
            "type": "string",
            "format": "uuid",
            "minLength": 36,
            "maxLength": 36,
            "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
            "description": "Opaque UUID from the private session.handle event; hex may be either case. Not a credential, client_id or internal lease ID."
          }
        }
      },
      "RtcSessionResponse": {
        "type": "object",
        "required": [
          "client_id",
          "state"
        ],
        "description": "Experimental admission observation only; not ICE/DTLS connectivity or healthy/flowing media. The current response contains only client_id and state. Ignore unknown response fields.",
        "properties": {
          "client_id": {
            "type": "string",
            "pattern": "^[0-9]+$",
            "description": "The exact accepted signaling connection's u64 identifier as a decimal string, never a JSON number."
          },
          "state": {
            "type": "string",
            "enum": [
              "accepted"
            ],
            "description": "Admission observed on the owning media thread; not a media-health guarantee or atomic membership-removal fence."
          }
        }
      },
      "RtcSessionReadError": {
        "type": "object",
        "required": [
          "code",
          "message"
        ],
        "description": "Experimental 503/504 errors use the same code/message envelope as ApiError, with experiment-specific codes. Other handler failures use ApiError.",
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "unavailable",
              "deadline_exceeded"
            ]
          },
          "message": {
            "type": "string",
            "description": "Human-readable detail; do not parse."
          }
        }
      },
      "AccountSession": {
        "type": "object",
        "required": [
          "tenant_id",
          "session_token",
          "expires_at"
        ],
        "properties": {
          "tenant_id": {
            "type": "string",
            "description": "The account's auto-provisioned tenant."
          },
          "session_token": {
            "type": "string",
            "description": "Opaque bearer (as_...), shown once."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this session stops working: 30 days after it was issued. Log in again for a new one."
          }
        }
      },
      "CreateApiKeyResponse": {
        "type": "object",
        "description": "Operator-plane key creation (POST /internal/v1/api-keys). The account plane returns AccountCreateApiKeyResponse, which has no tenant_id.",
        "required": [
          "key_id",
          "api_key",
          "tenant_id",
          "environment",
          "scopes"
        ],
        "properties": {
          "key_id": {
            "type": "string",
            "description": "The key's public id (`ak_live_…` or `ak_test_…`). Use it to revoke the key; it is not a credential."
          },
          "api_key": {
            "type": "string",
            "description": "Full ak_<env>_<id>.<secret>, shown once."
          },
          "tenant_id": {
            "type": "string",
            "description": "The tenant the key acts on. A `test` key's data lives in its own namespace, `{tenant_id}::test`, and never mixes with live data."
          },
          "environment": {
            "type": "string",
            "enum": [
              "live",
              "test"
            ],
            "description": "`live` or `test`. A test key sees only test data."
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "rooms:read",
                "rooms:write",
                "members:write",
                "tokens:mint",
                "messages:write",
                "webhooks:manage",
                "usage:read",
                "rooms:delete",
                "data:erase"
              ]
            },
            "description": "What the key may do. A call outside these scopes is refused with 403 when it is made, not when the key is created."
          }
        }
      },
      "RegisteredPrincipal": {
        "type": "object",
        "required": [
          "principal_id",
          "kind"
        ],
        "properties": {
          "principal_id": {
            "type": "string",
            "description": "The principal id you registered, 1–128 characters."
          },
          "kind": {
            "type": "string",
            "enum": [
              "agent",
              "bot",
              "service",
              "system"
            ],
            "description": "What kind of non-human sender this is. Only a registered principal can be sent as from `POST /v1/server/rooms/{room_id}/messages`."
          }
        }
      },
      "UnreadResponse": {
        "type": "object",
        "required": [
          "unread_count",
          "last_read_seq",
          "latest_seq"
        ],
        "properties": {
          "unread_count": {
            "type": "integer",
            "format": "int64",
            "description": "messages after last_read_seq not sent by the caller"
          },
          "last_read_seq": {
            "type": "integer",
            "format": "int64",
            "description": "read cursor; advances monotonically and syncs across the devices of a principal"
          },
          "latest_seq": {
            "type": "integer",
            "format": "int64",
            "description": "The room's highest `event_seq` at the time of the request."
          }
        }
      },
      "ReactionRequest": {
        "type": "object",
        "required": [
          "emoji"
        ],
        "properties": {
          "emoji": {
            "type": "string",
            "maxLength": 32,
            "description": "The reaction: 1–8 characters and at most 32 bytes, usually a single emoji. A message holds a bounded number of distinct reactions."
          }
        }
      },
      "ReactionResult": {
        "type": "object",
        "required": [
          "changed"
        ],
        "properties": {
          "changed": {
            "type": "boolean",
            "description": "false when the call was a no-op, which makes retries safe"
          }
        }
      },
      "PushKeys": {
        "type": "object",
        "required": [
          "p256dh",
          "auth"
        ],
        "properties": {
          "p256dh": {
            "type": "string",
            "description": "The subscription's public encryption key, exactly as `PushSubscription.toJSON()` returns it (base64url)."
          },
          "auth": {
            "type": "string",
            "description": "The subscription's authentication secret, exactly as `PushSubscription.toJSON()` returns it (base64url)."
          }
        }
      },
      "PushSubscribeRequest": {
        "type": "object",
        "required": [
          "endpoint",
          "keys"
        ],
        "properties": {
          "endpoint": {
            "type": "string",
            "format": "uri",
            "description": "Push service endpoint from PushSubscription.toJSON(). Must be https with a public host: cleartext, loopback, private and link-local IP literals are rejected with 400. A hostname is resolved at each delivery; if it resolves to such an address the server does not connect and drops the subscription."
          },
          "keys": {
            "$ref": "#/components/schemas/PushKeys",
            "description": "The encryption keys from `PushSubscription.toJSON()`."
          },
          "user_agent": {
            "type": "string",
            "description": "Optional free-text label for the device, stored with the subscription."
          }
        }
      },
      "InitMultipartRequest": {
        "type": "object",
        "required": [
          "original_name",
          "declared_mime",
          "byte_size"
        ],
        "properties": {
          "original_name": {
            "type": "string",
            "description": "The file name, 1–255 bytes after trimming. Names containing `/`, `\\`, a NUL byte or `..` are refused."
          },
          "declared_mime": {
            "type": "string",
            "description": "verified against magic bytes after upload; a mismatch fails the attachment"
          },
          "byte_size": {
            "type": "integer",
            "format": "int64",
            "description": "Exact size in bytes. Multipart is for files larger than 5 MiB, up to the deployment's upload limit (100 MiB by default); smaller files use the single-part upload."
          },
          "part_size": {
            "type": "integer",
            "format": "int64",
            "description": "optional; server chooses a default when omitted"
          }
        }
      },
      "InitMultipartResponse": {
        "type": "object",
        "required": [
          "attachment_id",
          "upload_id",
          "part_size",
          "part_urls",
          "expires_at"
        ],
        "properties": {
          "attachment_id": {
            "type": "string",
            "format": "uuid",
            "description": "The attachment's id. Use it to complete the upload and to attach the file to a message."
          },
          "upload_id": {
            "type": "string",
            "description": "The object store's id for this multipart upload. Opaque; the complete call does not need it."
          },
          "part_size": {
            "type": "integer",
            "format": "int64",
            "description": "Bytes per part: 16 MiB unless you asked for another size, clamped to 5–64 MiB. Split the file at this size; only the last part may be smaller."
          },
          "part_urls": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            },
            "description": "one presigned PUT per part, in order"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the part URLs stop accepting uploads."
          }
        }
      },
      "MultipartPart": {
        "type": "object",
        "required": [
          "part_number",
          "etag"
        ],
        "properties": {
          "part_number": {
            "type": "integer",
            "description": "1-based"
          },
          "etag": {
            "type": "string",
            "description": "ETag returned by the presigned PUT for this part"
          }
        }
      },
      "CompleteMultipartRequest": {
        "type": "object",
        "required": [
          "parts"
        ],
        "properties": {
          "parts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MultipartPart"
            },
            "description": "Every part, with the `ETag` header the object store returned for its PUT, in part-number order."
          }
        }
      },
      "UploadKeyPackagesRequest": {
        "type": "object",
        "required": [
          "packages"
        ],
        "properties": {
          "device_id": {
            "type": "string",
            "description": "The device these key packages belong to. Defaults to `default`."
          },
          "packages": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "opaque base64 MLS key packages; never interpreted server-side"
          },
          "last_resort": {
            "type": "boolean",
            "default": false,
            "description": "`true` publishes the device's single last-resort package (RFC 9750): it is served when the device's regular pool is dry and is never consumed by being served. The request must carry exactly one package; a new last-resort upload replaces the previous one."
          },
          "expires_in_seconds": {
            "type": "integer",
            "minimum": 1,
            "maximum": 31536000,
            "description": "Server-side lifetime. Past it the package is never handed out and is swept. Omit to keep the package until it is consumed."
          }
        }
      },
      "ConsumedKeyPackage": {
        "type": "object",
        "required": [
          "device_id",
          "package"
        ],
        "properties": {
          "device_id": {
            "type": "string",
            "description": "The device this key package belongs to."
          },
          "package": {
            "type": "string",
            "description": "The MLS key package, base64-encoded. Opaque to the server."
          },
          "last_resort": {
            "type": "boolean",
            "description": "`true` when this is the device's last-resort package, served because its regular pool was dry. Other joiners may have received the same one; expect the owner to replace it. Always present."
          }
        }
      },
      "ConsumeKeyPackagesResponse": {
        "type": "object",
        "required": [
          "principal_id",
          "packages"
        ],
        "properties": {
          "principal_id": {
            "type": "string",
            "description": "Whose key packages these are."
          },
          "packages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ConsumedKeyPackage"
            },
            "description": "At most one package per device of the principal, oldest first. A regular package is handed out once: it is marked consumed and never served again. When a device's regular pool is dry its last-resort package is served instead, unconsumed, and flagged `last_resort: true`. Expired packages are never served."
          }
        }
      },
      "PutGroupInfoRequest": {
        "type": "object",
        "required": [
          "epoch",
          "group_info"
        ],
        "properties": {
          "epoch": {
            "type": "integer",
            "format": "int64",
            "minimum": 0,
            "description": "The MLS epoch this GroupInfo describes. An epoch older than the one already published is refused with 409; the same epoch may be re-published."
          },
          "group_info": {
            "type": "string",
            "description": "base64 TLS-serialized GroupInfo (an MLSMessage of type group_info). Opaque to the server; at most 64 KiB."
          },
          "ratchet_tree": {
            "type": "string",
            "description": "base64 TLS-serialized ratchet tree, so Welcomes can omit the ratchet_tree extension. Opaque; at most 256 KiB — about a hundred members on a post-quantum cipher suite."
          }
        }
      },
      "GroupInfoResponse": {
        "type": "object",
        "required": [
          "epoch",
          "group_info",
          "updated_by",
          "updated_at"
        ],
        "properties": {
          "epoch": {
            "type": "integer",
            "format": "int64",
            "description": "The epoch the published state describes."
          },
          "group_info": {
            "type": "string",
            "description": "base64 TLS-serialized GroupInfo, exactly as published."
          },
          "ratchet_tree": {
            "type": "string",
            "description": "base64 TLS-serialized ratchet tree, when the committer published one."
          },
          "updated_by": {
            "type": "string",
            "description": "The principal who published it."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ApiError": {
        "type": "object",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "unauthorized",
              "forbidden",
              "chat_send_forbidden",
              "chat_history_forbidden",
              "attachment_upload_forbidden",
              "attachment_download_forbidden",
              "blocked",
              "not_found",
              "conflict",
              "invalid_request",
              "payload_too_large",
              "rate_limited",
              "quota_exceeded",
              "unsupported_media_type",
              "internal",
              "not_ready"
            ],
            "description": "Stable, machine-readable error code. Branch on this, never on `message`."
          },
          "message": {
            "type": "string",
            "description": "Human-readable detail for logs and developers. The wording may change; do not parse it."
          }
        }
      },
      "Message": {
        "type": "object",
        "required": [
          "id",
          "event_seq",
          "room_id",
          "sender_id",
          "client_message_id",
          "kind",
          "body",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The message's id."
          },
          "event_seq": {
            "type": "integer",
            "format": "int64",
            "description": "The message's position: strictly increasing within a room, assigned at commit. Use it for ordering and for catch-up (`after_seq`). It is not contiguous — one sequence is shared by all rooms — so a jump between two messages does not mean one is missing."
          },
          "room_id": {
            "type": "string",
            "format": "uuid",
            "description": "The room the message belongs to."
          },
          "sender_id": {
            "type": "string",
            "description": "The principal that sent the message. For a client send it comes from the session token, never the body; for a server send (`POST /v1/server/rooms/{room_id}/messages`) it is the `sender_principal_id` you named, which must be a registered non-human principal."
          },
          "sender_kind": {
            "type": "string",
            "enum": [
              "agent",
              "bot",
              "service",
              "system"
            ],
            "description": "Immutable non-human sender classification captured when a server send creates the message. It survives replay, edit, delete, history, search, realtime events and webhooks. Absent for human-authored messages."
          },
          "client_message_id": {
            "type": "string",
            "description": "The sender's idempotency key for this message."
          },
          "kind": {
            "type": "string",
            "enum": [
              "text",
              "image",
              "voice",
              "video",
              "story",
              "file",
              "system",
              "deleted"
            ],
            "description": "What the message is. Once a message is soft-deleted its kind reads `deleted` and its `body` is null."
          },
          "body": {
            "description": "The message content as sent: any JSON value, at most 16 KiB serialised (conventionally `{\"text\": …}`). In an E2EE room it is `{\"ct\": base64}` ciphertext, up to 128 KiB. Null once the message is deleted."
          },
          "reply_to": {
            "type": "string",
            "format": "uuid",
            "description": "The id of the message this one replies to, in the same room. Absent when it is not a reply."
          },
          "forward_depth": {
            "type": "integer",
            "minimum": 1,
            "description": "Present on a forwarded message: how many forwards deep it is (1 for a forward of an original, 2 for a forward of a forward, …). Render \"Forwarded\" from it, and \"Forwarded many times\" past a threshold of your choosing. The source message is never named — recipients may not be in the room it came from. Absent on a message composed in its room."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the message was committed."
          },
          "edited_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the message was last edited. Absent if never edited."
          },
          "deleted_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the message was soft-deleted. Absent unless deleted."
          },
          "attachments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AttachmentRef"
            },
            "description": "The files attached to the message. Absent when there are none."
          },
          "reactions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReactionAggregate"
            },
            "description": "The message's reactions, aggregated per emoji. Absent when there are none. This is the authoritative state: `reaction.added`/`reaction.removed` events are never replayed, so after a reconnect rebuild from here rather than from the events."
          }
        }
      },
      "AttachmentRef": {
        "type": "object",
        "required": [
          "id",
          "original_name",
          "mime",
          "byte_size",
          "status",
          "has_thumbnail"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The attachment's id."
          },
          "original_name": {
            "type": "string",
            "description": "The file name as uploaded (sanitised)."
          },
          "mime": {
            "type": "string",
            "description": "The attachment's content type."
          },
          "byte_size": {
            "type": "integer",
            "format": "int64",
            "description": "Size in bytes."
          },
          "status": {
            "type": "string",
            "description": "`pending` until the upload completes, `uploaded` while it is verified, then `ready` or `failed`."
          },
          "width": {
            "type": "integer",
            "description": "Image width in pixels, when known."
          },
          "height": {
            "type": "integer",
            "description": "Image height in pixels, when known."
          },
          "has_thumbnail": {
            "type": "boolean",
            "description": "Whether a thumbnail was generated; the download response then carries `thumbnail_url`."
          }
        }
      },
      "MessagePage": {
        "type": "object",
        "required": [
          "messages",
          "latest_seq"
        ],
        "properties": {
          "messages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Message"
            },
            "description": "The messages. From history, oldest first (ascending `event_seq`); from search, best match first, then newest."
          },
          "latest_seq": {
            "type": "integer",
            "format": "int64",
            "description": "The room's highest `event_seq` at query time. Keep it for catch-up after a reconnect."
          }
        }
      },
      "PostMessageRequest": {
        "type": "object",
        "required": [
          "client_message_id"
        ],
        "properties": {
          "client_message_id": {
            "type": "string",
            "maxLength": 128,
            "description": "Your idempotency key, 1–128 characters. Resending the same key returns the original message instead of creating a duplicate, so generate it once per message and reuse it on retries."
          },
          "kind": {
            "type": "string",
            "default": "text",
            "enum": [
              "text",
              "image",
              "voice",
              "video",
              "story",
              "file",
              "system",
              "mls.commit",
              "mls.proposal",
              "mls.application",
              "mls.welcome"
            ],
            "description": "The message kind. Defaults to `text`. Plaintext rooms accept the plain kinds; an E2EE room accepts only the `mls.*` kinds, and a plain room refuses them (409)."
          },
          "body": {
            "description": "The message content: any JSON value, at most 16 KiB serialised. The server does not require a shape; clients conventionally send an object such as `{\"text\": …}`, which is what previews and search read. For `mls.*` kinds it must be `{\"ct\": base64}`, up to 128 KiB. Required unless `forward_of` is set. A `mentions` array of principal ids (at most 50, each an active member of the room, validated on send; 400 otherwise) names members: it is stored and delivered as sent, and a mentioned member is pushed even while they have muted the room. Not on `system` or `mls.*` messages."
          },
          "reply_to": {
            "type": "string",
            "format": "uuid",
            "description": "Id of a message in the same room this one replies to."
          },
          "attachment_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "maxItems": 10,
            "description": "Up to 10 attachments to attach, each already `ready` and uploaded to this room."
          },
          "forward_of": {
            "type": "string",
            "format": "uuid",
            "description": "Forward a message instead of composing one: the id of a message in any room you are an active member of (this one included). The server copies its kind, body and media into this room — `kind`, `body` and `attachment_ids` in this request are ignored — and the copy carries `forward_depth` one deeper than the source. Only `text`, `image`, `voice`, `video`, `file` and `story` messages can be forwarded, not a deleted one, and not into an E2EE room (409). A source you cannot see — unknown, another tenant's, in a room you are not in — is one 400. `mentions` in the source do not travel. Media is copied inside the store as ready attachments of this room, uploaded by you."
          }
        }
      },
      "MessageResponse": {
        "type": "object",
        "required": [
          "message",
          "duplicate"
        ],
        "properties": {
          "message": {
            "$ref": "#/components/schemas/Message",
            "description": "The message as stored."
          },
          "duplicate": {
            "type": "boolean",
            "description": "true when client_message_id was already persisted; the original message is returned"
          }
        }
      },
      "InitAttachmentRequest": {
        "type": "object",
        "required": [
          "original_name",
          "declared_mime",
          "byte_size"
        ],
        "properties": {
          "original_name": {
            "type": "string",
            "maxLength": 255,
            "description": "The file name, 1–255 bytes after trimming. Names containing `/`, `\\`, a NUL byte or `..` are refused."
          },
          "declared_mime": {
            "type": "string",
            "enum": [
              "image/jpeg",
              "image/png",
              "image/webp",
              "image/gif",
              "image/heic",
              "image/heif",
              "audio/ogg",
              "audio/mpeg",
              "audio/mp4",
              "audio/webm",
              "video/mp4",
              "video/webm",
              "video/quicktime",
              "application/pdf",
              "text/plain",
              "application/zip",
              "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
              "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
              "application/vnd.openxmlformats-officedocument.presentationml.presentation"
            ],
            "description": "The file's type. It must be on the allowlist, and the server checks the uploaded bytes match it. JPEG, PNG, WebP and GIF are thumbnailed; HEIC/HEIF (an iPhone photo) is stored and served as sent, without a thumbnail. Word, Excel and PowerPoint (Office Open XML) and ZIP are served for download only; legacy `.doc`/`.xls`/`.ppt` are refused. In an E2EE room only `application/octet-stream` (ciphertext) is accepted."
          },
          "byte_size": {
            "type": "integer",
            "format": "int64",
            "minimum": 1,
            "description": "Exact size in bytes, from 1 up to the deployment's upload limit (100 MiB by default). Counts against the tenant's monthly byte quota."
          },
          "sha256": {
            "type": "string",
            "description": "Optional SHA-256 of the file, hex. When given, the upload is rejected if the stored bytes do not match it."
          },
          "poster_of": {
            "type": "string",
            "format": "uuid",
            "description": "Declare this upload as the poster frame of an existing video attachment in the same room. The upload must be an image and the referenced attachment a video, else 400. The video's download response then carries `poster_url`."
          }
        }
      },
      "InitAttachmentResponse": {
        "type": "object",
        "required": [
          "attachment_id",
          "upload_url",
          "upload_expires_at",
          "required_headers"
        ],
        "properties": {
          "attachment_id": {
            "type": "string",
            "format": "uuid",
            "description": "The attachment's id. Complete the upload with it, then attach it to a message."
          },
          "upload_url": {
            "type": "string",
            "format": "uri",
            "description": "Presigned PUT URL, 5-minute expiry; upload directly, bytes never pass through the API"
          },
          "upload_expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When `upload_url` stops accepting the PUT. The window scales with the file size, from 5 minutes up to 2 hours."
          },
          "required_headers": {
            "type": "array",
            "items": {
              "type": "array",
              "prefixItems": [
                {
                  "type": "string"
                },
                {
                  "type": "string"
                }
              ]
            },
            "description": "Headers the PUT to `upload_url` must carry, as `[name, value]` pairs. The signature covers them, so send each one exactly."
          }
        }
      },
      "DownloadResponse": {
        "type": "object",
        "required": [
          "download_url",
          "expires_at",
          "mime",
          "original_name",
          "disposition"
        ],
        "properties": {
          "download_url": {
            "type": "string",
            "format": "uri",
            "description": "A presigned GET URL for the file."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When `download_url` (and `thumbnail_url`) stop working. Ask again for a fresh one."
          },
          "mime": {
            "type": "string",
            "description": "The file's content type."
          },
          "original_name": {
            "type": "string",
            "description": "The file name as uploaded (sanitised)."
          },
          "thumbnail_url": {
            "type": "string",
            "format": "uri",
            "description": "Presigned URL of the generated thumbnail. Absent when none exists."
          },
          "poster_url": {
            "type": "string",
            "format": "uri",
            "description": "Presigned URL of the video's poster frame, when one was uploaded with `poster_of`. Absent otherwise."
          },
          "disposition": {
            "type": "string",
            "enum": [
              "inline",
              "attachment"
            ],
            "description": "How the URL serves the file: `inline` for images, audio and video; `attachment` (a download) for everything else, such as PDFs, text and zip files."
          }
        }
      },
      "PatchRoomRequest": {
        "type": "object",
        "description": "Both fields optional; send only what changes.",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "active",
              "closed"
            ],
            "description": "`closed` stops new session tokens being minted for the room (409); members keep access with the tokens they hold. `active` reopens it."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "Informational expiry time, stored and returned. Nothing closes or deletes the room when it passes."
          }
        }
      },
      "AccountCreateApiKeyResponse": {
        "type": "object",
        "description": "Account-plane key creation. Unlike the operator plane's CreateApiKeyResponse it carries no tenant_id: the tenant is implied by the account session.",
        "required": [
          "key_id",
          "api_key",
          "environment",
          "scopes"
        ],
        "properties": {
          "key_id": {
            "type": "string",
            "description": "The key's public id (`ak_live_…` or `ak_test_…`). Use it to rotate or revoke the key; it is not a credential."
          },
          "api_key": {
            "type": "string",
            "description": "Full ak_<env>_<id>.<secret>, shown once."
          },
          "environment": {
            "type": "string",
            "enum": [
              "live",
              "test"
            ],
            "description": "`live` or `test`. A test key sees only test data."
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "rooms:read",
                "rooms:write",
                "members:write",
                "tokens:mint",
                "messages:write",
                "webhooks:manage",
                "usage:read",
                "rooms:delete",
                "data:erase"
              ]
            },
            "description": "What the key may do. A call outside these scopes is refused with 403 when it is made."
          }
        }
      },
      "CompleteMultipartResponse": {
        "type": "object",
        "required": [
          "attachment_id",
          "status"
        ],
        "properties": {
          "attachment_id": {
            "type": "string",
            "format": "uuid",
            "description": "The attachment's id."
          },
          "status": {
            "type": "string",
            "enum": [
              "uploaded"
            ],
            "description": "Always `uploaded` — accepted and queued for verification. Unlike the single-part complete there is no idempotent no-op: completing again once the upload has left `pending` answers 409."
          }
        }
      },
      "Room": {
        "type": "object",
        "required": [
          "id",
          "tenant_id",
          "created_by",
          "status",
          "e2ee",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The room's id, assigned by the server."
          },
          "tenant_id": {
            "type": "string",
            "description": "The tenant that owns the room."
          },
          "external_ref": {
            "type": "string",
            "description": "Your own reference for the room, if you gave one. Creating a room with an `external_ref` that already exists in the tenant returns that room."
          },
          "created_by": {
            "type": "string",
            "description": "The principal recorded as the room's creator."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "closed"
            ],
            "description": "`active`, or `closed` (no new tokens are minted for it)."
          },
          "e2ee": {
            "type": "boolean",
            "description": "End-to-end encrypted room. When true, the server accepts only ciphertext message kinds, rejects search and reactions, and pushes are content-free — the client must encrypt before sending."
          },
          "e2ee_scheme": {
            "type": "string",
            "enum": [
              "mls",
              "megolm"
            ],
            "description": "Which client E2EE scheme the room's members run, when the creator declared it: `mls` (the primary scheme) or `megolm` (the matrix-sdk-crypto transport). The two cannot share a room. Absent when undeclared and on plaintext rooms."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the room was created."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "Informational expiry time, if one was set. Nothing closes or deletes the room when it passes."
          }
        }
      },
      "SessionTokenRequest": {
        "type": "object",
        "required": [
          "tenant_id",
          "room_id",
          "principal_id"
        ],
        "properties": {
          "tenant_id": {
            "type": "string",
            "description": "The tenant the room belongs to."
          },
          "room_id": {
            "type": "string",
            "format": "uuid",
            "description": "The room the token is for. A token opens exactly one room."
          },
          "principal_id": {
            "type": "string",
            "description": "Who the token is for. Must be an active member of the room."
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "member",
              "viewer"
            ],
            "description": "Optional: mint with fewer rights than the member has. At or below the member's stored role (a higher one is 403); anything but owner|member|viewer is 400. Omit to use the member's own role."
          },
          "ttl_seconds": {
            "type": "integer",
            "minimum": 60,
            "maximum": 3600,
            "default": 600,
            "description": "Token lifetime in seconds. Defaults to 600 and is clamped to 60–3600."
          }
        }
      },
      "SessionTokenResponse": {
        "type": "object",
        "required": [
          "access_token",
          "expires_at",
          "rtc_websocket_url",
          "chat_websocket_url",
          "ice_servers"
        ],
        "properties": {
          "access_token": {
            "type": "string",
            "description": "The room-scoped session token (an Ed25519-signed JWT). Present it as `Authorization: Bearer …` over HTTP and as the WebSocket subprotocol `bearer.<token>`, never in a URL."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the token expires. Mint a new one before then; a socket closes with 4401 at expiry."
          },
          "rtc_websocket_url": {
            "type": "string",
            "format": "uri",
            "description": "The call-signalling WebSocket URL."
          },
          "chat_websocket_url": {
            "type": "string",
            "format": "uri",
            "description": "The chat WebSocket URL."
          },
          "ice_servers": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "urls"
              ],
              "properties": {
                "urls": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "description": "The server URLs. They never carry a `?transport=` query, which Safari refuses."
                },
                "username": {
                  "type": "string",
                  "description": "expiry_unix:principal — ephemeral, 600 s"
                },
                "credential": {
                  "type": "string",
                  "description": "base64(HMAC-SHA1(turn_secret, username))"
                }
              }
            },
            "description": "STUN and TURN servers for WebRTC, with short-lived TURN credentials. Pass them to `RTCPeerConnection` unchanged."
          }
        }
      },
      "Participant": {
        "type": "object",
        "required": [
          "principal_id",
          "role",
          "online"
        ],
        "properties": {
          "principal_id": {
            "type": "string",
            "description": "The member's principal id."
          },
          "role": {
            "type": "string",
            "description": "The member's role: `owner`, `member` or `viewer`."
          },
          "online": {
            "type": "boolean",
            "description": "live presence at query time, from Valkey; absent presence reads as false rather than failing the call"
          }
        }
      },
      "ParticipantsResponse": {
        "type": "object",
        "required": [
          "participants"
        ],
        "properties": {
          "participants": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Participant"
            },
            "description": "Every active member of the room, with chat presence. Not who is in a call."
          }
        }
      },
      "RoomMember": {
        "type": "object",
        "required": [
          "tenant_id",
          "room_id",
          "principal_id",
          "role"
        ],
        "properties": {
          "tenant_id": {
            "type": "string",
            "description": "The tenant that owns the room."
          },
          "room_id": {
            "type": "string",
            "format": "uuid",
            "description": "The room."
          },
          "principal_id": {
            "type": "string",
            "description": "The member's principal id."
          },
          "role": {
            "type": "string",
            "description": "`owner`, `member` or `viewer`."
          },
          "joined_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the principal first joined the room. Restoring a removed member keeps the original time."
          },
          "left_at": {
            "type": "string",
            "format": "date-time",
            "description": "set when the member was removed; the row is retained so history stays auditable"
          },
          "muted_until": {
            "type": "string",
            "format": "date-time",
            "description": "Until when this member has push notifications from the room silenced; absent when not muted. `9999-12-31T00:00:00Z` stands for \"until unmuted\". Live delivery and call rings are unaffected."
          }
        }
      },
      "TrackPreference": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "publisher",
          "track_id",
          "layer"
        ],
        "properties": {
          "publisher": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128,
            "description": "The publisher's principal id — the msid stream id a `chatbox.msid-principal` socket sees."
          },
          "track_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 256,
            "description": "The publisher's own track id as the msid carries it (or the `<client id>-<mid>` stand-in a bare m-line gets)."
          },
          "layer": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "auto"
            ],
            "description": "Named by quality, resolved against the layers the publisher is sending now, ordered by measured bitrate: `low` the cheapest, `high` the dearest, `medium` the middle one or the dearer of two. `auto` is no preference: the adaptive policy decides."
          }
        }
      },
      "MemberPreferencesRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "tracks"
        ],
        "properties": {
          "tracks": {
            "type": "array",
            "maxItems": 128,
            "items": {
              "$ref": "#/components/schemas/TrackPreference"
            },
            "description": "The member's whole list, replaced. An `auto` entry clears that track; an empty list clears everything."
          }
        }
      },
      "MemberPreferences": {
        "type": "object",
        "required": [
          "principal_id",
          "in_call",
          "tracks"
        ],
        "properties": {
          "principal_id": {
            "type": "string"
          },
          "in_call": {
            "type": "boolean",
            "description": "Whether the member has a signaling connection in this room's call right now. Preferences set for a member who is not — or whose last connection just closed — are held for a 30 s grace and then dropped."
          },
          "tracks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TrackPreference"
            },
            "description": "What is pinned, sorted by publisher then track id. `auto` entries are never listed."
          }
        }
      },
      "RtcPublishPolicy": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "audio": {
            "type": "boolean",
            "default": true,
            "description": "May publish audio."
          },
          "video": {
            "type": "boolean",
            "default": true,
            "description": "May publish video (any source — the server cannot tell a screen from a camera)."
          },
          "max_audio_tracks": {
            "type": "integer",
            "minimum": 0,
            "description": "Most audio publications active at once. Omitted: no limit."
          },
          "max_video_tracks": {
            "type": "integer",
            "minimum": 0,
            "description": "Most video publications active at once. Omitted: no limit."
          }
        }
      },
      "RtcSubscribePolicy": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "mode"
        ],
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "all",
              "none",
              "allowlist"
            ],
            "description": "Which publishers the member may receive. Applied by the media loop from the leg's first offer: a refused publisher's sections are never offered; a tightening stops its packets at once, removes its sections and re-offers the leg; a loosening offers the newly allowed publishers."
          },
          "publishers": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 64,
            "description": "Principal ids, exact opaque strings, 1–128 characters each, at most 64; only with `allowlist`. A SET: duplicates are 400, the server sorts the ids canonically before storing and comparing, order never affects forwarding, and the same set in a different order at an equal revision is an idempotent no-op, not `revision_conflict`. GET returns the sorted list."
          }
        }
      },
      "RtcChatPolicy": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "send": {
            "type": "boolean",
            "default": true,
            "description": "May send chat messages. `false`: a send fails with an error ack `chat_send_forbidden`; the socket stays open."
          },
          "history": {
            "type": "boolean",
            "default": true,
            "description": "May read history. `false` also closes the chat socket with reason `policy_changed`."
          }
        }
      },
      "RtcAttachmentPolicy": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "upload": {
            "type": "boolean",
            "default": true
          },
          "download": {
            "type": "boolean",
            "default": true
          }
        }
      },
      "RtcPolicyDocument": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "revision"
        ],
        "properties": {
          "revision": {
            "type": "integer",
            "format": "int64",
            "minimum": 1,
            "description": "Your monotonic revision for this member's policy. Higher than stored: replaced. Equal with the same document: no-op. Equal with a different document: 409 `revision_conflict`. Lower: 409 `stale_revision`. Never restarts: after a `DELETE`, the next revision must exceed the deletion's tombstone."
          },
          "publish": {
            "$ref": "#/components/schemas/RtcPublishPolicy"
          },
          "subscribe": {
            "$ref": "#/components/schemas/RtcSubscribePolicy"
          },
          "chat": {
            "$ref": "#/components/schemas/RtcChatPolicy"
          },
          "attachments": {
            "$ref": "#/components/schemas/RtcAttachmentPolicy"
          },
          "roster": {
            "type": "string",
            "enum": [
              "all",
              "authorized-only",
              "none"
            ],
            "description": "Who a restricted subscriber sees in the RTC roster frames (`peers`, `peer.joined`, `peer.left`): `all`, `authorized-only` (the publishers it may receive, nobody else) or `none`. Omitted: `all` unless `subscribe.mode` is `none` or `allowlist`, then `authorized-only`. A change re-sends the member a fresh `peers` snapshot under the new filter."
          },
          "devices": {
            "$ref": "#/components/schemas/RtcDevicePolicy"
          }
        }
      },
      "RtcPolicyState": {
        "type": "object",
        "required": [
          "principal_id",
          "stored_revision",
          "applied_revision",
          "applied",
          "in_call",
          "active_legs",
          "applied_legs",
          "subscribe_enforced",
          "enforcement",
          "policy"
        ],
        "properties": {
          "principal_id": {
            "type": "string"
          },
          "stored_revision": {
            "type": "integer",
            "format": "int64"
          },
          "applied_revision": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "The revision every active leg of the member runs; `null` while the member has no leg or a leg has not reported yet."
          },
          "applied": {
            "type": "boolean",
            "description": "Every component that is both supported and present in the document has reached `stored_revision` (see `enforcement`). `false` while the member has no live leg: there is nothing to apply the publish rules to yet."
          },
          "applied_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "in_call": {
            "type": "boolean"
          },
          "active_legs": {
            "type": "integer"
          },
          "applied_legs": {
            "type": "integer",
            "description": "Legs running `applied_revision`; differs from `active_legs` only for the moment a change is still landing."
          },
          "subscribe_enforced": {
            "type": "boolean",
            "description": "`true` since Phase 2: `subscribe` and `roster` are enforced. Superseded by `enforcement.subscribe.supported`; kept for the first clients."
          },
          "enforcement": {
            "$ref": "#/components/schemas/RtcPolicyEnforcement"
          },
          "policy": {
            "$ref": "#/components/schemas/RtcPolicyDocument"
          }
        }
      },
      "RtcPolicyEnforcement": {
        "type": "object",
        "description": "Per-component enforcement state. `applied` at the top level is true only when every component that is both `supported` and `requested` has reached `stored_revision`; a requested component this release does not enforce stays visible here and is never counted as applied.",
        "required": [
          "publish",
          "capabilities",
          "subscribe",
          "roster",
          "devices"
        ],
        "properties": {
          "publish": {
            "$ref": "#/components/schemas/RtcComponentEnforcement"
          },
          "capabilities": {
            "$ref": "#/components/schemas/RtcComponentEnforcement"
          },
          "subscribe": {
            "$ref": "#/components/schemas/RtcComponentEnforcement"
          },
          "roster": {
            "$ref": "#/components/schemas/RtcComponentEnforcement"
          },
          "devices": {
            "$ref": "#/components/schemas/RtcComponentEnforcement"
          }
        }
      },
      "RtcComponentEnforcement": {
        "type": "object",
        "required": [
          "supported",
          "requested",
          "applied_revision"
        ],
        "properties": {
          "supported": {
            "type": "boolean",
            "description": "Whether this server release enforces the component at all. All four since Phase 2."
          },
          "requested": {
            "type": "boolean",
            "description": "Whether the stored document has this section (`capabilities` = `chat` or `attachments`)."
          },
          "applied_revision": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "The revision the component runs at. `publish`: the revision every live leg has applied, `null` with no leg. `capabilities`: the stored revision — read from the membership row at every request, and by a live chat socket within about a second of the write. `null` when unsupported or not requested."
          }
        }
      },
      "RtcPolicyRevisionError": {
        "type": "object",
        "required": [
          "code",
          "message",
          "current_revision"
        ],
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "stale_revision",
              "revision_conflict",
              "policy_deleted"
            ],
            "description": "`stale_revision` and `revision_conflict` are 409s on PUT and DELETE; `policy_deleted` is the 404 on a DELETE whose policy was already deleted at another revision."
          },
          "message": {
            "type": "string"
          },
          "current_revision": {
            "type": "integer",
            "format": "int64",
            "description": "The revision stored now (a policy's, or a deletion's tombstone), to rebase on."
          }
        }
      },
      "MuteRequest": {
        "type": "object",
        "required": [
          "duration"
        ],
        "additionalProperties": false,
        "properties": {
          "duration": {
            "type": "string",
            "enum": [
              "8h",
              "1w",
              "always"
            ],
            "description": "How long to silence push notifications: eight hours, one week, or until unmuted. `9999-12-31T00:00:00Z` stands for \"until unmuted\"."
          }
        },
        "description": "Mute a conversation for one member. Push notifications only: the member still receives every event live over the WebSocket, and a call still rings."
      },
      "ConversationMute": {
        "type": "object",
        "required": [
          "room_id",
          "muted_until"
        ],
        "properties": {
          "room_id": {
            "type": "string",
            "format": "uuid",
            "description": "The conversation."
          },
          "muted_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Until when push notifications from this conversation are silenced for you; null when not muted. `9999-12-31T00:00:00Z` stands for \"until unmuted\"."
          }
        },
        "description": "The caller's own mute on a conversation, as the inbox also reports it."
      },
      "BlockEntry": {
        "type": "object",
        "required": [
          "user_id",
          "display_name",
          "phone",
          "since"
        ],
        "properties": {
          "user_id": {
            "type": "string",
            "format": "uuid",
            "description": "The blocked user."
          },
          "display_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Their display name, or null."
          },
          "phone": {
            "type": "string",
            "description": "Their phone number, E.164."
          },
          "since": {
            "type": "string",
            "format": "date-time",
            "description": "When you blocked them."
          }
        },
        "description": "One entry of your block list."
      },
      "BlockState": {
        "type": "object",
        "required": [
          "user_id",
          "blocked"
        ],
        "properties": {
          "user_id": {
            "type": "string",
            "format": "uuid"
          },
          "blocked": {
            "type": "boolean"
          },
          "since": {
            "type": "string",
            "format": "date-time",
            "description": "When the block was placed; present when `blocked` is true. Blocking again keeps the original time."
          }
        },
        "description": "The result of blocking or unblocking one user."
      },
      "NotAdded": {
        "type": "object",
        "required": [
          "user_id",
          "reason"
        ],
        "properties": {
          "user_id": {
            "type": "string",
            "format": "uuid",
            "description": "Someone you named who was not added."
          },
          "reason": {
            "type": "string",
            "enum": [
              "blocked"
            ],
            "description": "`blocked`: they have blocked you."
          }
        },
        "description": "A member you named who was left out, and why. The rest of the request went ahead."
      },
      "RoomStatus": {
        "type": "object",
        "required": [
          "room",
          "member_count",
          "online_participants",
          "rtc_participants"
        ],
        "properties": {
          "room": {
            "$ref": "#/components/schemas/Room",
            "description": "The room."
          },
          "member_count": {
            "type": "integer",
            "format": "int64",
            "description": "How many active members the room has."
          },
          "online_participants": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "principals with a live chat WebSocket"
          },
          "rtc_participants": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "principals currently published to the SFU"
          }
        }
      },
      "CompleteAttachmentResponse": {
        "type": "object",
        "required": [
          "attachment_id",
          "status"
        ],
        "properties": {
          "attachment_id": {
            "type": "string",
            "format": "uuid",
            "description": "The attachment's id."
          },
          "status": {
            "type": "string",
            "enum": [
              "uploaded",
              "ready"
            ],
            "description": "`uploaded` — accepted and queued for verification (the normal answer); `ready` — this attachment was already verified, so completing it again is a no-op. Any other state answers 409."
          }
        }
      },
      "MessageEnvelope": {
        "type": "object",
        "required": [
          "message"
        ],
        "properties": {
          "message": {
            "$ref": "#/components/schemas/Message",
            "description": "The message as stored."
          }
        }
      },
      "ReceiptResult": {
        "type": "object",
        "required": [
          "advanced"
        ],
        "properties": {
          "advanced": {
            "type": "boolean",
            "description": "false when the cursor already covered this message, which makes retries safe"
          }
        }
      },
      "RemovedResult": {
        "type": "object",
        "required": [
          "removed"
        ],
        "properties": {
          "removed": {
            "type": "boolean",
            "description": "Always true: removing a principal who is not an active member answers 404."
          }
        }
      },
      "OneTimeKeyCounts": {
        "type": "object",
        "required": [
          "signed_curve25519"
        ],
        "properties": {
          "signed_curve25519": {
            "type": "integer",
            "format": "int64",
            "description": "How many unclaimed one-time keys the device has left on the server. Upload more before it runs low."
          }
        }
      },
      "KeysUploadResponse": {
        "type": "object",
        "required": [
          "one_time_key_counts"
        ],
        "properties": {
          "one_time_key_counts": {
            "$ref": "#/components/schemas/OneTimeKeyCounts",
            "description": "The device's remaining one-time keys after this upload, per algorithm."
          }
        }
      },
      "KeysQueryResponse": {
        "type": "object",
        "required": [
          "device_keys"
        ],
        "properties": {
          "device_keys": {
            "type": "object",
            "description": "principal id -> device id -> the device key object that principal published",
            "additionalProperties": {
              "type": "object",
              "additionalProperties": {
                "type": "object"
              }
            }
          }
        }
      },
      "KeysClaimResponse": {
        "type": "object",
        "required": [
          "one_time_keys"
        ],
        "properties": {
          "one_time_keys": {
            "type": "object",
            "description": "principal id -> device id -> { key_id: key }; a device is absent when its pool is dry and it published no fallback key",
            "additionalProperties": {
              "type": "object",
              "additionalProperties": {
                "type": "object"
              }
            }
          }
        }
      },
      "ToDeviceSendResponse": {
        "type": "object",
        "required": [
          "queued"
        ],
        "properties": {
          "queued": {
            "type": "integer",
            "format": "int64",
            "description": "per-device rows written; a wildcard recipient fans out to every known device"
          }
        }
      },
      "ToDeviceEvent": {
        "type": "object",
        "required": [
          "sender",
          "type",
          "content"
        ],
        "properties": {
          "sender": {
            "type": "string",
            "description": "server-stamped from the token, never taken from the request body"
          },
          "type": {
            "type": "string",
            "description": "The event type the sender used, for example `m.room.encrypted`."
          },
          "content": {
            "type": "object",
            "description": "The event content as the sender wrote it. Opaque to the server."
          }
        }
      },
      "ToDeviceFetchResponse": {
        "type": "object",
        "required": [
          "events",
          "next_token",
          "one_time_key_counts",
          "unused_fallback_key_algorithms",
          "device_list_changes",
          "device_list_watermark"
        ],
        "properties": {
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ToDeviceEvent"
            },
            "description": "Queued to-device events for this device, oldest first. Acknowledge them on the next fetch so they are deleted."
          },
          "next_token": {
            "type": "integer",
            "format": "int64",
            "description": "pass back as ack once the batch is processed; an unacked batch is re-served"
          },
          "one_time_key_counts": {
            "$ref": "#/components/schemas/OneTimeKeyCounts",
            "description": "The device's remaining one-time keys, per algorithm."
          },
          "unused_fallback_key_algorithms": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Algorithms whose fallback key has not been used yet. Empty means upload a new fallback key."
          },
          "device_list_changes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "principals whose device list changed since device_watermark"
          },
          "device_list_watermark": {
            "type": "integer",
            "format": "int64",
            "description": "Pass this back as `device_watermark` on the next fetch to learn which users' device lists changed since."
          }
        }
      },
      "ReactionAggregate": {
        "type": "object",
        "required": [
          "emoji",
          "count",
          "principals"
        ],
        "description": "One emoji's reactions to a message, as stored — not an event.",
        "properties": {
          "emoji": {
            "type": "string",
            "description": "The emoji reacted with."
          },
          "count": {
            "type": "integer",
            "format": "int64",
            "description": "How many principals have reacted with it. Always `principals.length`."
          },
          "principals": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Who reacted, by principal id."
          }
        }
      },
      "RecordingSegment": {
        "type": "object",
        "required": [
          "started_at",
          "bytes",
          "download_url",
          "expires_at"
        ],
        "properties": {
          "started_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this segment's first frame was recorded. Segments run up to a minute; a gap between one segment's start and the next means the camera was not publishing."
          },
          "bytes": {
            "type": "integer",
            "format": "int64",
            "description": "The stored size of the segment, which is what it counts against the tenant’s recording storage."
          },
          "download_url": {
            "type": "string",
            "description": "A signed link to the fragmented-MP4 segment, valid for 5 minutes. Fetch the list again for a fresh one rather than storing these."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When download_url stops working."
          }
        }
      },
      "RecordingsResponse": {
        "type": "object",
        "required": [
          "segments"
        ],
        "properties": {
          "segments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RecordingSegment"
            },
            "description": "Newest first. Empty when the camera has recorded nothing in the window, and on a deployment with no recording store."
          }
        }
      },
      "DirectoryGroupMember": {
        "type": "object",
        "required": [
          "user_id",
          "phone",
          "role"
        ],
        "properties": {
          "user_id": {
            "type": "string",
            "format": "uuid",
            "description": "The member's directory user id — also their principal id in the room."
          },
          "display_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "As set on their profile, or null."
          },
          "phone": {
            "type": "string",
            "description": "E.164."
          },
          "about": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 256,
            "description": "The profile line, up to 256 characters; null when not set."
          },
          "avatar_version": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When their current profile photo was set; null when there is none. The value to cache the picture by: fetch `GET /v1/directory/users/{user_id}/avatar` once per version (its `ETag` is this value)."
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "member",
              "viewer"
            ],
            "description": "`owner` manages the group; see the group-management operations."
          },
          "joined_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When they first joined; a restored member keeps the original time."
          }
        }
      },
      "DirectoryProfile": {
        "type": "object",
        "required": [
          "id",
          "phone",
          "display_name",
          "about",
          "avatar_version"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The user's id, which is also their chat `principal_id`."
          },
          "phone": {
            "type": "string",
            "description": "The user's verified phone number, E.164."
          },
          "display_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The user's display name, or null if not set."
          },
          "about": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 256,
            "description": "The profile line, up to 256 characters; null when not set."
          },
          "avatar_version": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the current profile photo was set; null when there is none. The value to cache the picture by: fetch `GET /v1/directory/users/{user_id}/avatar` once per version (its `ETag` is this value)."
          }
        },
        "description": "A directory user's own profile, as `GET /v1/directory/me` and every profile change return it."
      },
      "AvatarUpload": {
        "type": "object",
        "required": [
          "upload_id",
          "upload_url",
          "expires_at",
          "max_bytes"
        ],
        "properties": {
          "upload_id": {
            "type": "string",
            "format": "uuid",
            "description": "Hand this to `complete` after the PUT."
          },
          "upload_url": {
            "type": "string",
            "format": "uri",
            "description": "Presigned PUT for the photo bytes. Send the raw bytes with the declared `Content-Type`."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the upload URL stops working."
          },
          "max_bytes": {
            "type": "integer",
            "format": "int64",
            "description": "The size cap, 2 MiB. Downscale on the device first."
          }
        }
      },
      "ConversationInfo": {
        "type": "object",
        "required": [
          "room_id",
          "kind",
          "name",
          "description",
          "icon_attachment_id",
          "member_count",
          "created_at"
        ],
        "properties": {
          "room_id": {
            "type": "string",
            "format": "uuid",
            "description": "The conversation's room."
          },
          "kind": {
            "type": "string",
            "enum": [
              "direct",
              "group"
            ],
            "description": "`direct` or `group`."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The group's name; null for a direct conversation or an unnamed group."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 1024,
            "description": "The group's description, up to 1024 characters; null when there is none."
          },
          "icon_attachment_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The group's icon: an image attachment of the group's own room, or null. Fetch it like any picture posted there — `GET /v1/rooms/{room_id}/attachments/{attachment_id}/download` with a room token — and cache it by id; a changed icon is a new id."
          },
          "member_count": {
            "type": "integer",
            "description": "Active members."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the conversation was created."
          }
        },
        "description": "A conversation as a member sees it. A direct conversation has no name, description or icon of its own: its peer names it (see the inbox)."
      },
      "GroupInvite": {
        "type": "object",
        "required": [
          "invite_id",
          "room_id",
          "created_by",
          "created_at",
          "expires_at",
          "max_uses",
          "uses"
        ],
        "properties": {
          "invite_id": {
            "type": "string",
            "format": "uuid",
            "description": "The link, for revoking it."
          },
          "room_id": {
            "type": "string",
            "format": "uuid",
            "description": "The group it admits to."
          },
          "created_by": {
            "type": "string",
            "format": "uuid",
            "description": "The owner who created it."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When it stops working; null for a link that does not expire."
          },
          "max_uses": {
            "type": [
              "integer",
              "null"
            ],
            "description": "How many people may join through it; null for any number."
          },
          "uses": {
            "type": "integer",
            "description": "How many have. A member joining again is not a use."
          }
        },
        "description": "An invite link, without its token: the token is shown once, in the answer that created it."
      },
      "RtcDevicePolicy": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "max_active",
          "on_limit"
        ],
        "description": "How many devices the member may have in the call at once. A device is the session token's `device_id`: under this policy a new leg with the same `device_id` as a live one is that device reconnecting and replaces its predecessor (closed 4412 `device_reconnected`) whatever `on_limit` says, never counting twice. A leg minted without a `device_id` is a device of its own — mint with one when using a limit, or a reconnect can be refused until the abandoned leg's keepalive gives up (up to 50 s). Counts RTC call legs only, not chat sockets. Checked at each leg's admission, and re-applied to live legs within about a second when the policy changes: a lowered limit closes the surplus at once (`reject-new`: the newest devices, 4411; `replace-old`: the oldest, 4412; `allow`: none). Every conflict is reported as an `rtc.device_conflict` webhook event.",
        "properties": {
          "max_active": {
            "type": "integer",
            "minimum": 1,
            "maximum": 16,
            "description": "Most devices the member may have in the call at once."
          },
          "on_limit": {
            "type": "string",
            "enum": [
              "allow",
              "reject-new",
              "replace-old"
            ],
            "description": "`allow`: admit the new device anyway and only report it. `reject-new`: the new leg gets an `error` event (`reason: device_limit`) and is closed 4411; the devices already in the call are untouched. `replace-old`: the new leg is admitted and the member's oldest device is closed 4412 `policy_changed`."
          }
        }
      },
      "RtcForwardingSubscriber": {
        "type": "object",
        "required": [
          "principal_id",
          "client_id",
          "layer"
        ],
        "properties": {
          "principal_id": {
            "type": "string"
          },
          "client_id": {
            "type": "string",
            "description": "The receiving connection, a u64 as a decimal string (it exceeds 2^53)."
          },
          "layer": {
            "type": [
              "string",
              "null"
            ],
            "description": "The simulcast layer (RID) it receives; `null` for a single-encoding track or before a layer is chosen."
          }
        }
      },
      "RtcForwardingTrack": {
        "type": "object",
        "required": [
          "mid",
          "kind",
          "subscribers"
        ],
        "properties": {
          "mid": {
            "type": "string",
            "description": "The publisher's `a=mid` for the track."
          },
          "track_id": {
            "type": "string",
            "description": "The publisher's own track id (what a `chatbox.msid-principal` subscriber reads off the msid), when the SFU has reported it."
          },
          "kind": {
            "type": "string",
            "enum": [
              "audio",
              "video"
            ]
          },
          "subscribers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RtcForwardingSubscriber"
            },
            "description": "Every connection the track is forwarded to right now."
          }
        }
      },
      "RtcForwardingPublisher": {
        "type": "object",
        "required": [
          "principal_id",
          "client_id",
          "tracks"
        ],
        "properties": {
          "principal_id": {
            "type": "string"
          },
          "client_id": {
            "type": "string",
            "description": "The publishing connection, a u64 as a decimal string."
          },
          "tracks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RtcForwardingTrack"
            }
          }
        }
      },
      "RtcForwardingState": {
        "type": "object",
        "required": [
          "room_id",
          "in_call",
          "publishers"
        ],
        "properties": {
          "room_id": {
            "type": "string",
            "format": "uuid"
          },
          "in_call": {
            "type": "boolean",
            "description": "`false` when nobody is in the call; `publishers` is then empty."
          },
          "publishers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RtcForwardingPublisher"
            },
            "description": "Each connection that publishes, with what it publishes and who receives it. A connection that only receives appears as a subscriber, not here."
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, expired or invalid credentials",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        },
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          }
        }
      },
      "Forbidden": {
        "description": "Authenticated, but the token is not scoped to this room or principal",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        },
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          }
        }
      },
      "Blocked": {
        "description": "The direct conversation is frozen by a block (code `blocked`): one of the two people has blocked the other. Neither can send, react, type or start a call in it, and neither can open a new direct conversation with the other, until the block is lifted. History stays readable. Both sides get this answer; it does not say who blocked whom.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        },
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          }
        }
      },
      "ForbiddenOrBlocked": {
        "description": "Authenticated but not permitted here (code `forbidden`), or — in a direct turnkey-directory conversation — frozen by a block (code `blocked`): one of the two people has blocked the other, so neither sends, reacts, types or calls in it until the block is lifted. Branch on `code`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        },
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          }
        }
      },
      "NotFound": {
        "description": "No such room, message, member or attachment",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        },
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          }
        }
      },
      "Conflict": {
        "description": "Conflicts with current state",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        },
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          }
        }
      },
      "PayloadTooLarge": {
        "description": "Body exceeds MAX_UPLOAD_BYTES or the per-message limit",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        },
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        },
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          }
        }
      }
    },
    "headers": {
      "RetryAfter": {
        "description": "Whole seconds until the refused operation may be retried.",
        "schema": {
          "type": "integer",
          "format": "int64",
          "minimum": 1
        }
      },
      "RtcSessionNoStore": {
        "description": "Every response for the exact /experimental/rtc/session path is no-store, including disabled-route 404, errors, method rejection, preflight and caught panics.",
        "schema": {
          "type": "string",
          "const": "no-store"
        }
      },
      "XRequestId": {
        "description": "The id of this request. Yours, if you sent an `X-Request-Id`; otherwise a UUID the server assigned. Every log line the server keeps for the request carries it, so quote it to support.",
        "schema": {
          "type": "string"
        }
      }
    },
    "parameters": {
      "XRequestId": {
        "name": "X-Request-Id",
        "in": "header",
        "required": false,
        "description": "Your own id for this request. It is returned in the response's X-Request-Id header and attached to every log line kept for the request, so quote it when you contact support. Use printable ASCII. Without it, the server assigns a UUID.",
        "schema": {
          "type": "string"
        }
      }
    }
  },
  "paths": {
    "/experimental/rtc/session": {
      "post": {
        "summary": "Experimental read-only observation of the caller's exact accepted RTC session",
        "operationId": "read",
        "tags": [
          "rtc-experimental"
        ],
        "x-experimental": true,
        "description": "Not stable or GA. RTC_SESSION_CONTROL_ENABLED defaults to false; only literal true registers this route, otherwise POST returns 404. The socket must separately opt in with exactly one literal control=1 on /v1/rtc/ws (duplicate or encoded control keys/values do not enable it). Its private session.handle event follows initial peers and is never broadcast. This HTTP request requires Authorization: Bearer, rtc:subscribe and exact equality to the socket's stored claims: tid, rid, sub, did (absence differs from empty), jti, exp, iss, aud, role, permissions (including order), iat and nbf. WebSocket subprotocol credentials alone do not authenticate HTTP. A renewed JWT requires a new opted-in WebSocket and handle. Expiry is strict at exp, without verifier leeway. After exact ownership and nonblocking read permits, a fresh active-membership query must succeed before the media-thread exact-session/port/epoch/writer check; HTTP rechecks expiry and ownership before responding. Membership observation is NOT atomic with concurrent membership removal. accepted is admission only, not connected transport or media health. No mutations or room/source inventory. All responses for this exact path are Cache-Control: no-store. A shared two-second deadline covers the read; pressure/unavailability is 503, deadline expiry is 504.",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RtcSessionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Own exact session observed as accepted; decimal-string client_id, no media-health claim",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RtcSessionResponse" }
              }
            },
            "headers": {
              "Cache-Control": { "$ref": "#/components/headers/RtcSessionNoStore" },
              "X-Request-Id": { "$ref": "#/components/headers/XRequestId" }
            }
          },
          "400": {
            "description": "invalid_request: malformed JSON or not exactly one hyphenated UUID handle",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiError" }
              }
            },
            "headers": {
              "Cache-Control": { "$ref": "#/components/headers/RtcSessionNoStore" },
              "X-Request-Id": { "$ref": "#/components/headers/XRequestId" }
            }
          },
          "401": {
            "description": "unauthorized: missing/invalid bearer or token expired (no expiry leeway)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiError" }
              }
            },
            "headers": {
              "Cache-Control": { "$ref": "#/components/headers/RtcSessionNoStore" },
              "X-Request-Id": { "$ref": "#/components/headers/XRequestId" }
            }
          },
          "403": {
            "description": "forbidden: missing rtc:subscribe or no longer an active member",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiError" }
              }
            },
            "headers": {
              "Cache-Control": { "$ref": "#/components/headers/RtcSessionNoStore" },
              "X-Request-Id": { "$ref": "#/components/headers/XRequestId" }
            }
          },
          "404": {
            "description": "not_found: route disabled, unknown/non-owned handle, or session retired/no longer current; renewed JWT claims do not own an old handle",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiError" }
              }
            },
            "headers": {
              "Cache-Control": { "$ref": "#/components/headers/RtcSessionNoStore" },
              "X-Request-Id": { "$ref": "#/components/headers/XRequestId" }
            }
          },
          "413": {
            "description": "payload_too_large: body collection fails or exceeds 256 bytes",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiError" }
              }
            },
            "headers": {
              "Cache-Control": { "$ref": "#/components/headers/RtcSessionNoStore" },
              "X-Request-Id": { "$ref": "#/components/headers/XRequestId" }
            }
          },
          "503": {
            "description": "unavailable: engine/control lane unavailable, global/per-session read pressure, or membership lookup failure",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RtcSessionReadError" }
              }
            },
            "headers": {
              "Cache-Control": { "$ref": "#/components/headers/RtcSessionNoStore" },
              "X-Request-Id": { "$ref": "#/components/headers/XRequestId" }
            }
          },
          "504": {
            "description": "deadline_exceeded: shared two-second read budget expired",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RtcSessionReadError" }
              }
            },
            "headers": {
              "Cache-Control": { "$ref": "#/components/headers/RtcSessionNoStore" },
              "X-Request-Id": { "$ref": "#/components/headers/XRequestId" }
            }
          },
          "default": {
            "description": "Framework/middleware response; no JSON ApiError body is promised (for example a caught panic). The no-store header still applies.",
            "headers": {
              "Cache-Control": { "$ref": "#/components/headers/RtcSessionNoStore" }
            }
          }
        }
      }
    },
    "/v1/account/signup": {
      "post": {
        "summary": "Create a customer account (open self-serve) and its tenant; returns a session",
        "description": "Signup is open self-serve, with no email verification. The email is trimmed and lowercased and must contain exactly one `@`. The password must be 12 to 256 bytes. Anything else is a 400. The call creates a new tenant (`acct_…`) owned by the account and logs you straight in, returning the same 30-day `as_` session that `login` returns. Limited to 20 signups a minute per client IP and 400 a minute across all callers. Past that the answer is 429 `rate_limited` with `Retry-After`.",
        "operationId": "signup",
        "tags": [
          "account"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "password"
                ],
                "additionalProperties": false,
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "Your email address; it becomes the login. Stored lowercased. Not verified: there is no email step."
                  },
                  "password": {
                    "type": "string",
                    "minLength": 12,
                    "maxLength": 256,
                    "description": "12–256 characters. Stored only as an argon2 hash."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Session (token shown once)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountSession"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "An email without exactly one `@`, or a password outside 12–256 bytes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "409": {
            "description": "email already registered",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "description": "Over 20 signups a minute from this client address, or 400 a minute in all.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/account/login": {
      "post": {
        "summary": "Log in with email + password; returns a session",
        "description": "Each successful login creates a new, independent session valid for 30 days. Sessions from earlier logins keep working until they expire or are logged out. A wrong password and an unknown email get the same 401. An email that is not shaped like an address gets 400 before any lookup. Limited to 30 attempts a minute per client IP and 600 a minute across all callers (429 `rate_limited` with `Retry-After`).",
        "operationId": "login",
        "tags": [
          "account"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "password"
                ],
                "additionalProperties": false,
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "The email you signed up with."
                  },
                  "password": {
                    "type": "string",
                    "description": "Your password."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Session (token shown once)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountSession"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "The email is not shaped like an address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "description": "Over 30 attempts a minute from this client address, or 600 a minute in all.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/account/logout": {
      "post": {
        "summary": "Invalidate the presented account session",
        "description": "Deletes only the session in the `Authorization` header. The account's other sessions and its API keys are untouched. Afterwards that token answers 401 everywhere, including on a repeated logout.",
        "operationId": "logout",
        "tags": [
          "account"
        ],
        "security": [
          {
            "account_session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Whether a session was removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "logged_out": {
                      "type": "boolean",
                      "description": "True when this call removed the session. False only if a concurrent logout removed it first; either way the session no longer works."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/account/api-keys": {
      "get": {
        "summary": "List the account's API keys (never includes secrets)",
        "description": "Returns every key the account has minted, `live` and `test`, newest first. Revoked keys are included, with `revoked_at` set. Each entry carries `id`, `name`, `environment`, `plan`, `scopes`, `created_at`, `last_used_at`, `revoked_at` and `expires_at`. There is no pagination.",
        "operationId": "listKeys",
        "tags": [
          "account"
        ],
        "security": [
          {
            "account_session": []
          }
        ],
        "responses": {
          "200": {
            "description": "The account's keys",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "api_keys": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      },
                      "description": "The account's keys, without secrets."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      },
      "post": {
        "summary": "Mint an API key for the account's tenant (secret shown once)",
        "description": "`environment` must be `live` or `test`. A test key works in a separate sandbox namespace of your tenant and never sees live rooms, messages or usage. Omit `scopes` for the default set (`rooms:read`, `rooms:write`, `members:write`, `tokens:mint`, `messages:write`). You may also request `webhooks:manage`, `usage:read`, `rooms:delete` and `data:erase`. Any other scope name, or any other `environment`, is a 400. Keys made here are on the `default` plan and do not expire. Only a hash of the secret is stored, so `api_key` appears in this response and nowhere else.",
        "operationId": "createKey",
        "tags": [
          "account"
        ],
        "security": [
          {
            "account_session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "environment"
                ],
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Optional label for the key, for your own records."
                  },
                  "environment": {
                    "type": "string",
                    "enum": [
                      "live",
                      "test"
                    ],
                    "description": "`live` or `test`. A test key works in its own data namespace and never sees live data."
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "rooms:read",
                        "rooms:write",
                        "members:write",
                        "tokens:mint",
                        "messages:write",
                        "webhooks:manage",
                        "usage:read",
                        "rooms:delete",
                        "data:erase"
                      ]
                    },
                    "description": "Scopes to grant. Omit for the default set: rooms:read, rooms:write, members:write, tokens:mint, messages:write. `usage:read`, `webhooks:manage`, `rooms:delete` and `data:erase` must be asked for."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new credential (secret shown once)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountCreateApiKeyResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "An `environment` other than `live` or `test`, or an unknown scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/account/api-keys/{key_id}/rotate": {
      "post": {
        "summary": "Rotate a key: mint a replacement and revoke the old (secret shown once)",
        "description": "In one transaction, mints a replacement that carries the old key's name, environment, scopes, plan and expiry, and revokes the old key. The old secret is refused from its next request. Session tokens already minted with the old key stay valid until they expire. Not idempotent: if the response is lost, a retry gets 404 because the old key is already revoked. The replacement still exists and shows in `GET /v1/account/api-keys`, but its secret cannot be shown again, so revoke it and create a new key.",
        "operationId": "rotateKey",
        "tags": [
          "account"
        ],
        "security": [
          {
            "account_session": []
          }
        ],
        "parameters": [
          {
            "name": "key_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The key's public id (`ak_live_…` or `ak_test_…`), as returned when it was created or listed."
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "The rotated credential",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key_id": {
                      "type": "string",
                      "description": "The replacement key's id. The old key is revoked in the same step."
                    },
                    "api_key": {
                      "type": "string",
                      "description": "The replacement key, `ak_<env>_<id>.<secret>`. Shown once; store it now."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "not an owned, live key",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        }
      }
    },
    "/v1/account/api-keys/{key_id}": {
      "delete": {
        "summary": "Revoke a key the account owns",
        "description": "Sets the key's `revoked_at`. The key stays in the key list with that timestamp. The key's row is read on every request, so the next request made with this key gets 401. Session tokens it already minted stay valid until they expire. Revoking a key that is already revoked answers 404, which a retry can treat as done.",
        "operationId": "deleteKey",
        "tags": [
          "account"
        ],
        "security": [
          {
            "account_session": []
          }
        ],
        "parameters": [
          {
            "name": "key_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The key's public id (`ak_live_…` or `ak_test_…`)."
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "revoked": {
                      "type": "boolean",
                      "description": "True once the key no longer works."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "not an owned, live key",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        }
      }
    },
    "/v1/account/directory-apps": {
      "get": {
        "summary": "List the tenant's directory apps",
        "description": "Newest first. Each entry has `app_id`, `delivery_mode`, `delivery_webhook_url` (webhook mode only) and `created_at`. It also has `push_fcm` and `push_apns`, which say whether native-push credentials are stored. The delivery secret, SMS-provider credentials and push credentials are never returned.",
        "operationId": "listDirectoryApps",
        "tags": [
          "account"
        ],
        "security": [
          {
            "account_session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Directory apps",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "directory_apps": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      },
                      "description": "The account's directory apps. Secrets are never returned."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      },
      "post": {
        "summary": "Provision a public directory app_id (webhook OR SMS-provider delivery)",
        "description": "Set exactly one of `delivery_webhook_url` or `provider`. In webhook mode the response carries `delivery_secret`, shown only here. Each OTP is POSTed to your URL as JSON `{app_id, phone, code, expires_at}` with an `x-ollacore-signature: t=<unix seconds>,v1=<hex>` header, where `v1` is HMAC-SHA256 of `<t>.<body>` keyed with that secret. The URL is checked again on every delivery. In provider mode `provider.kind` is `twilio`, `plivo` or `msg91`. `provider.config` is checked for that vendor now, so a misconfiguration fails here rather than when a user asks for a code.",
        "operationId": "createDirectoryApp",
        "tags": [
          "account"
        ],
        "security": [
          {
            "account_session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "description": "Set exactly one of delivery_webhook_url (we POST the signed OTP) or provider (we call the customer's SMS vendor).",
                "properties": {
                  "delivery_webhook_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Webhook mode: where we POST each OTP, signed, for you to deliver. Must be a public `https://` URL: one whose address is private, loopback or link-local is refused with 400 here, and every delivery checks again. A name that doesn't resolve yet is accepted now and refused at delivery if it still doesn't. Set this or `provider`, not both."
                  },
                  "provider": {
                    "type": "object",
                    "required": [
                      "kind",
                      "config"
                    ],
                    "additionalProperties": false,
                    "properties": {
                      "kind": {
                        "type": "string",
                        "enum": [
                          "twilio",
                          "plivo",
                          "msg91"
                        ],
                        "description": "Which SMS vendor. `config` holds that vendor's credentials and is validated for it now."
                      },
                      "config": {
                        "type": "object",
                        "description": "Vendor-specific credentials; validated for kind at creation.",
                        "additionalProperties": true
                      }
                    },
                    "description": "Provider mode: your own SMS vendor account, which we call to send the OTP. Set this or `delivery_webhook_url`, not both. Credentials cannot be updated later; rotate by creating a new app."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new directory app",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "app_id": {
                      "type": "string",
                      "description": "The app's public id. Safe to ship in your mobile build; the directory endpoints take it to find your tenant."
                    },
                    "delivery_mode": {
                      "type": "string",
                      "enum": [
                        "webhook",
                        "provider"
                      ],
                      "description": "How OTPs reach users: `webhook` or `provider`."
                    },
                    "delivery_secret": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "HMAC secret, shown once. Webhook mode only; absent in provider mode."
                    },
                    "delivery_webhook_url": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The delivery webhook. Webhook mode only; absent in provider mode."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "Invalid request: both or neither of `delivery_webhook_url` and `provider`; a provider configuration of the wrong shape; or a `delivery_webhook_url` that is not `https://` or whose address is not public (private, loopback, link-local or otherwise reserved). A name that does not resolve yet is accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/account/directory-apps/{app_id}": {
      "delete": {
        "summary": "Delete a directory app the tenant owns",
        "description": "The `app_id` stops working at once: `otp/request` and `otp/verify` answer 404 for it. Users belong to the tenant, not the app, so users, conversations and existing `du_` sessions stay. Push tokens registered under the deleted app use the credentials of the tenant's only remaining directory app. They get no native push if the tenant has no other app or more than one.",
        "operationId": "deleteDirectoryApp",
        "tags": [
          "account"
        ],
        "security": [
          {
            "account_session": []
          }
        ],
        "parameters": [
          {
            "name": "app_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The directory app's public id."
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean",
                      "description": "True once the app is gone. Its users and data stay in the tenant."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "not an owned app",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        }
      }
    },
    "/v1/directory/otp/request": {
      "post": {
        "summary": "Request a phone OTP (delivered via the app's configured channel)",
        "description": "Sends a 6-digit code that is valid for 5 minutes. Only its hash is stored. `phone` must be E.164 (`+` and 8 to 15 digits), or the call is a 400. Four limits apply, each a 429 with `Retry-After` whose `message` names the limit: 300 requests a minute per app (`otp_app`), 5 a minute per phone number (`otp_phone`), 30 a minute per client IP (`otp_ip`), and at most 3 codes per number in 60 seconds (`otp_resend`). The code is delivered before the response: in webhook mode it is POSTed to your webhook with one retry after 500 ms, and in provider mode your SMS vendor is called. A delivery failure answers 500 `internal`. A retry issues a new code. The response is the same whether or not the number is already registered.",
        "operationId": "otpRequest",
        "tags": [
          "directory"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "app_id",
                  "phone"
                ],
                "additionalProperties": false,
                "properties": {
                  "app_id": {
                    "type": "string",
                    "description": "Your directory app's public id."
                  },
                  "phone": {
                    "type": "string",
                    "description": "E.164"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Dispatched",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "dispatched": {
                      "type": "boolean",
                      "description": "True when a code was handed to your webhook or SMS vendor. The answer is the same whether or not the number is registered."
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When the code stops working: 5 minutes after it was issued."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "`phone` is not E.164 (`+` and 8–15 digits).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "404": {
            "description": "unknown app",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "description": "A limit was reached; `message` names it: `otp_app` (300 a minute per app), `otp_phone` (5 a minute per number), `otp_ip` (30 a minute per client address) or `otp_resend` (3 codes per number in 60 s).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/directory/otp/verify": {
      "post": {
        "summary": "Verify a phone OTP; returns a directory session",
        "description": "Only the newest unexpired code for the number can be verified, so requesting a new code supersedes older ones. Each code allows 5 attempts, right or wrong, and works once. After that, or after it expires, every attempt is a 401 until the user requests a new code. On success the user is created if the number is new to the tenant, and a new `du_` session valid for 30 days is returned. Users belong to the tenant, so the same number reaches the same user through any of the tenant's apps. An unknown `app_id` is a 404, and a malformed `phone` is a 400.",
        "operationId": "otpVerify",
        "tags": [
          "directory"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "app_id",
                  "phone",
                  "code"
                ],
                "additionalProperties": false,
                "properties": {
                  "app_id": {
                    "type": "string",
                    "description": "Your directory app's public id."
                  },
                  "phone": {
                    "type": "string",
                    "description": "The same E.164 number the code was sent to."
                  },
                  "code": {
                    "type": "string",
                    "description": "The 6-digit code. At most 5 attempts per code; each code works once."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Session (token shown once)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "session_token": {
                      "type": "string",
                      "description": "The `du_…` directory session for this user, valid 30 days. Store it securely on the device."
                    },
                    "user_id": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The directory user's id. It is also their chat `principal_id`."
                    },
                    "display_name": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The user's display name, or null if not set yet."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "`phone` is not E.164.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "description": "No unexpired code for this number, a wrong code, a code already used, or one past its 5 attempts. Request a new code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "404": {
            "description": "Unknown `app_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/directory/logout": {
      "post": {
        "summary": "Revoke the presented directory session",
        "description": "Signs the end-user out. `du_` sessions otherwise live for 30 days with no way to withdraw them, so this is what makes a leaked token recoverable. Pass `all_devices` to revoke every session the user holds.",
        "operationId": "directoryLogout",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "all_devices": {
                    "type": "boolean",
                    "default": false,
                    "description": "Revoke every session for this user, not just the one presented."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed out",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "logged_out": {
                      "type": "boolean",
                      "description": "Always true: the session (or every session, with `all_devices`) no longer works."
                    },
                    "sessions_revoked": {
                      "type": "integer",
                      "description": "How many sessions were revoked."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/directory/me": {
      "get": {
        "summary": "The authenticated directory user",
        "description": "Returns the profile of the user the `du_` session belongs to. `id` is also the user's chat `principal_id`. `avatar_version` is the value to cache their picture by when fetching `GET /v1/directory/users/{user_id}/avatar`. Change the name or about line with `PATCH /v1/directory/me`.",
        "operationId": "meGet",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "responses": {
          "200": {
            "description": "The profile",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DirectoryProfile"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      },
      "patch": {
        "summary": "Update the authenticated user's display name and/or about line",
        "operationId": "mePatch",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "minProperties": 1,
                "properties": {
                  "display_name": {
                    "type": "string",
                    "maxLength": 128,
                    "description": "New display name, 1–128 characters after trimming."
                  },
                  "about": {
                    "type": "string",
                    "maxLength": 256,
                    "description": "New profile line, up to 256 characters after trimming; an empty string clears it."
                  }
                },
                "description": "At least one of the two."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The profile as stored",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DirectoryProfile"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "Nothing to update, a display name outside 1–128 characters, or an about line over 256.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "description": "A field left out is untouched. An empty `about` clears it. Both are checked before either is written. Answers the whole profile."
      },
      "delete": {
        "summary": "Delete your account",
        "description": "What an app store requires an app to offer. You leave every conversation as `leave` would — a group whose only owner you were passes to its longest-standing member, a conversation left empty closes, your live sockets close and each room hears `member.removed` — then every session you hold is revoked, on every device, and the same erasure as `POST /v1/server/principals/{principal_id}/erase` is queued: your messages, uploads, profile photo, phone number, push devices, blocks and invite links are deleted within moments. Irreversible; ask the user to confirm first. Signing in again with the same phone creates a new, empty account.",
        "operationId": "meDelete",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted: this session no longer authenticates; the erasure is under way",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "deleted"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "deleted": {
                      "type": "boolean",
                      "const": true,
                      "description": "Always true."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/directory/me/avatar": {
      "delete": {
        "summary": "Remove your profile photo",
        "description": "The photo and its thumbnail are deleted from storage and `avatar_version` becomes null. Idempotent.",
        "operationId": "meAvatarDelete",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "The profile, without a photo",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DirectoryProfile"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/directory/me/avatar/init": {
      "post": {
        "summary": "Start a profile-photo upload",
        "description": "Answers a presigned PUT for the bytes. PUT them, then call `complete`. Only image types (JPEG, PNG, WebP, GIF), at most 2 MiB; the bytes are sniffed at `complete`, so the declared type is a hint, not a promise. An upload never completed is deleted after an hour. Ten starts a minute per user.",
        "operationId": "meAvatarInit",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "declared_mime",
                  "byte_size"
                ],
                "properties": {
                  "declared_mime": {
                    "type": "string",
                    "enum": [
                      "image/jpeg",
                      "image/png",
                      "image/webp",
                      "image/gif"
                    ],
                    "description": "The image type you will PUT."
                  },
                  "byte_size": {
                    "type": "integer",
                    "format": "int64",
                    "minimum": 1,
                    "maximum": 2097152,
                    "description": "Exact size of the bytes, at most 2 MiB."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Where to PUT the bytes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AvatarUpload"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "`byte_size` outside 1..=2 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "415": {
            "description": "`declared_mime` is not an image type a profile photo may be.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/directory/me/avatar/complete": {
      "post": {
        "summary": "Make the uploaded bytes your profile photo",
        "description": "Synchronous: the object is read back, sniffed, decoded and thumbnailed before this answers, so the answer is final. On success the profile comes back with its new `avatar_version`, and the previous photo is deleted. Bytes that are not a decodable image are refused with 415 and the object is removed.",
        "operationId": "meAvatarComplete",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "upload_id"
                ],
                "properties": {
                  "upload_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "From `init`."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The profile with its new photo",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DirectoryProfile"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "The uploaded object is larger than 2 MiB (it is removed).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "Nothing has been uploaded for this `upload_id` yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "415": {
            "description": "The bytes are not an image, or could not be decoded (the object is removed).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        }
      }
    },
    "/v1/server/webhooks": {
      "post": {
        "summary": "Register a webhook endpoint",
        "description": "Requires `webhooks:manage`. `url` must be `https`, and its host must resolve within 3 seconds to public addresses only. Private, loopback, link-local and similar ranges are refused with 400, and the same check runs again on every delivery. `event_types` must name at least one supported type. A tenant can have at most 10 enabled endpoints, and creating one beyond that is refused with 409. The response returns the HMAC signing `secret` only this once; store it to verify `X-Ollacore-Signature`. An endpoint cannot be edited: to change one, create a new endpoint and delete the old one.",
        "operationId": "createWebhook",
        "tags": [
          "webhooks"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url",
                  "event_types"
                ],
                "additionalProperties": false,
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "https only"
                  },
                  "event_types": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "message.created",
                        "message.updated",
                        "message.deleted",
                        "receipt.delivered",
                        "receipt.read",
                        "reaction.added",
                        "reaction.removed",
                        "attachment.ready",
                        "attachment.failed",
                        "call.started",
                        "call.ended",
                        "member.added",
                        "member.removed",
                        "member.updated",
                        "rtc.policy.denied",
                        "rtc.device_conflict"
                      ]
                    },
                    "description": "The events to deliver, at least one. Any of the event types listed on the webhooks page."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Endpoint (secret shown once)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "The webhook's id (`whe_…`). Use it to list deliveries or disable the endpoint."
                    },
                    "url": {
                      "type": "string",
                      "description": "The endpoint deliveries are POSTed to."
                    },
                    "event_types": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "enum": [
                          "message.created",
                          "message.updated",
                          "message.deleted",
                          "receipt.delivered",
                          "receipt.read",
                          "reaction.added",
                          "reaction.removed",
                          "attachment.ready",
                          "attachment.failed",
                          "call.started",
                          "call.ended",
                          "member.added",
                          "member.removed",
                          "member.updated",
                          "rtc.policy.denied",
                          "rtc.device_conflict"
                        ]
                      },
                      "description": "The events this endpoint receives."
                    },
                    "secret": {
                      "type": "string",
                      "description": "The HMAC signing secret. Shown once: store it to verify the `X-Ollacore-Signature` header on every delivery. Each delivery is a JSON envelope `{version, id, type, tenant_id, room_id, occurred_at, data}` (`version` is 1) of at most 262144 bytes; see API.md, \"Webhooks\"."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "Invalid url or event type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "The tenant already has 10 enabled endpoints. Delete one first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "422": {
            "description": "A field the operation does not take, such as `tenant_id` (the tenant comes from the key).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      },
      "get": {
        "summary": "List webhook endpoints",
        "description": "Requires `webhooks:manage`. Returns every endpoint of the key's tenant, newest first. Disabled endpoints are included with `enabled: false`, whether they were disabled by `DELETE` or automatically after five consecutive failed deliveries. The signing secret is never returned.",
        "operationId": "listWebhooks",
        "tags": [
          "webhooks"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "responses": {
          "200": {
            "description": "Endpoints (no secrets)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "description": "The webhook's id (`whe_…`)."
                      },
                      "url": {
                        "type": "string",
                        "description": "The endpoint deliveries are POSTed to."
                      },
                      "event_types": {
                        "type": "array",
                        "items": {
                          "type": "string",
                          "enum": [
                            "message.created",
                            "message.updated",
                            "message.deleted",
                            "receipt.delivered",
                            "receipt.read",
                            "reaction.added",
                            "reaction.removed",
                            "attachment.ready",
                            "attachment.failed",
                            "call.started",
                            "call.ended",
                            "member.added",
                            "member.removed",
                            "member.updated",
                            "rtc.policy.denied",
                            "rtc.device_conflict"
                          ]
                        },
                        "description": "The events this endpoint receives."
                      },
                      "enabled": {
                        "type": "boolean",
                        "description": "False once the endpoint is disabled, by you or automatically after 5 deliveries in a row fail for good."
                      },
                      "created_at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "When the endpoint was created."
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/server/webhooks/{webhook_id}": {
      "parameters": [
        {
          "name": "webhook_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The webhook endpoint's id (`whe_…`)."
        }
      ],
      "delete": {
        "summary": "Disable a webhook endpoint (soft delete; history retained)",
        "description": "Requires `webhooks:manage`. Sets `enabled: false` on the endpoint. The endpoint and its delivery history stay listed, and it stops counting toward the 10-endpoint limit. Deliveries that are still pending or waiting for a retry are cancelled instead of sent, and a replay to the endpoint is refused with 409. A disabled endpoint cannot be enabled again; 404 if the id is unknown or the endpoint is already disabled.",
        "operationId": "deleteWebhook",
        "tags": [
          "webhooks"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "responses": {
          "200": {
            "description": "Disabled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "disabled": {
                      "type": "boolean",
                      "description": "True once the endpoint stops receiving. It is disabled, not deleted: its delivery history is kept."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/server/webhooks/{webhook_id}/deliveries": {
      "parameters": [
        {
          "name": "webhook_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The webhook endpoint's id (`whe_…`)."
        }
      ],
      "get": {
        "summary": "Recent delivery attempts for an endpoint",
        "operationId": "listWebhookDeliveries",
        "tags": [
          "webhooks"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "responses": {
          "200": {
            "description": "Deliveries",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "description": "The delivery's id (`whd_…`). Replay a failed one by id."
                      },
                      "event_id": {
                        "type": "string",
                        "description": "The event's id (`evt_…`), shared by every endpoint that received it. Deduplicate on it."
                      },
                      "event_type": {
                        "type": "string",
                        "description": "The event type delivered."
                      },
                      "status": {
                        "type": "string",
                        "description": "`pending` or `retrying` while in flight, `delivered` on a 2xx, `failed` after the last retry, `cancelled` if the endpoint was disabled first."
                      },
                      "attempt": {
                        "type": "integer",
                        "description": "Retries made so far, counting from 0: a delivery that succeeded first time shows 0. A failed attempt is retried after 30 s, 2 min, 10 min, 30 min and 2 h; the sixth attempt is the last."
                      },
                      "last_status_code": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "description": "The HTTP status of the last attempt, or null if it never got a response."
                      },
                      "last_error": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Why the last attempt failed, or null."
                      },
                      "created_at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "When the delivery was queued."
                      },
                      "updated_at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "When the delivery last changed state."
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "description": "Newest first. A delivery is kept until 30 days after it settles (delivered, failed or cancelled), then deleted; one still pending or retrying is kept until it settles. Erasing a room, a principal or the tenant deletes the deliveries of the events erased, and scheduled retention deletes those of the messages it expires.",
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/server/webhooks/{webhook_id}/deliveries/{delivery_id}/replay": {
      "parameters": [
        {
          "name": "webhook_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The webhook endpoint's id (`whe_…`)."
        },
        {
          "name": "delivery_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The delivery's id (`whd_…`), from the delivery log."
        }
      ],
      "post": {
        "summary": "Replay a delivery (re-enqueue)",
        "operationId": "replayWebhookDelivery",
        "tags": [
          "webhooks"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "responses": {
          "200": {
            "description": "Replayed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "replayed": {
                      "type": "boolean",
                      "description": "True once the delivery is queued again, from attempt 0."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "description": "Re-sends a stored delivery to its endpoint, with the same event id. Possible until 30 days after the delivery settled; after that, or once an erasure or retention removed it, the delivery no longer exists and this returns 404.",
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/server/usage": {
      "get": {
        "summary": "Metered usage totals for the credential tenant",
        "description": "Requires `usage:read`. Sums the tenant's metered events over `[from, to)`, which defaults to the 30 days ending now. The metrics are `message.sent`, one per new message (an idempotent repeat is not counted), and `attachment.bytes`, counted once an attachment is verified `ready` (failed uploads are not counted). A metric with no events in the window is left out. A `test` key sees only its sandbox's usage.",
        "operationId": "getUsage",
        "tags": [
          "server"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Window start (inclusive). Defaults to 30 days before `to`."
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Window end (exclusive). Defaults to now. Window may not exceed 366 days."
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "Per-metric totals over the window",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "from",
                    "to",
                    "usage"
                  ],
                  "properties": {
                    "from": {
                      "type": "string",
                      "format": "date-time",
                      "description": "Start of the window summed (inclusive). Defaults to 30 days before `to`."
                    },
                    "to": {
                      "type": "string",
                      "format": "date-time",
                      "description": "End of the window summed. Defaults to now; at most 366 days after `from`."
                    },
                    "usage": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "metric",
                          "total"
                        ],
                        "properties": {
                          "metric": {
                            "type": "string",
                            "description": "e.g. message.sent, attachment.bytes"
                          },
                          "total": {
                            "type": "integer",
                            "format": "int64",
                            "description": "The metric's total over the window."
                          }
                        }
                      },
                      "description": "One entry per metered metric with usage in the window: `message.sent` (count) and `attachment.bytes` (bytes)."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "Invalid window (from >= to, or exceeds 366 days)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/v1/server/rooms": {
      "post": {
        "summary": "Create a room in the credential tenant",
        "description": "Requires `rooms:write`. The room is created in the key's tenant (a `test` key's sandbox) and has no members; add them with `POST /v1/server/rooms/{room_id}/members`. With `external_ref` the call is idempotent per tenant: repeating it returns the existing room unchanged and ignores the other fields. Without `external_ref`, every call creates a new room. `e2ee` and `e2ee_scheme` (`mls` or `megolm`, which requires `e2ee: true`) are set at creation and cannot change. `expires_at` is informational only: nothing closes or deletes the room when it passes.",
        "operationId": "provisionRoom",
        "tags": [
          "server"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "created_by"
                ],
                "additionalProperties": false,
                "properties": {
                  "created_by": {
                    "type": "string",
                    "description": "The principal recorded as the room's creator, 1–128 characters. It does not make them a member; add members separately."
                  },
                  "external_ref": {
                    "type": "string",
                    "description": "Your own reference for the room. If a room with this `external_ref` already exists in the tenant, it is returned instead of a new one."
                  },
                  "expires_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Optional informational expiry. Stored and returned; nothing closes or deletes the room when it passes."
                  },
                  "e2ee": {
                    "type": "boolean",
                    "description": "Create an end-to-end encrypted room. The server then relays only ciphertext (`mls.*` kinds) and refuses search, reactions and previews. Cannot be changed later."
                  },
                  "e2ee_scheme": {
                    "type": "string",
                    "enum": [
                      "mls",
                      "megolm"
                    ],
                    "description": "Declare which client scheme the members will run: `mls` (primary) or `megolm`. Requires `e2ee: true` (400 otherwise); immutable; echoed on the room so clients load the right engine."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Room",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Room"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "A `created_by` of the wrong length, or an invalid `e2ee_scheme`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "Unknown field (e.g. a supplied tenant_id) rejected",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/server/rooms/{room_id}": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. A room in another tenant, or one being erased, is 404."
        }
      ],
      "get": {
        "summary": "Fetch a room in the credential tenant",
        "description": "Requires `rooms:read`. A room in another tenant, a room that does not exist and a room being erased all return 404. `status` is `active` or `closed`. `expires_at` is informational only.",
        "operationId": "getRoom",
        "tags": [
          "server"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "responses": {
          "200": {
            "description": "Room",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Room"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      },
      "delete": {
        "summary": "Erase (hard-delete) a room and all its data (GDPR)",
        "description": "Marks the room deleting and asynchronously purges its object-store blobs, then the stored copies of its events (webhook deliveries, pending fan-out and push work), then cascades the row delete. Requires scope rooms:delete.",
        "operationId": "deleteRoom",
        "tags": [
          "server"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "responses": {
          "200": {
            "description": "Erasure accepted; the room is being purged",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "deleting"
                  ],
                  "properties": {
                    "deleting": {
                      "type": "boolean",
                      "description": "True: the room is inaccessible now, and its rows and files are erased in the background."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/server/rooms/{room_id}/members": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. A room in another tenant, or one being erased, is 404."
        }
      ],
      "post": {
        "summary": "Add a room member",
        "operationId": "addRoomMember",
        "tags": [
          "server"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "principal_id"
                ],
                "additionalProperties": false,
                "properties": {
                  "principal_id": {
                    "type": "string",
                    "description": "Who to add, 1–128 characters. Any opaque id you choose; a removed member is restored."
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "owner",
                      "member",
                      "viewer"
                    ],
                    "description": "`owner`, `member` or `viewer`. Anything else is 400."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Member",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomMember"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "description": "A room holds at most 5,000 active members, on every plane; once it is full the add is refused with 409 and nothing changes. Restoring a removed member or changing an existing member's role never needs a seat."
      }
    },
    "/v1/server/rooms/{room_id}/members/{principal_id}": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. A room in another tenant, or one being erased, is 404."
        },
        {
          "name": "principal_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The member to remove."
        }
      ],
      "delete": {
        "summary": "Remove a room member",
        "description": "Requires `members:write`. Sets the membership's `left_at`; the room's history is kept, and adding the principal again restores the membership. The removal takes effect at once: the member's live chat and RTC sockets close with `4403`, and their HTTP requests and new session tokens are refused. A durable `member.removed` event goes to the room's connected clients and to webhooks subscribed to it. E2EE clients use this event as the cue to rotate keys. 404 if the principal is not an active member.",
        "operationId": "removeRoomMember",
        "tags": [
          "server"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "responses": {
          "200": {
            "description": "Removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "removed": {
                      "type": "boolean",
                      "description": "True if the principal was an active member and is now removed. Their open sockets close within 30 seconds."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/server/rooms/{room_id}/members/{principal_id}/mute": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. A room in another tenant, or one being erased, is 404."
        },
        {
          "name": "principal_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The member. 404 if they are not an active member of the room."
        },
        {
          "$ref": "#/components/parameters/XRequestId"
        }
      ],
      "put": {
        "summary": "Mute a room for a member",
        "description": "Silence push notifications from this room for one member — eight hours, one week, or until unmuted — on behalf of a user whose settings your own server holds (a turnkey-directory user does this for themselves with `PUT /v1/directory/conversations/{room_id}/mute`). Push only: live delivery and call rings are unaffected, and no event is emitted. Requires `members:write`.",
        "operationId": "muteRoomMember",
        "tags": [
          "server"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MuteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The member, with `muted_until` set.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomMember"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "A `duration` outside `8h|1w|always`, or an unknown field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Unmute a room for a member",
        "description": "Push notifications again for this member. Idempotent. Requires `members:write`.",
        "operationId": "unmuteRoomMember",
        "tags": [
          "server"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "responses": {
          "200": {
            "description": "The member, with no `muted_until`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomMember"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/server/rooms/{room_id}/members/{principal_id}/preferences": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. A room in another tenant, or one being erased, is 404."
        },
        {
          "name": "principal_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The member whose received layers are being chosen. 404 if they are not an active member of the room."
        },
        {
          "$ref": "#/components/parameters/XRequestId"
        }
      ],
      "put": {
        "summary": "Set a member's simulcast layer preferences",
        "description": "Which simulcast layer of which publisher's track this member should receive, chosen by your server instead of by the adaptive policy — a \"pin this speaker in high quality\" control. The whole list is replaced: an entry with `layer: auto` clears that track, an empty list clears everything. Each entry is keyed the way the member's client already sees the track: the publisher's principal id and the publisher's own track id, exactly the stream id and track id a `chatbox.msid-principal` socket reads off the msid. `low`, `medium` and `high` name the publisher's layers by measured bitrate (with two layers `medium` is the dearer); a pinned layer that stops is replaced by the dearest live layer below it and taken back when it returns. Applied on the media loop's next layer report, within about a second, to every connection the member has; the size cap the adaptive policy applies in large calls does not apply to a pin. Held for the member's session: a member who is not in the call yet, or whose last connection just closed, keeps the list for a 30 s grace, so a PeerConnection rebuild finds it and a member who left for good does not. Requires `members:write`.",
        "operationId": "setMemberPreferences",
        "tags": [
          "server"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MemberPreferencesRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What is now pinned for the member, sorted by publisher then track id (`auto` entries are never listed), and whether the member is in the call.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemberPreferences"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "More than 128 entries, a `publisher` outside 1..=128 characters, a `track_id` outside 1..=256 characters, or the same publisher and track listed twice.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "A `layer` outside `low|medium|high|auto`, a missing field, or an unknown field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        }
      },
      "get": {
        "summary": "Read a member's simulcast layer preferences",
        "description": "What is pinned for this member right now and whether they are in the call. An empty list for a member with no preference, which is every member by default. Requires `rooms:read`.",
        "operationId": "getMemberPreferences",
        "tags": [
          "server"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "responses": {
          "200": {
            "description": "The member's current preferences.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemberPreferences"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/server/rooms/{room_id}/members/{principal_id}/rtc-policy": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. A room in another tenant, or one being erased, is 404."
        },
        {
          "name": "principal_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The member the policy applies to. 404 if they are not an active member of the room."
        },
        {
          "$ref": "#/components/parameters/XRequestId"
        }
      ],
      "put": {
        "summary": "Set a member's RTC media policy",
        "description": "Replace the member's media policy — what they may publish and receive, who they see in the roster, plus the chat and attachment rights the same token would otherwise carry. A policy is a RESTRICTION set: effective rights are token ∩ the member's current role ∩ this document, never more. Whole-document replacement under the `revision` rules: a higher revision is stored; the same revision with an identical document (`subscribe.publishers` compared as a set) is a no-op; the same revision with a different document is 409 `revision_conflict`; a lower revision is 409 `stale_revision` — both carry `current_revision`. The sequence is monotonic for the member's lifetime: a `DELETE` leaves its revision as a tombstone the next PUT must exceed. Enforced where every request already checks membership: the next HTTP request, and the chat and RTC sockets within about a second (the member's live sockets are told within this request and re-read the row; the chat socket's 30-second recheck remains the fallback). Each enforcement reads role and policy from one membership-row snapshot: a role raised later cannot grant beyond the token, a role lowered removes rights on this same schedule. On the RTC socket a publication the policy refuses is answered inactive and reported as a structured `error` event; the offer completes normally. Stored on the membership row, so it survives removal and restoration; `DELETE` is the only way back to role-only behaviour. Requires `members:write`.",
        "operationId": "setMemberRtcPolicy",
        "tags": [
          "server"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RtcPolicyDocument"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stored document and how far it has reached the member's live legs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RtcPolicyState"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "`revision` below 1, an allowlist that is empty, longer than 64 or lists a publisher twice, `publishers` given with a mode other than `allowlist`, a publisher id outside 1..=128 characters, an unknown field, or a document over 16 KiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`stale_revision`: the revision is lower than the stored one. `revision_conflict`: the same revision as stored, with a different document. The body adds `current_revision`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RtcPolicyRevisionError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "get": {
        "summary": "Read a member's RTC media policy and whether it has been applied",
        "description": "The stored document with `stored_revision`, and the engine's view of the member's live legs: `applied_revision` is the revision every active leg runs (`null` with no leg — nothing live to apply, the stored revision applies at the next registration), `applied` is whether it equals the stored one, `active_legs` and `applied_legs` show a change still landing. 404 for a member with no policy: a policy that exists restricts, one that does not does not, and a reconciler must be able to tell the two apart. Requires `rooms:read`.",
        "operationId": "getMemberRtcPolicy",
        "tags": [
          "server"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "responses": {
          "200": {
            "description": "The stored document and its application state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RtcPolicyState"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "summary": "Remove a member's RTC media policy",
        "description": "Back to token ∩ current role from the next evaluation, on the same schedule as a policy change. A deletion is a write in the same revision sequence as PUT: `revision` (required) must be above the stored one and becomes the row's tombstone — 409 `stale_revision` below the stored revision, 409 `revision_conflict` at it, both carrying `current_revision`. Optionally `If-Match: \"<revision>\"` states the revision you believe is stored; if it is not, 409 `revision_conflict` with the actual one and nothing changes. After deletion GET answers 404; a retry of the same deletion (same `revision`) answers 204 again with no further effect, a DELETE below the tombstone is 409 `stale_revision`, and one above it is 404 `policy_deleted` carrying the tombstone as `current_revision`. The `If-Match` check and the write are one atomic step, so a concurrent PUT cannot slip between them. The next PUT must exceed the tombstone (at it: `revision_conflict`; below it: `stale_revision`; both report the tombstone as `current_revision`) — so a delayed write allocated before the deletion can never bring an obsolete policy back. The sequence never restarts for the member's lifetime. 404 when the member has no policy. Requires `members:write`.",
        "operationId": "deleteMemberRtcPolicy",
        "tags": [
          "server"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "parameters": [
          {
            "name": "revision",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64",
              "minimum": 1
            },
            "description": "This deletion's revision, from the same monotonic allocator as your PUTs; must be above the stored revision. Becomes the tombstone."
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Precondition: the revision you believe is stored, quoted or bare (`\"12\"`). 409 `revision_conflict` with `current_revision` when it is not."
          }
        ],
        "responses": {
          "204": {
            "description": "Removed; the revision remains as a tombstone. Also the answer to a retry of this same deletion (a tombstone already at this `revision`), with no further effect — so a client that lost the first 204 can retry safely.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "`revision` missing or not a positive integer, or an `If-Match` that is not a revision.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found`: not an active member, or no policy was ever written for them. `policy_deleted`: the policy was already deleted at a different revision than this request's (`current_revision` is that deletion's); this request's deletion did not happen.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ApiError"
                    },
                    {
                      "$ref": "#/components/schemas/RtcPolicyRevisionError"
                    }
                  ]
                }
              }
            }
          },
          "409": {
            "description": "`stale_revision`: `revision` is below the stored policy's or the tombstone's. `revision_conflict`: `revision` equals the stored policy's, or `If-Match` named a revision other than the stored one. `current_revision` says which.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RtcPolicyRevisionError"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/server/session-tokens": {
      "post": {
        "summary": "Mint a room-scoped end-user session token (tenant pinned to the key)",
        "description": "Requires `tokens:mint`. Mints a signed token for one principal in one room of the key's tenant; the caller cannot choose the tenant. `role` defaults to the member's stored role and cannot be above it, and `ttl_seconds` defaults to 600, clamped to 60–3600. `device_id` is carried in the token so each of a principal's devices gets its own call leg. The bundle also holds the chat and RTC WebSocket URLs and ICE servers with TURN credentials. A room whose `status` is `closed` is refused with 409. Revoking the key does not invalidate tokens it already minted; removing the member does.",
        "operationId": "createSessionToken",
        "tags": [
          "server"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "room_id",
                  "principal_id"
                ],
                "additionalProperties": false,
                "properties": {
                  "room_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The room the token is for. A token opens exactly one room."
                  },
                  "principal_id": {
                    "type": "string",
                    "description": "Who the token is for. Must be an active member of the room (403 otherwise)."
                  },
                  "role": {
                    "type": "string",
                    "description": "Optional: mint with fewer rights than the member has. At or below the member's stored role (a higher one is 403); anything but owner|member|viewer is 400. Omit to use the member's own role."
                  },
                  "ttl_seconds": {
                    "type": "integer",
                    "description": "Token lifetime in seconds. Defaults to 600 and is clamped to 60–3600."
                  },
                  "device_id": {
                    "type": "string",
                    "description": "Identifies one device of the principal. Required for the E2EE endpoints, and lets the same user join a call from two devices at once."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token bundle",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionTokenResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "`role` is not one of owner | member | viewer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The principal is not an active member of the room, the key lacks the `tokens:mint` scope, or `role` is above the member's stored role (a viewer cannot be minted an owner token).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The room is closed: no new tokens are minted for it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/server/principals": {
      "post": {
        "summary": "Register an authoritative non-human sender identity",
        "description": "Requires `members:write`. Records `principal_id` (1–128 characters) in the key's tenant as a non-human sender of kind `agent`, `bot`, `service` or `system`. Registering it again replaces the kind. Registration is what allows `POST /v1/server/rooms/{room_id}/messages` to send as this principal, and those messages carry its `sender_kind`; messages the principal sends with a session token do not. Registering does not add the principal to any room.",
        "operationId": "registerPrincipal",
        "tags": [
          "server"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "principal_id",
                  "kind"
                ],
                "additionalProperties": false,
                "properties": {
                  "principal_id": {
                    "type": "string",
                    "description": "The id to register, 1–128 characters."
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "agent",
                      "bot",
                      "service",
                      "system"
                    ],
                    "description": "`agent`, `bot`, `service` or `system`. Registering is what allows your server to send as this principal; a human cannot be registered."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Registered",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegisteredPrincipal"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "`kind` is not `agent`, `bot`, `service` or `system`, or `principal_id` is outside 1–128 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "A field the operation does not take, such as `tenant_id` (the tenant comes from the key).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/server/principals/{principal_id}/erase": {
      "parameters": [
        {
          "name": "principal_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The principal whose data to erase, across every room of your tenant."
        }
      ],
      "post": {
        "summary": "Erase a principal's data across the tenant (GDPR subject erasure)",
        "description": "Async: hard-deletes the principal's messages, memberships, uploaded attachment blobs, receipts, reactions, push subscriptions and E2EE key material; for a directory user, also their profile, OTP challenges and sessions. Every stored copy of their events goes too: the webhook delivery log and any fan-out or push work not yet done. Requires scope data:erase.",
        "operationId": "erasePrincipal",
        "tags": [
          "server"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "responses": {
          "200": {
            "description": "Erasure accepted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "erasing"
                  ],
                  "properties": {
                    "erasing": {
                      "type": "boolean",
                      "description": "True: the principal's data is being erased in the background (messages, files, memberships, receipts, reactions, push subscriptions, keys and any directory identity)."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/server/rooms/{room_id}/messages": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. A room in another tenant, or one being erased, is 404."
        }
      ],
      "post": {
        "summary": "Send a message as a non-human (agent/bot/service/system) principal",
        "description": "Requires `messages:write`. The sender must be registered with `POST /v1/server/principals` and be an active member of the room; any other principal, including any human, is refused with 403. The message carries `sender_kind` and goes through the same path as a client send: it is persisted, then published as `message.created` to the room's sockets, webhooks and push. It is idempotent per room and sender on `client_message_id` (1–128 characters): a repeat returns the original message and does not count against the quota again. `kind` is one of `text`, `image`, `voice`, `video`, `story`, `file` or `system`, with a body of at most 16 KiB. An e2ee room accepts only `mls.*` kinds, up to 128 KiB, and anything else is refused with 409. A message has at most 10 attachments, each already `ready` in this room, and at most 50 `mentions`, all active members. With `forward_of`, the server copies the source message's kind, body and media; the sender must be a member of the source room, and the destination must not be e2ee.",
        "operationId": "sendMessage",
        "tags": [
          "server"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "sender_principal_id",
                  "client_message_id"
                ],
                "additionalProperties": false,
                "properties": {
                  "sender_principal_id": {
                    "type": "string",
                    "description": "Who to send as. Must be a registered agent/bot/service/system principal and an active member of the room; sending as a human is refused (403)."
                  },
                  "client_message_id": {
                    "type": "string",
                    "description": "Your idempotency key, 1–128 characters. Resending it returns the original message."
                  },
                  "kind": {
                    "type": "string",
                    "description": "The message kind. Defaults to `text`."
                  },
                  "body": {
                    "type": "object",
                    "description": "The message content: any JSON value, at most 16 KiB serialised. The server does not require a shape; clients conventionally send an object such as `{\"text\": …}`, which is what previews and search read. Required unless `forward_of` is set. A `mentions` array of principal ids (at most 50, each an active member of the room, validated on send; 400 otherwise) names members: it is stored and delivered as sent, and a mentioned member is pushed even while they have muted the room. Not on `system` or `mls.*` messages."
                  },
                  "reply_to": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Id of a message in the same room this one replies to."
                  },
                  "attachment_ids": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "description": "Up to 10 `ready` attachments from this room."
                  },
                  "forward_of": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Forward a message instead of composing one: the id of a message in any room the sender principal is an active member of (this one included). The server copies its kind, body and media into this room — `kind`, `body` and `attachment_ids` in this request are ignored — and the copy carries `forward_depth` one deeper than the source. Only `text`, `image`, `voice`, `video`, `file` and `story` messages can be forwarded, not a deleted one, and not into an E2EE room (409). A source you cannot see — unknown, another tenant's, in a room you are not in — is one 400. `mentions` in the source do not travel. Media is copied inside the store as ready attachments of this room, uploaded by the sender."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Persisted message",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Message"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "A bad `kind`, `client_message_id`, `mentions`, `reply_to`, attachment or `forward_of`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "The tenant has used its monthly message quota (`quota_exceeded`). Retrying will not help until the quota resets at the start of the month (UTC) or the plan changes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenOrBlocked"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "description": "The body is over 16 KiB, or over 128 KiB for an `mls.*` kind.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/internal/v1/plans/{name}": {
      "put": {
        "summary": "Create or update a plan (rate tier + monthly usage quotas, operator only)",
        "description": "Replaces the whole plan. Optional fields left out become null: unlimited quota, no plan retention, no description. `name` is 1–64 characters, `rate_limit` and `rate_window_seconds` must be positive, and quotas and `retention_days` must be at least 0 or null; anything else is 400. Plans are global. The rate limit applies per key, from that key's next request. For each quota and for retention, a tenant gets the most generous plan among its live keys, and null counts as unlimited. A tenant's own retention setting overrides the plan's. Monthly quotas count `message.sent` and `attachment.bytes` per UTC calendar month.",
        "operationId": "putPlan",
        "tags": [
          "internal"
        ],
        "security": [
          {
            "hmac": []
          }
        ],
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The plan's name, 1–64 characters. The call creates the plan or replaces it."
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "rate_limit",
                  "rate_window_seconds"
                ],
                "properties": {
                  "rate_limit": {
                    "type": "integer",
                    "format": "int64",
                    "description": "Requests allowed per window, per API key on this plan. Must be positive."
                  },
                  "rate_window_seconds": {
                    "type": "integer",
                    "format": "int64",
                    "description": "The rate-limit window in seconds. Must be positive."
                  },
                  "monthly_message_quota": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "format": "int64",
                    "description": "Monthly message cap; omit or null for unlimited."
                  },
                  "monthly_byte_quota": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "format": "int64",
                    "description": "Monthly attachment-byte cap; omit or null for unlimited."
                  },
                  "retention_days": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "format": "int64",
                    "description": "Auto-expire messages older than this many days; omit or null to keep forever."
                  },
                  "description": {
                    "type": "string",
                    "description": "Optional human description of the plan."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The upserted plan name",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "plan": {
                      "type": "string",
                      "description": "The plan's name."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "A `name` outside 1–64 characters, a rate limit or window that is not positive, or a negative quota or retention.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/internal/v1/api-keys": {
      "post": {
        "summary": "Provision a customer server API key (operator only)",
        "description": "`environment` is `live` or `test`. A `test` key works in a separate `{tenant_id}::test` sandbox with its own rooms, messages and usage, so `tenant_id` may not contain `::`. Each scope must be one of `rooms:read`, `rooms:write`, `members:write`, `tokens:mint`, `messages:write`, `webhooks:manage`, `usage:read`, `rooms:delete` or `data:erase`. `plan` defaults to `default` and must already exist. Any invalid value is 400. The tenant, and for a `test` key its sandbox, is registered if new. `api_key` (`ak_<env>_<id>.<secret>`) is returned only once, because the server stores only a SHA-256 digest of the secret. After `expires_at` the key is refused with 401.",
        "operationId": "createApiKey",
        "tags": [
          "internal"
        ],
        "security": [
          {
            "hmac": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "tenant_id",
                  "environment",
                  "scopes"
                ],
                "properties": {
                  "tenant_id": {
                    "type": "string",
                    "description": "The tenant the key acts on, 1–128 characters. Must not contain `::`, which is reserved for the test namespace."
                  },
                  "name": {
                    "type": "string",
                    "description": "Optional label for the key."
                  },
                  "environment": {
                    "type": "string",
                    "enum": [
                      "live",
                      "test"
                    ],
                    "description": "`live` or `test`. A test key works in the `{tenant_id}::test` namespace."
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "rooms:read",
                        "rooms:write",
                        "members:write",
                        "tokens:mint",
                        "messages:write",
                        "webhooks:manage",
                        "usage:read",
                        "rooms:delete",
                        "data:erase"
                      ]
                    },
                    "description": "The scopes to grant. Each must be a known scope."
                  },
                  "expires_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Optional expiry. After it the key is refused as if revoked."
                  },
                  "plan": {
                    "type": "string",
                    "description": "Rate-tier plan name (see the plans table); defaults to 'default'."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new credential (secret shown once)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateApiKeyResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "A bad `environment`, scope, `plan` or `tenant_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/internal/v1/api-keys/{key_id}": {
      "parameters": [
        {
          "name": "key_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The API key's public id (`ak_live_…` or `ak_test_…`)."
        }
      ],
      "delete": {
        "summary": "Revoke a customer server API key (operator only)",
        "description": "Takes effect on the key's next request, which is refused with 401. 404 if the id is unknown or the key is already revoked. Session tokens the key already minted stay valid until they expire, at most one hour. After revocation the key's plan no longer counts toward the tenant's quotas or retention window.",
        "operationId": "revokeApiKey",
        "tags": [
          "internal"
        ],
        "security": [
          {
            "hmac": []
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "revoked": {
                      "type": "boolean",
                      "description": "True once the key no longer works."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/internal/v1/tenants/{tenant_id}": {
      "parameters": [
        {
          "name": "tenant_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The tenant to erase."
        }
      ],
      "delete": {
        "summary": "Purge an entire tenant (operator offboarding, GDPR)",
        "description": "Async, in two steps. At once, every way in is revoked: the tenant's account (and its sessions), API keys, directory apps, pending OTP challenges and directory sessions. About 45 seconds later — long enough for any request already in flight to finish — every room is purged (blobs, rows and every stored copy of its events), then the rest of the tenant's data: usage ledger, webhooks and their delivery log, push subscriptions, identities, the directory, and E2EE key material. Most destructive; HMAC-only.",
        "operationId": "purgeTenant",
        "tags": [
          "internal"
        ],
        "security": [
          {
            "hmac": []
          }
        ],
        "responses": {
          "200": {
            "description": "Purge accepted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "purging"
                  ],
                  "properties": {
                    "purging": {
                      "type": "boolean",
                      "description": "True: every room and all of the tenant's data are being erased in the background. Irreversible."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/rooms/{room_id}/messages/search": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member."
        }
      ],
      "get": {
        "summary": "Full-text search within one room (PostgreSQL FTS). Rejected in e2ee rooms — the server holds only ciphertext.",
        "description": "Needs `chat:history`. `q` must be 1–256 bytes after trimming. The search runs against `body.text` of messages that are not deleted, using PostgreSQL's `simple` configuration: there is no stemming and every word must match. Results are ordered by relevance, then newest first. `limit` defaults to 50 and is capped at 100, and there is no cursor for further pages. Results include attachments but not reactions.",
        "operationId": "searchMessages",
        "tags": [
          "messages"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "search terms"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Maximum results, 1–100. Defaults to 50."
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "Matching messages",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessagePage"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "`q` is empty or longer than 256 bytes after trimming.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "room is e2ee; the server cannot search ciphertext",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/rooms/{room_id}/unread": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member."
        }
      ],
      "get": {
        "summary": "Unread count and read cursor for the calling principal in this room.",
        "description": "Needs only active membership. `unread_count` counts messages after `last_read_seq` that someone else sent and that are not deleted. `last_read_seq` moves only forward, and only through read receipts. `latest_seq` is the room's newest `event_seq`. E2EE rooms work the same way.",
        "operationId": "unread",
        "tags": [
          "receipts"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Unread state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnreadResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/rooms/{room_id}/messages/{message_id}/reactions": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member."
        },
        {
          "name": "message_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The message's id, in this room."
        }
      ],
      "post": {
        "summary": "Add an emoji reaction. Idempotent: re-adding the same emoji returns changed=false. Rejected in e2ee rooms.",
        "description": "Needs `chat:send`. `emoji` is trimmed and must be 1–8 characters and at most 32 bytes. A message holds at most 20 distinct emoji; once it has 20, every add returns 400. When the set changes, a durable `reaction.added` event `{ message_id, event_seq, principal_id, emoji }` goes to the room's sockets and to subscribed webhooks; a repeat publishes nothing. In an e2ee room, clients send reactions as encrypted `mls.application` messages instead. In a direct conversation frozen by a block, the call returns 403 `blocked`.",
        "operationId": "addReaction",
        "tags": [
          "messages"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReactionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Whether the reaction set changed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReactionResult"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenOrBlocked"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "room is e2ee",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      },
      "delete": {
        "summary": "Remove a reaction previously added by the calling principal. Idempotent.",
        "description": "Needs `chat:send`. It removes only the caller's own reaction with that `emoji`, which follows the same 1–8 character rule as adding. When a reaction was removed, a durable `reaction.removed` event goes to the room's sockets and to subscribed webhooks. Removing a reaction that is not there returns `changed: false` and publishes nothing. Like adding, it returns 409 in an e2ee room and 403 `blocked` in a direct conversation frozen by a block.",
        "operationId": "removeReaction",
        "tags": [
          "messages"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReactionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Whether the reaction set changed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReactionResult"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenOrBlocked"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The room is e2ee.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/rooms/{room_id}/attachments/init-multipart": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member."
        }
      ],
      "post": {
        "summary": "Begin a multipart upload for a large file. Returns one presigned URL per part; parts may be uploaded in parallel and retried individually.",
        "description": "Needs `attachment:upload`, and applies the same `declared_mime`, `original_name`, e2ee and monthly byte quota rules as `init`. `byte_size` must be over 5 MiB and at most the deployment's `MAX_UPLOAD_BYTES` (400 otherwise); smaller files use `init`. `part_size` defaults to 16 MiB and is clamped to 5–64 MiB. An upload may have at most 512 parts; going over returns 400 asking for a larger `part_size`. The file is split into `part_size` chunks, one per URL in order, with a shorter last chunk. Keep the `ETag` that each part's PUT returns, because `complete-multipart` needs it. All part URLs share one `expires_at`, 5 minutes to 2 hours depending on size. An upload not completed within 6 hours is aborted and deleted.",
        "operationId": "initMultipart",
        "tags": [
          "attachments"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InitMultipartRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Upload id and presigned part URLs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InitMultipartResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "`byte_size` not over 5 MiB or over the deployment's upload limit, more than 512 parts at this `part_size`, or a bad `original_name`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "This upload would take the tenant past its monthly byte quota (`quota_exceeded`). Retrying will not help until the quota resets at the start of the month (UTC) or the plan changes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "413": {
            "description": "The request body is over 512 KiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "415": {
            "description": "declared type not permitted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/rooms/{room_id}/attachments/{attachment_id}/complete-multipart": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member."
        },
        {
          "name": "attachment_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The attachment's id, as returned when the upload began."
        }
      ],
      "post": {
        "summary": "Finish a multipart upload by supplying every part ETag. Server-side verification (size, checksum, magic bytes) runs before the attachment becomes referenceable.",
        "description": "Only the uploader may complete an upload, and only while the attachment is still `pending`. Unlike `complete`, a retry after a successful completion returns 409. `parts` lists 1–512 `{ part_number, etag }` pairs. An empty or oversized list returns 400; a list the object store rejects, such as one with a missing part or a wrong ETag, returns 409. The reassembled object must match the declared `byte_size` exactly (409 otherwise). The attachment then becomes `uploaded` and goes through the same verification as a single-part upload, ending in a durable `attachment.ready` or `attachment.failed` event.",
        "operationId": "completeMultipart",
        "tags": [
          "attachments"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompleteMultipartRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Attachment accepted for verification: the id and `uploaded` — not the full attachment, which arrives with `attachment.ready`. Not idempotent: completing again once the upload has left `pending` answers 409.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompleteMultipartResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "An empty `parts` list, or one over 512.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The attachment is not a pending multipart upload (so a retry after success is a 409), the store rejected the part list, or the assembled size differs from `byte_size`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/push/subscriptions": {
      "post": {
        "summary": "Register a Web Push subscription for the calling principal. Upserted by endpoint.",
        "description": "The token's principal must still be an active member of the token's room. `endpoint` must be an `https` URL of at most 1024 characters whose host is not a private, loopback or link-local IP literal (400 otherwise). Host names are resolved and checked again each time a push is sent. The subscription belongs to the principal, not to one room or device. Subscribing the same `endpoint` again replaces its keys and clears any earlier delivery failure. The subscription receives pushes for new messages in the principal's rooms when they have no chat socket open to that room (at most one per room every 30 seconds), and for call rings. Web Push only works when the deployment has a VAPID key configured (see `GET /v1/push/vapid-public-key`).",
        "operationId": "pushSubscribe",
        "tags": [
          "push"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PushSubscribeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Registered",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "subscribed"
                  ],
                  "properties": {
                    "subscribed": {
                      "type": "boolean",
                      "description": "Always true on success."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "`endpoint` is not `https`, is over 1024 characters, or names a private, loopback or link-local address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The principal is no longer an active member of the token's room.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      },
      "delete": {
        "summary": "Remove a Web Push subscription. Subscriptions rejected by the push service (404/410) are removed automatically as well.",
        "description": "Deletes only the caller's own subscription for that `endpoint` and returns `removed`. Deleting a subscription that does not exist returns `removed: false`, so the call is safe to retry. It checks the token but not membership, so a principal removed from the room can still delete its subscription.",
        "operationId": "pushUnsubscribe",
        "tags": [
          "push"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "endpoint"
                ],
                "properties": {
                  "endpoint": {
                    "type": "string",
                    "format": "uri",
                    "description": "The subscription's endpoint URL. Only your own subscription is removed; the call works even after you leave the room, while your token is still valid."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "removed"
                  ],
                  "properties": {
                    "removed": {
                      "type": "boolean",
                      "description": "True if a subscription owned by the caller matched the endpoint and was deleted; false if there was nothing to delete."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/push/vapid-public-key": {
      "get": {
        "summary": "VAPID application server public key, for PushManager.subscribe(). Corresponds to the private key the server signs with.",
        "description": "Needs no authentication. Pass `vapid_public_key` as `applicationServerKey` to `PushManager.subscribe()`, then register the resulting subscription with `POST /v1/push/subscriptions`. A 404 means Web Push is not enabled on this deployment.",
        "operationId": "vapidPublicKey",
        "tags": [
          "push"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Base64url-encoded P-256 public key",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "vapid_public_key"
                  ],
                  "properties": {
                    "vapid_public_key": {
                      "type": "string",
                      "description": "The server's VAPID public key. Pass it as `applicationServerKey` to `pushManager.subscribe`."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "404": {
            "description": "push not configured on this deployment",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/keypackages": {
      "post": {
        "summary": "MLS: upload key packages for the calling device — regular ones, or its single last-resort package. Opaque blobs; the server never interprets them.",
        "description": "The token's principal must still be an active member of the token's room. `packages` holds 1–50 base64 blobs, each decoding to between 1 byte and 64 KiB (400 otherwise). They are stored under `device_id`, which defaults to `default` when omitted and is not taken from the token. A package already on file (same SHA-256) is skipped, so `stored` counts only new packages. With `last_resort: true` the request must carry exactly one package, which replaces the device's previous last-resort package. `expires_in_seconds` (1 to 31,536,000, one year) sets how long the server keeps the package: after that it is never served and is swept. If omitted, the package is kept until it is consumed.",
        "operationId": "uploadKeyPackages",
        "tags": [
          "keypackages"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UploadKeyPackagesRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Count stored",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "stored"
                  ],
                  "properties": {
                    "stored": {
                      "type": "integer",
                      "description": "How many key packages were stored."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "No packages or more than 50, a blob that is not base64 or decodes outside 1 byte–64 KiB, a last-resort upload with other than one package, or `expires_in_seconds` out of range.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The principal is no longer an active member of the token's room.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "413": {
            "description": "The request body is over 512 KiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/keypackages/count": {
      "get": {
        "summary": "MLS: remaining unconsumed key packages per device, so a client knows when to replenish.",
        "description": "The token's principal must still be an active member of the token's room. For each of the caller's devices that has packages on file, `remaining` counts the unexpired, unconsumed regular packages, and `last_resort` says whether an unexpired last-resort package is held. A device with nothing left does not appear in `devices`. When `remaining` reaches zero, consumers receive the reusable last-resort package, so replenish before that.",
        "operationId": "keyPackageCount",
        "tags": [
          "keypackages"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Per-device remaining counts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "devices"
                  ],
                  "properties": {
                    "devices": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "device_id",
                          "remaining"
                        ],
                        "properties": {
                          "device_id": {
                            "type": "string",
                            "description": "The device."
                          },
                          "remaining": {
                            "type": "integer",
                            "description": "How many unconsumed, unexpired regular key packages it has left. The last-resort package is not counted here."
                          },
                          "last_resort": {
                            "type": "boolean",
                            "description": "Whether an unexpired last-resort package is on file for this device. Always present."
                          }
                        }
                      },
                      "description": "Your devices that have key packages on the server."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The principal is no longer an active member of the token's room.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/rooms/{room_id}/mls/group-info": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member."
        },
        {
          "$ref": "#/components/parameters/XRequestId"
        }
      ],
      "put": {
        "summary": "MLS: publish the group's latest GroupInfo and, optionally, its ratchet tree after a commit. Opaque to the server; an older epoch than the published one is refused.",
        "description": "Any active member may publish; a room that is not e2ee returns 409. `epoch` must be 0 or more. `group_info` (base64, up to 64 KiB) and the optional `ratchet_tree` (base64, up to 256 KiB) are stored as a single row per room that each publish replaces. Omitting `ratchet_tree` clears any previously stored tree. Oversized or non-base64 blobs return 400. Publishing the same `epoch` again is accepted, so a committer can retry safely. Nothing is published to the room's sockets.",
        "operationId": "putGroupInfo",
        "tags": [
          "mls"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PutGroupInfoRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Published",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "epoch"
                  ],
                  "properties": {
                    "epoch": {
                      "type": "integer",
                      "format": "int64",
                      "description": "The epoch now published."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "A blob that is not base64 or is over its limit (64 KiB GroupInfo, 256 KiB ratchet tree), or a negative `epoch`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "description": "The request body is over 512 KiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "get": {
        "summary": "MLS: the room's latest published GroupInfo and ratchet tree, for a joiner that needs the tree or an external join.",
        "description": "Any active member may read it. Returns the stored `epoch`, the base64 `group_info` and `ratchet_tree`, and `updated_by` and `updated_at` for the latest publish. `ratchet_tree` is null when the publisher omitted it. The call returns 404 until a member has published GroupInfo for this room, which means a room that is not e2ee always returns 404.",
        "operationId": "getGroupInfo",
        "tags": [
          "mls"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "The latest published state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GroupInfoResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/rooms/{room_id}/keypackages/{principal_id}": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member."
        },
        {
          "name": "principal_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "Whose key package to consume. They must share an active room with the caller."
        }
      ],
      "get": {
        "summary": "MLS: fetch and CONSUME one key package per device of the target principal. Atomic — concurrent callers never receive the same regular package; a device whose pool is dry serves its last-resort package unconsumed.",
        "description": "Both the caller and the target principal must be active members of this room (403 otherwise). For each of the target's devices, the oldest unexpired regular package is consumed and returned. A device with none left returns its last-resort package with `last_resort: true`; that package stays on file and can be served again. Expired packages are never served. The call returns 404 when none of the target's devices has any package.",
        "operationId": "consumeKeyPackages",
        "tags": [
          "keypackages"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "One package per device",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConsumeKeyPackagesResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller or the target is not an active member of this room.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/internal/v1/principals/{principal_id}/unread": {
      "parameters": [
        {
          "name": "principal_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The principal whose unread counts to return."
        }
      ],
      "get": {
        "summary": "Per-room unread counts for one principal across a tenant. Public tokens are room-scoped by design, so the cross-room badge is a control-plane concern.",
        "description": "`tenant_id` is required. Lists every room where the principal is an active member, including rooms with no unread messages. A room's count is the messages after the principal's read cursor, excluding their own and deleted messages, and `total` is the sum. A principal with no memberships gets `total: 0` and an empty list.",
        "operationId": "principalUnread",
        "tags": [
          "internal"
        ],
        "security": [
          {
            "hmac": []
          }
        ],
        "parameters": [
          {
            "name": "tenant_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The tenant the principal belongs to."
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "Total and per-room counts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "total",
                    "rooms"
                  ],
                  "properties": {
                    "total": {
                      "type": "integer",
                      "format": "int64",
                      "description": "Unread messages across all the principal's rooms, for an app-icon badge."
                    },
                    "rooms": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "room_id",
                          "unread_count"
                        ],
                        "properties": {
                          "room_id": {
                            "type": "string",
                            "format": "uuid",
                            "description": "The room."
                          },
                          "unread_count": {
                            "type": "integer",
                            "format": "int64",
                            "description": "Messages after the principal's read cursor in that room."
                          }
                        }
                      },
                      "description": "Per-room unread counts."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/e2ee/keys/upload": {
      "post": {
        "summary": "E2EE transport: upload device/one-time/fallback keys (Matrix /keys/upload shape; opaque blobs). Token must carry an explicit device_id (did claim).",
        "description": "Limited to 30 calls per principal per 10 seconds, and the principal must still be an active member of the token's room. `device_keys` replaces the calling device's identity keys. The first upload for a new device bumps the principal's device-list version, so co-members see the principal in `device_list_changes`. Each call takes up to 100 `one_time_keys` and 8 `fallback_keys`, and each key blob may be at most 64 KiB serialized (400 otherwise). A one-time key whose id is already stored is ignored, and a fallback key replaces the device's previous fallback key for that algorithm.",
        "operationId": "keysUpload",
        "tags": [
          "e2ee"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "device_keys": {
                    "type": "object",
                    "description": "The device's identity keys, in the Matrix `/keys/upload` shape. At most 64 KiB."
                  },
                  "one_time_keys": {
                    "type": "object",
                    "description": "One-time keys to add, keyed `<algorithm>:<key id>` (Matrix shape). At most 100 per upload."
                  },
                  "fallback_keys": {
                    "type": "object",
                    "description": "Fallback keys, served when the one-time keys run out (Matrix shape)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Remaining one-time key count for the calling device",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeysUploadResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "missing did claim / oversized blob",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/e2ee/keys/query": {
      "post": {
        "summary": "E2EE transport: device keys of users (Matrix /keys/query shape). Requires a shared active room with every queried user.",
        "description": "Limited to 60 calls per principal per 10 seconds. The token must carry an explicit `device_id` (400 otherwise). Every principal in the query must be the caller or share an active room with the caller; if any one does not, the whole request returns 403. The response contains all devices of each principal, whatever device list the request names. A principal with no uploaded keys is left out of the result.",
        "operationId": "keysQuery",
        "tags": [
          "e2ee"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "device_keys"
                ],
                "properties": {
                  "device_keys": {
                    "type": "object",
                    "description": "principal -> {} (device filter ignored; all devices returned)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Device keys, keyed by principal then device id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeysQueryResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "The token was minted without an explicit `device_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "no shared room",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/e2ee/keys/claim": {
      "post": {
        "summary": "E2EE transport: claim one one-time key per device (Matrix /keys/claim shape); atomic consume, fallback key served when the pool is dry. Same shared-room gate as query.",
        "description": "Limited to 120 calls per principal per 10 seconds. The token must carry an explicit `device_id` (400 otherwise). For each requested device, the oldest unclaimed one-time key is deleted and returned. When none is left, the device's newest fallback key is returned instead and kept on file. It is marked as used, so it no longer appears in its owner's `unused_fallback_key_algorithms`, which tells the owner to replace it. Devices with no key at all are left out of the result, and the algorithm named in the request is ignored.",
        "operationId": "keysClaim",
        "tags": [
          "e2ee"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "one_time_keys"
                ],
                "properties": {
                  "one_time_keys": {
                    "type": "object",
                    "description": "principal -> device -> algorithm"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Claimed one-time keys, keyed by principal then device id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeysClaimResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "The token was minted without an explicit `device_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "no shared room",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/e2ee/todevice": {
      "get": {
        "summary": "E2EE transport sync: to-device batch (ack-by-next-token deletion, crash-safe), one-time key counts, unused-fallback flag, device-list changes.",
        "description": "The token must carry an explicit `device_id` (400 otherwise), and only that device's inbox is returned. Pass the previous response's `next_token` as `ack`: events up to it are deleted and the next batch of at most 100 events is returned, oldest first. An unacknowledged batch is therefore served again after a crash. Each event carries the server-stamped `sender` principal, its `type` and its `content`. Pass the previous `device_list_watermark` as `device_watermark`; `device_list_changes` then lists the caller and any co-members whose devices changed since. Undelivered events are purged after 30 days.",
        "operationId": "todeviceFetch",
        "tags": [
          "e2ee"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "ack",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int64",
              "default": 0
            },
            "description": "next_token of the last fully processed batch"
          },
          {
            "name": "device_watermark",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int64",
              "default": 0
            },
            "description": "The `device_list_watermark` from your previous fetch; 0 (the default) on the first."
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "Pending to-device events plus key and device-list state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToDeviceFetchResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "The token was minted without an explicit `device_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The principal is no longer an active member of the token's room.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "summary": "E2EE transport: queue device-addressed payloads (Matrix /sendToDevice shape). Sender server-stamped from token; device \"*\" expands to all known devices; recipients must share an active room.",
        "description": "Limited to 60 calls per principal per 10 seconds. The token must carry an explicit `device_id` (400 otherwise). `event_type` must be 1–128 characters and each device's `content` at most 64 KiB. After `*` is expanded to every device the recipient has uploaded keys for, one request may queue at most 500 rows (400 otherwise). If any recipient is neither the caller nor shares an active room with the caller, the whole request returns 403. Queued events wait in each device's inbox until that device fetches and acknowledges them through `GET /v1/e2ee/todevice`; no socket event announces them. Events never fetched are purged after 30 days.",
        "operationId": "todeviceSend",
        "tags": [
          "e2ee"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "event_type",
                  "messages"
                ],
                "properties": {
                  "event_type": {
                    "type": "string",
                    "description": "The to-device event type, 1–128 characters, for example `m.room.encrypted`."
                  },
                  "messages": {
                    "type": "object",
                    "description": "principal -> device|* -> content"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Number of per-device rows queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToDeviceSendResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "The token was minted without an explicit `device_id`. Or an `event_type` outside 1–128 characters, a `content` over 64 KiB, or more than 500 rows once `*` is expanded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/rooms/{room_id}/messages": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member."
        }
      ],
      "get": {
        "summary": "Message history (ascending event_seq)",
        "description": "Needs `chat:history`. `limit` defaults to 100 and is capped at 500. `after_seq` returns the messages after that `event_seq` (exclusive), oldest first. `before_seq` on its own returns the `limit` messages just before it, still oldest first, for scrolling back. With both set, the page starts after `after_seq` and stops before `before_seq`. Unlike the WebSocket `catchup`, this endpoint adds no overlap window. Deleted messages stay in the page as tombstones (`kind` `deleted`, `body` null). Each message includes its attachments and reaction aggregates, and `latest_seq` is the room's newest `event_seq`. In an e2ee room the bodies are the stored `mls.*` ciphertext.",
        "operationId": "listMessages",
        "tags": [
          "messages"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "after_seq",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Only messages with `event_seq` greater than this. After a reconnect, pass the last `event_seq` you processed. With neither cursor, the page starts at the room's oldest message; with `before_seq` alone, it pages backwards (see `before_seq`)."
          },
          {
            "name": "before_seq",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Only messages with `event_seq` less than this. Given without `after_seq`, it pages backwards: the page is the `limit` messages just before `before_seq`, still oldest first. To keep scrolling up, pass the first message's `event_seq` as the next `before_seq`. With `after_seq` as well, the page starts after `after_seq` and stops before `before_seq`."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "maximum": 500,
              "default": 100
            },
            "description": "Maximum messages, 1–500. Defaults to 100. Counted from the oldest matching message, or from the newest when paging backwards with `before_seq` alone."
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessagePage"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "summary": "Send a message (idempotent by client_message_id)",
        "description": "Needs `chat:send`. Each principal may send 30 messages per 10 seconds, a limit shared with the WebSocket send path. Resending a `client_message_id` (1–128 characters) returns the original message with `duplicate: true` and writes, publishes and meters nothing. A new message commits with a durable `message.created` event. The event goes to the room's sockets and to subscribed webhooks, and members who are offline and not muted get a push. A mention gets through a mute, and in an e2ee room the push carries no sender or preview. Plain rooms accept the kinds `text`, `image`, `voice`, `video`, `story`, `file` and `system`, with a body of at most 16 KiB and at most 50 active members in `body.mentions`. E2EE rooms accept only `mls.*` kinds, with a body of exactly `{\"ct\": <base64>}` up to 128 KiB; sending the other room type's kinds returns 409. `attachment_ids` takes at most 10 attachments from this room, and each must be `ready` (409 otherwise). `reply_to` must be a message in this room. With `forward_of`, the server ignores `kind`, `body` and `attachment_ids` and copies the kind, body and media of a non-deleted message from a room the sender is in, dropping its mentions. An e2ee room cannot receive a forward.",
        "operationId": "postMessage",
        "tags": [
          "messages"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PostMessageRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Persisted (or original on duplicate)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "A bad `kind`, `client_message_id`, `mentions`, `reply_to` or `forward_of`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "The tenant has used its monthly message quota (`quota_exceeded`). Retrying will not help until the quota resets at the start of the month (UTC) or the plan changes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenOrBlocked"
          },
          "409": {
            "description": "A kind of the other room type (`mls.*` in a plain room, a plain kind in an e2ee room), an attachment that is not `ready`, or a forward into an e2ee room.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/rooms/{room_id}/messages/{message_id}": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member."
        },
        {
          "name": "message_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The message's id, in this room."
        }
      ],
      "patch": {
        "summary": "Edit own message",
        "description": "Editing a message to the body it already has changes nothing: the same message is returned with 200, edited_at does not move and no second message.updated is published, so a client that resends after losing an ack does not turn one edit into two versions.",
        "operationId": "editMessage",
        "tags": [
          "messages"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "description": "The new content, replacing the old: any JSON value, at most 16 KiB serialised. Only the sender can edit."
                  },
                  "if_version": {
                    "type": "string",
                    "description": "Optional precondition: the version of the message this edit was composed against, which is its edited_at as an RFC 3339 timestamp, or \"0\" when it has never been edited. The edit applies only while the message is still on that version; otherwise 409 conflict. Without it an edit is last-writer-wins, which is unsafe for a request that may be resent after a lost response."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The edited message",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      },
      "delete": {
        "summary": "Soft-delete a message",
        "description": "The sender may delete their own message, and an owner of the room anyone's — a group admin removing a message — read from the membership when the delete happens, so a demoted owner cannot. Anyone else gets 404, as for an id that does not exist. Idempotent: deleting a message that is already deleted returns the same tombstone with 200 and publishes no second event, so a client that resends after losing an ack is not told the deletion never happened.",
        "operationId": "deleteMessage",
        "tags": [
          "messages"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "The tombstoned message",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageEnvelope"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/rooms/{room_id}/messages/{message_id}/delivered": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member."
        },
        {
          "name": "message_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The message's id, in this room."
        }
      ],
      "post": {
        "summary": "Delivery receipt",
        "description": "Needs `chat:send`, so a `viewer` cannot post receipts. A principal's first delivery receipt for a message is recorded and publishes a durable `receipt.delivered` event `{ message_id, event_seq, principal_id }` to the room's sockets and to subscribed webhooks. Posting it again returns `advanced: false` and publishes nothing. A delivery receipt does not change `last_read_seq` or the unread count.",
        "operationId": "receiptDelivered",
        "tags": [
          "receipts"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Whether this was the principal's first delivery receipt for this message; a repeat is `false` and publishes nothing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReceiptResult"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/rooms/{room_id}/messages/{message_id}/read": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member."
        },
        {
          "name": "message_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The message's id, in this room."
        }
      ],
      "post": {
        "summary": "Read receipt",
        "description": "Needs `chat:send`, so a `viewer` cannot post receipts. A principal's first read receipt for a message is recorded and publishes a durable `receipt.read` event `{ message_id, event_seq, principal_id }` to the room's sockets and to subscribed webhooks. It also raises the member's `last_read_seq` to that message's `event_seq` (never lowering it); `GET /v1/rooms/{room_id}/unread` counts from that value. Posting it again returns `advanced: false` and publishes nothing. `advanced` is tracked per message, so a first receipt for an older message returns `true` without moving `last_read_seq`.",
        "operationId": "receiptRead",
        "tags": [
          "receipts"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Whether this was the principal's first read receipt for this message; a repeat is `false` and publishes nothing. `last_read_seq` only ever moves forward, so a first receipt for an older message is `true` without moving it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReceiptResult"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/rooms/{room_id}/participants": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member."
        }
      ],
      "get": {
        "summary": "Members with presence",
        "description": "Needs only active membership. Lists the room's current members (anyone who has left is excluded) with their `role` and an `online` flag. `online` is true while the member keeps a chat WebSocket open to this room; the presence heartbeat expires after 30 seconds. If presence cannot be read, every member is reported offline and the request still succeeds.",
        "operationId": "participants",
        "tags": [
          "rooms"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Room members with live presence",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ParticipantsResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/rooms/{room_id}/attachments/init": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member."
        }
      ],
      "post": {
        "summary": "Begin a direct upload",
        "description": "Needs `attachment:upload`. `declared_mime` must be a permitted type; an e2ee room accepts only `application/octet-stream` ciphertext and refuses `poster_of`. `original_name` must be 1–255 bytes with no `/`, `\\`, NUL or `..`. `byte_size` must be between 1 and the deployment's `MAX_UPLOAD_BYTES` (100 MiB by default); a larger size returns 400. The monthly byte quota is checked against the declared size. `poster_of` marks an image as the poster of a video attachment in the same room. The response is a presigned PUT URL whose lifetime grows with the size, from 5 minutes to 2 hours, plus the headers to send with it. An upload that has not been completed and verified within 2 hours is deleted. Files larger than 5 MiB can use `init-multipart` instead.",
        "operationId": "initAttachment",
        "tags": [
          "attachments"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InitAttachmentRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Presigned PUT",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InitAttachmentResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "`byte_size` outside 1 to the deployment's upload limit, or a bad `original_name` or `poster_of`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "This upload would take the tenant past its monthly byte quota (`quota_exceeded`). Retrying will not help until the quota resets at the start of the month (UTC) or the plan changes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "413": {
            "description": "The request body is over 512 KiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "415": {
            "description": "Blocked or unknown type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/rooms/{room_id}/attachments/{attachment_id}/complete": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member."
        },
        {
          "name": "attachment_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The attachment's id, as returned when the upload began."
        }
      ],
      "post": {
        "summary": "Finish upload (server HEAD-verifies size, queues verification + thumbnail)",
        "description": "Only the uploader may complete an upload (403 otherwise). The server checks that the object exists at exactly the declared `byte_size` (409 if it is missing or a different size). It then marks the attachment `uploaded` and queues a single verification job. That job re-checks the size, checks `sha256` if one was declared and requires the leading bytes to match `declared_mime`; images also get a thumbnail. The result arrives as a durable `attachment.ready` or `attachment.failed` event; `attachment.failed` includes a `reason`, and the failed object is deleted. Only a `ready` attachment can be sent in a message or downloaded. Calling complete again after the attachment is `ready` returns 200 with `status` `ready`; calling it while verification is running, or after it failed, returns 409. In an e2ee room, ciphertext is checked only by size and the optional `sha256`.",
        "operationId": "completeAttachment",
        "tags": [
          "attachments"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Attachment id and its post-upload status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompleteAttachmentResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The object is not uploaded yet or its size differs from `byte_size`, or the attachment is already verifying or failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/rooms/{room_id}/attachments/{attachment_id}/download": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member."
        },
        {
          "name": "attachment_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The attachment's id, as returned when the upload began."
        }
      ],
      "get": {
        "summary": "Authorized presigned download (only status=ready)",
        "description": "Needs `attachment:download`. Returns a presigned GET `download_url` valid for 5 minutes, along with the verified `mime`, `original_name` and a `disposition` (`inline` or `attachment`; e2ee ciphertext is always `attachment`). Images also get a `thumbnail_url`, and videos get a `poster_url` when a ready poster exists; both are presigned for the same 5 minutes. Request the attachment again for fresh URLs instead of caching them. An attachment that is not `ready`, whether still verifying or failed, returns 409.",
        "operationId": "downloadAttachment",
        "tags": [
          "attachments"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DownloadResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The attachment is not `ready`: still verifying, or failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/internal/v1/rooms": {
      "get": {
        "summary": "Resolve a room by the external reference the control plane assigned at creation. Lets a control plane recover a room id it no longer holds, without keeping its own mirror in sync.",
        "description": "Both `tenant_id` and `external_ref` are required, and `external_ref` must match exactly within that tenant. The room is returned whatever its `status`.",
        "operationId": "lookupRoom",
        "tags": [
          "internal"
        ],
        "security": [
          {
            "hmac": []
          }
        ],
        "parameters": [
          {
            "name": "tenant_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The tenant to look in."
          },
          {
            "name": "external_ref",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "unique per tenant; the same ref always resolves to the same room, which is what makes room creation idempotent"
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "The room",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Room"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "no room with that external_ref in the tenant",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "summary": "Create room (idempotent via tenant_id+external_ref)",
        "description": "Registers `tenant_id` (1–128 characters) as a tenant if it is new. With `external_ref` the call is idempotent per tenant: repeating it returns the existing room unchanged and ignores the other fields. Without `external_ref`, every call creates a new room. The room has no members. `e2ee` and `e2ee_scheme` (`mls` or `megolm`, which requires `e2ee: true`) are set at creation and cannot change. `expires_at` is informational only: nothing closes or deletes the room when it passes.",
        "operationId": "createRoom",
        "tags": [
          "internal"
        ],
        "security": [
          {
            "hmac": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "tenant_id",
                  "created_by"
                ],
                "properties": {
                  "tenant_id": {
                    "type": "string",
                    "description": "The tenant to create the room in, 1–128 characters."
                  },
                  "created_by": {
                    "type": "string",
                    "description": "The principal recorded as the room's creator."
                  },
                  "external_ref": {
                    "type": "string",
                    "description": "Your own reference for the room. If one with this `external_ref` exists in the tenant, it is returned instead."
                  },
                  "expires_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Optional informational expiry. Nothing closes or deletes the room when it passes."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Room",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Room"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "`tenant_id` outside 1–128 characters, an invalid `e2ee_scheme`, or a malformed body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/internal/v1/rooms/{room_id}": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id."
        }
      ],
      "patch": {
        "summary": "Update room status/expiry",
        "description": "Leave a field out to keep its current value. `expires_at` cannot be cleared, because null also keeps the current value. `status` must be `active` or `closed` (400 otherwise). Closing a room only stops new session tokens from being minted for it (409 `room is closed`). Members keep their tokens, open sockets and history, and can still send until their tokens expire. Setting `active` lifts this. No event is published. `expires_at` remains informational only.",
        "operationId": "patchRoom",
        "tags": [
          "internal"
        ],
        "security": [
          {
            "hmac": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PatchRoomRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated room",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Room"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "`status` other than `active` or `closed`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No such room, or the room is being erased: it cannot be reactivated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/internal/v1/rooms/{room_id}/members": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id."
        }
      ],
      "post": {
        "summary": "Add/reactivate member",
        "operationId": "addMember",
        "tags": [
          "internal"
        ],
        "security": [
          {
            "hmac": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "principal_id"
                ],
                "properties": {
                  "principal_id": {
                    "type": "string",
                    "description": "Who to add, 1–128 characters. A removed member is restored."
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "owner",
                      "member",
                      "viewer"
                    ],
                    "default": "member",
                    "description": "`owner`, `member` or `viewer`. Defaults to `member`; anything else is 400."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The added or reactivated member",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomMember"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "description": "A room holds at most 5,000 active members, on every plane; once it is full the add is refused with 409 and nothing changes. Restoring a removed member or changing an existing member's role never needs a seat."
      }
    },
    "/internal/v1/rooms/{room_id}/members/{principal_id}": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id."
        },
        {
          "name": "principal_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The member to remove."
        }
      ],
      "delete": {
        "summary": "Remove member (marks left_at; blocks new tokens immediately)",
        "description": "Sets the membership's `left_at`; history is kept, and adding the principal again restores the membership. The removal takes effect at once: the member's live chat and RTC sockets close with `4403`, and their HTTP requests and new session tokens are refused. A durable `member.removed` event goes to the room's connected clients and to webhooks subscribed to it. E2EE clients use this event as the cue to rotate keys. 404 if the room does not exist or the principal is not an active member.",
        "operationId": "removeMember",
        "tags": [
          "internal"
        ],
        "security": [
          {
            "hmac": []
          }
        ],
        "responses": {
          "200": {
            "description": "Whether a member row was removed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RemovedResult"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/internal/v1/session-tokens": {
      "post": {
        "summary": "Mint a room-scoped session token + TURN credentials",
        "description": "`role` defaults to the member's stored role and may only lower it: a token above the member's role (viewer → owner) is refused with 403, a role outside owner|member|viewer with 400.",
        "operationId": "mintSessionToken",
        "tags": [
          "internal"
        ],
        "security": [
          {
            "hmac": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SessionTokenRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token bundle",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionTokenResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "`role` is not one of owner | member | viewer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The principal is not an active member of the room, or `role` is above the member's stored role (a viewer cannot be minted an owner token).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "404": {
            "description": "Room not found in tenant",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/internal/v1/rooms/{room_id}/status": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id."
        }
      ],
      "get": {
        "summary": "Room, member count, online + RTC participants",
        "description": "`member_count` counts active members. `online_participants` lists the members who currently have chat presence. `rtc_participants` lists the principals in the room's call on the SFU at request time, and is empty when nobody is in the call. If presence cannot be read, `online_participants` comes back empty and the request still succeeds.",
        "operationId": "roomStatus",
        "tags": [
          "internal"
        ],
        "security": [
          {
            "hmac": []
          }
        ],
        "responses": {
          "200": {
            "description": "Room, member count and live presence",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomStatus"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/health/live": {
      "get": {
        "summary": "Liveness",
        "description": "Returns 200 with the text `ok` whenever the process is serving HTTP. It checks no dependency. Unauthenticated.",
        "operationId": "live",
        "tags": [
          "ops"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Process is up",
            "content": {
              "text/plain; charset=utf-8": {
                "schema": {
                  "type": "string"
                },
                "example": "ok"
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/health/ready": {
      "get": {
        "summary": "Readiness (PostgreSQL, Valkey, object store)",
        "description": "Checks PostgreSQL with `SELECT 1`, then runs a Valkey command, then sends a HEAD request to the object store. Returns 503 naming the first dependency that failed, with its error as plain text, or 200 with `ready`. Unauthenticated.",
        "operationId": "ready",
        "tags": [
          "ops"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "PostgreSQL, Valkey and the object store all answered",
            "content": {
              "text/plain; charset=utf-8": {
                "schema": {
                  "type": "string"
                },
                "example": "ready"
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "503": {
            "description": "dependency down",
            "content": {
              "text/plain; charset=utf-8": {
                "schema": {
                  "type": "string"
                },
                "example": "postgres: connection refused"
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/metrics": {
      "get": {
        "summary": "Prometheus metrics",
        "description": "Prometheus text for this process: connection gauges, counters for messages, outbox, jobs and auth failures, and RTC series. Labels may carry a tenant id but never a room, principal, device or message id, and no tenant content is exposed. The public edge answers this path with 403, so it is scraped only on the internal network.",
        "operationId": "metrics",
        "tags": [
          "ops"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Prometheus exposition, text/plain; version=0.0.4",
            "content": {
              "text/plain; version=0.0.4": {
                "schema": {
                  "type": "string"
                },
                "example": "# HELP ..."
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/directory/contacts/lookup": {
      "post": {
        "summary": "Which contact phone numbers are registered directory users",
        "description": "Send up to 500 numbers per call. More is a 400. Matching is exact on the E.164 form (`+` and 8 to 15 digits), and anything else is silently dropped, so normalize numbers on the device first. Only users of the caller's own tenant are matched. Each match comes back with `user_id`, `phone`, `display_name`, `about` and `avatar_version`, in no particular order. Each user may call this 20 times a minute. Past that it answers 429 `rate_limited` with `Retry-After`.",
        "operationId": "contactsLookup",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "phones"
                ],
                "additionalProperties": false,
                "properties": {
                  "phones": {
                    "type": "array",
                    "maxItems": 500,
                    "items": {
                      "type": "string"
                    },
                    "description": "Up to 500 phone numbers, E.164. Invalid numbers are dropped rather than failing the batch."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Registered contacts in the caller's tenant",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "contacts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "user_id": {
                            "type": "string",
                            "format": "uuid",
                            "description": "The user's id, which is also their chat `principal_id`."
                          },
                          "phone": {
                            "type": "string",
                            "description": "The matched number, E.164."
                          },
                          "display_name": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "The user's display name, or null if not set."
                          },
                          "about": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "maxLength": 256,
                            "description": "The profile line, up to 256 characters; null when not set."
                          },
                          "avatar_version": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "When their current profile photo was set; null when there is none. The value to cache the picture by: fetch `GET /v1/directory/users/{user_id}/avatar` once per version (its `ETag` is this value)."
                          }
                        }
                      },
                      "description": "The numbers that belong to registered users in your tenant. Other tenants' users are never disclosed."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "More than 500 numbers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "description": "Over 20 lookups a minute by this user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/directory/users/{user_id}/avatar": {
      "parameters": [
        {
          "name": "user_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "A user of your tenant (yourself included)."
        },
        {
          "name": "size",
          "in": "query",
          "required": false,
          "schema": {
            "type": "string",
            "enum": [
              "thumb",
              "full"
            ],
            "default": "thumb"
          },
          "description": "`thumb`: a JPEG no larger than 256 px on its longest side. `full`: the bytes as uploaded."
        },
        {
          "$ref": "#/components/parameters/XRequestId"
        }
      ],
      "get": {
        "summary": "A user's profile photo",
        "description": "A redirect to a short-lived presigned URL for the picture. `ETag` is the photo's version, exactly the `avatar_version` the lists show, so a client that caches the bytes by version sends it in `If-None-Match` and gets 304 instead of a redirect. 404 when the user has no photo, is not one of your tenant's, or when either of you has blocked the other. The redirect itself must not be cached (`Cache-Control: private, no-store`); cache the bytes by version.",
        "operationId": "userAvatar",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "responses": {
          "302": {
            "description": "Follow `Location` for the bytes.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "format": "uri"
                },
                "description": "A presigned GET, valid for a few minutes."
              },
              "ETag": {
                "schema": {
                  "type": "string"
                },
                "description": "The photo's version, quoted: `\"2026-01-15T09:30:00Z\"`."
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "304": {
            "description": "Your `If-None-Match` matches the current version: the bytes you have are current.",
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "`size` is neither `thumb` nor `full`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/directory/conversations": {
      "get": {
        "summary": "The caller's directory conversations",
        "description": "Lists the direct and group conversations the caller is an active member of, newest-created first, in one unpaginated response. Rooms your server created with an `ak_` key do not appear here, even if the user is a member. For a chat list with the last message and unread counts, use `GET /v1/directory/inbox`.",
        "operationId": "conversationsList",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Conversations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "room_id": {
                            "type": "string",
                            "format": "uuid",
                            "description": "The conversation's room. Mint a token for it to chat or call."
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "direct",
                              "group"
                            ],
                            "description": "`direct` (two people) or `group`."
                          },
                          "name": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "The group's name; null for a direct conversation."
                          },
                          "description": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "maxLength": 1024,
                            "description": "The group's description, up to 1024 characters; null when there is none."
                          },
                          "icon_attachment_id": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "uuid",
                            "description": "The group's icon: an image attachment of the group's own room, or null. Fetch it like any picture posted there — `GET /v1/rooms/{room_id}/attachments/{attachment_id}/download` with a room token — and cache it by id; a changed icon is a new id."
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When the conversation was created."
                          }
                        }
                      },
                      "description": "The caller's directory conversations."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/directory/conversations/direct": {
      "post": {
        "summary": "Create or fetch the 1:1 conversation with a peer",
        "operationId": "conversationDirect",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "peer_user_id"
                ],
                "additionalProperties": false,
                "properties": {
                  "peer_user_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The other person's directory user id, in your tenant."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Conversation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room_id": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The 1:1 room. The same pair always gets the same room."
                    },
                    "kind": {
                      "type": "string",
                      "description": "Always `direct`."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "Invalid (e.g. self)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Blocked"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "description": "Refused with `blocked` (403) when either of you has blocked the other."
      }
    },
    "/v1/directory/conversations/group": {
      "post": {
        "summary": "Create a named group conversation",
        "operationId": "conversationGroup",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "member_user_ids"
                ],
                "additionalProperties": false,
                "properties": {
                  "member_user_ids": {
                    "type": "array",
                    "maxItems": 4999,
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "description": "The other members' directory user ids, all in your tenant. You are added as owner. A group holds at most 5,000 members including you; anything larger is 400 before any lookup."
                  },
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 128,
                    "description": "Optional group name, up to 128 characters."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Conversation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "room_id": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The new group's room. Each call creates a new group."
                    },
                    "kind": {
                      "type": "string",
                      "description": "Always `group`."
                    },
                    "name": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The group's name, or null."
                    },
                    "not_added": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NotAdded"
                      },
                      "description": "People you named who were not added, with the reason — only `blocked` today: they have blocked you. Absent or empty when everyone was added."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Blocked"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "description": "Anyone you named who has blocked you is left out and listed in `not_added`; the group is created with the rest. If everyone you named has blocked you, nothing is created: `blocked` (403)."
      }
    },
    "/v1/directory/conversations/{room_id}/token": {
      "get": {
        "summary": "Mint a room-scoped chat/RTC session token for the caller",
        "description": "Works for any room in the caller's tenant where the caller is an active member. The token carries the caller's own role in that room. It lasts 10 minutes, so mint a new one before `expires_at`. Pass `device_id` to stamp it into the token's `did` claim. A closed room answers 409 `conflict`. A room the caller is not an active member of answers 403, and a room outside the tenant answers 404.",
        "operationId": "conversationToken",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The conversation's room. The caller must be a member (403 otherwise); a room in another tenant is 404."
          },
          {
            "name": "device_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Identifies this device. Needed for the E2EE endpoints, and to join a call from two devices at once."
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "Session token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionTokenResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The room is closed: no new tokens are minted for it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        }
      }
    },
    "/v1/directory/conversations/{room_id}": {
      "get": {
        "summary": "A conversation, as a member sees it",
        "description": "Any active member may ask, of a group or a direct conversation: the group's name, description and icon, how many are in it, and when it began.",
        "operationId": "conversationGet",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The conversation. 404 if it is not a directory conversation of your tenant; 403 if you are not a member."
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "The conversation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationInfo"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "summary": "Update a group's name, description and/or icon",
        "description": "Owner only. A field left out is untouched; null (or an empty string) clears it. The icon must be a verified, ready image attachment uploaded to this very group (400 otherwise). Each change is posted into the room as a `system` message — `{\"type\": \"group.renamed\", \"name\": …, \"by\": <user id>}`, `{\"type\": \"group.description_changed\", \"description\": …, \"by\": …}`, `{\"type\": \"group.icon_changed\", \"icon_attachment_id\": …, \"by\": …}` — so every member sees it live and it stays in the history. A direct conversation answers 400.",
        "operationId": "conversationRename",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The group. 404 if it is not a directory conversation of your tenant; 403 if you are not a member; 403 if you are not its owner."
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "minProperties": 1,
                "properties": {
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 128,
                    "description": "The new name, up to 128 characters; null or empty clears it."
                  },
                  "description": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 1024,
                    "description": "The new description, up to 1024 characters; null or empty clears it."
                  },
                  "icon_attachment_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "description": "A ready image attachment of this group's room, or null to remove the icon."
                  }
                },
                "description": "At least one of the three."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The group as it now stands",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationInfo"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "Nothing to update, a name over 128 or a description over 1024 characters, an `icon_attachment_id` that is not a ready image of this group, or a direct conversation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/directory/conversations/{room_id}/members": {
      "get": {
        "summary": "List the members of a conversation",
        "description": "Any active member may ask, of a group or a direct conversation. Owners first, then by seniority. A group holds at most 5,000 members, so the list is never paged.",
        "operationId": "conversationMembers",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The conversation. 404 if it is not a directory conversation of your tenant; 403 if you are not a member."
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "The active members",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "members": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DirectoryGroupMember"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "summary": "Add members to a group",
        "description": "Owner only. Up to 256 ids per call, every one a directory user of your tenant (404 otherwise, and nothing is added). Your own id and duplicates are ignored. A previously removed member is restored. Each addition reaches connected clients as a durable `member.added` event. A room holds at most 5,000 active members, on every plane; once it is full the add is refused with 409 and nothing changes. Restoring a removed member or changing an existing member's role never needs a seat. Anyone you named who has blocked you is left out and listed in `not_added`; the rest are added, and a request that adds nobody for that reason is still a 200.",
        "operationId": "conversationAddMembers",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The group. 404 if it is not a directory conversation of your tenant; 403 if you are not its owner; 400 if it is a direct conversation."
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "member_user_ids"
                ],
                "additionalProperties": false,
                "properties": {
                  "member_user_ids": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 256,
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "description": "Who to add, 1–256 directory user ids."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Added",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "added": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "format": "uuid"
                      },
                      "description": "The ids admitted by this call, in request order, without duplicates."
                    },
                    "member_count": {
                      "type": "integer",
                      "description": "Active members after the call."
                    },
                    "not_added": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NotAdded"
                      },
                      "description": "People you named who were not added, with the reason — only `blocked` today: they have blocked you. Absent or empty when everyone was added."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/v1/directory/conversations/{room_id}/members/{user_id}": {
      "put": {
        "summary": "Make a member an owner, or an owner a member",
        "description": "Owner only. A group may have any number of owners (there is no separate admin role: owners are the admins), each able to do everything an owner can. The last owner cannot be demoted — make someone else an owner first — which is the same rule `leave` keeps. Announced to the room as a durable `member.updated` event `{ \"principal_id\": <user id>, \"role\": … }`, which webhooks may subscribe to.",
        "operationId": "conversationSetRole",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The group. 404 if it is not a directory conversation of your tenant; 403 if you are not a member; 403 if you are not an owner."
          },
          {
            "name": "user_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "An active member of the group (404 otherwise); yourself included."
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "role"
                ],
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "owner",
                      "member"
                    ],
                    "description": "The role to give."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The role as stored",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "user_id",
                    "role"
                  ],
                  "properties": {
                    "user_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "role": {
                      "type": "string",
                      "enum": [
                        "owner",
                        "member"
                      ]
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "A role other than `owner` or `member`, or a direct conversation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The member is the only owner.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Remove a member from a group",
        "description": "Owner only. Removing yourself is `leave`, which also settles ownership; asking to remove yourself here is 400. The removal reaches connected clients as a durable `member.removed` event.",
        "operationId": "conversationRemoveMember",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The group. 404 if it is not a directory conversation of your tenant; 403 if you are not its owner."
          },
          {
            "name": "user_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The member to remove. 404 if they are not an active member."
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "Removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "removed": {
                      "type": "boolean"
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/directory/conversations/{room_id}/leave": {
      "post": {
        "summary": "Leave a group",
        "description": "Any active member. If you were the only owner, the longest-standing remaining member becomes owner — announced as `member.added` with role `owner`. When nobody remains the room is closed (history stays readable to nobody; it is no longer a conversation). A direct conversation cannot be left: 400.",
        "operationId": "conversationLeave",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "parameters": [
          {
            "name": "room_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The group. 404 if it is not a directory conversation of your tenant; 403 if you are not a member."
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "Left",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "left": {
                      "type": "boolean"
                    },
                    "new_owner": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The member who inherited ownership, or null."
                    },
                    "closed": {
                      "type": "boolean",
                      "description": "True when you were the last member and the room closed."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/directory/conversations/{room_id}/mute": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The conversation, direct or group. 404 if it is not a directory conversation of your tenant; 403 if you are not a member."
        },
        {
          "$ref": "#/components/parameters/XRequestId"
        }
      ],
      "put": {
        "summary": "Mute a conversation",
        "description": "Silence push notifications from this conversation for yourself: eight hours, one week, or until you unmute. Live delivery over the WebSocket is unaffected and a call still rings. Private: no event is emitted and nobody else can see your mute. `GET /v1/directory/inbox` shows `muted_until` on each conversation, so a client needs no further request to draw the icon.",
        "operationId": "conversationMute",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MuteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Muted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationMute"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "A `duration` outside `8h|1w|always`, or an unknown field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Unmute a conversation",
        "description": "Push notifications again. Unmuting a conversation that was not muted answers the same 200.",
        "operationId": "conversationUnmute",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Unmuted; `muted_until` is null.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversationMute"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/directory/conversations/{room_id}/invites": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The group. 404 if it is not a directory conversation of your tenant; 403 if you are not a member; 403 if you are not an owner."
        },
        {
          "$ref": "#/components/parameters/XRequestId"
        }
      ],
      "get": {
        "summary": "A group's invite links",
        "description": "Owner only. The links that have not been revoked, newest first, without their tokens. An expired or used-up link stays listed, with the fields that say so, until it is revoked.",
        "operationId": "invitesList",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "responses": {
          "200": {
            "description": "The links",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "invites"
                  ],
                  "properties": {
                    "invites": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/GroupInvite"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "summary": "Create an invite link",
        "description": "Owner only. Whoever in your tenant presents the token joins the group as a member, under the room cap, until the link is revoked, expired or used up. The token is in this answer and nowhere else; only its hash is kept. A group holds at most 20 live links; an expiry is at most 30 days away; sixty creations a minute per user. A link dies with the owner who created it, should they be erased.",
        "operationId": "inviteCreate",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "expires_in_seconds": {
                    "type": "integer",
                    "format": "int64",
                    "minimum": 1,
                    "maximum": 2592000,
                    "description": "Seconds until the link stops working; omitted means it does not expire."
                  },
                  "max_uses": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "How many people may join through it; omitted means any number."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The link, with its token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "invite_id",
                    "room_id",
                    "created_by",
                    "created_at",
                    "expires_at",
                    "max_uses",
                    "uses",
                    "token"
                  ],
                  "properties": {
                    "invite_id": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The link, for revoking it."
                    },
                    "room_id": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The group it admits to."
                    },
                    "created_by": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The owner who created it."
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "expires_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "When it stops working; null for a link that does not expire."
                    },
                    "max_uses": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "How many people may join through it; null for any number."
                    },
                    "uses": {
                      "type": "integer",
                      "description": "How many have. A member joining again is not a use."
                    },
                    "token": {
                      "type": "string",
                      "description": "The token to share (`inv_` and 43 URL-safe characters). Shown only here."
                    }
                  },
                  "description": "The link as `GroupInvite`, plus its token."
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "`expires_in_seconds` outside 1..=2592000 or `max_uses` below 1.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The group already holds 20 live links.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/directory/conversations/{room_id}/invites/{invite_id}": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The group. 404 if it is not a directory conversation of your tenant; 403 if you are not a member; 403 if you are not an owner."
        },
        {
          "name": "invite_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "From `inviteCreate` or the list."
        },
        {
          "$ref": "#/components/parameters/XRequestId"
        }
      ],
      "delete": {
        "summary": "Revoke an invite link",
        "description": "Owner only. Whoever holds the token can no longer preview or join. 404 when the group has no such live link.",
        "operationId": "inviteRevoke",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "revoked"
                  ],
                  "properties": {
                    "revoked": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/directory/invites/preview": {
      "post": {
        "summary": "What an invite link leads to",
        "description": "Before joining: the group's name, description, icon and size, and whether you are already in it. Any user of the tenant may ask. One 404 for every way a link can be no good — unknown, another tenant's, revoked, expired, used up, or its group closed — so a token tells a stranger nothing. The token travels in the body, never the URL. Sixty a minute per user.",
        "operationId": "invitePreview",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "The link token, exactly as the owner received it (`inv_…`)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The group behind the link",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "room_id",
                    "name",
                    "description",
                    "icon_attachment_id",
                    "member_count",
                    "member",
                    "expires_at"
                  ],
                  "properties": {
                    "room_id": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The group the link admits to."
                    },
                    "name": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "description": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "maxLength": 1024,
                      "description": "The group's description, up to 1024 characters; null when there is none."
                    },
                    "icon_attachment_id": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uuid",
                      "description": "The group's icon, if any. A non-member cannot fetch it until they join; show it from the id once in."
                    },
                    "member_count": {
                      "type": "integer",
                      "description": "Active members."
                    },
                    "member": {
                      "type": "boolean",
                      "description": "Whether you are already in the group."
                    },
                    "expires_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "When the link stops working; null for one that does not expire."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/directory/invites/join": {
      "post": {
        "summary": "Join a group through an invite link",
        "description": "You join as a member. Someone already in the group gets `joined: false` and the link is not spent. The admission is announced as `member.added`, exactly as an owner adding you is, and the room cap applies (409 when the group is full). 404 for a link that is no good (see `invitePreview`). Sixty a minute per user.",
        "operationId": "inviteJoin",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "The link token, exactly as the owner received it (`inv_…`)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "In",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "room_id",
                    "joined",
                    "member_count"
                  ],
                  "properties": {
                    "room_id": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The group. Mint a token for it to chat or call."
                    },
                    "joined": {
                      "type": "boolean",
                      "description": "False when you were already in."
                    },
                    "member_count": {
                      "type": "integer",
                      "description": "Active members after the call."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The group is full (5,000 members).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/directory/blocks": {
      "get": {
        "summary": "Your block list",
        "description": "Who you have blocked, newest first, as directory users. Private: nobody else can see your list, and nobody is told they are on it. A block freezes the direct conversation between you (`blocked` on sends, reactions, typing, calls and on opening a new one), and keeps the blocked person from adding you to a group. Shared groups are unaffected.",
        "operationId": "blocksList",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "Your block list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "blocks"
                  ],
                  "properties": {
                    "blocks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BlockEntry"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/directory/blocks/{user_id}": {
      "parameters": [
        {
          "name": "user_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "A directory user of your tenant."
        },
        {
          "$ref": "#/components/parameters/XRequestId"
        }
      ],
      "put": {
        "summary": "Block a user",
        "description": "Idempotent: blocking someone already blocked answers the same, with the original `since`. 400 for yourself, 404 for an id that is not a user of your tenant, 409 when your list holds 1,000 users. Rate limited to 60 changes a minute.",
        "operationId": "blockUser",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Blocked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlockState"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "You cannot block yourself.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Your block list is full (1,000 users).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "summary": "Unblock a user",
        "description": "Idempotent: lifting a block that is not there answers the same, and says nothing about the id. Rate limited to 60 changes a minute.",
        "operationId": "unblockUser",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Not blocked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlockState"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/directory/devices": {
      "get": {
        "summary": "The caller's registered push-notification devices",
        "description": "Returns the caller's push tokens on every platform (`fcm`, `apns` and `web`), most recently registered or refreshed first. Each carries the `app_id` it was registered under. Tokens that FCM or APNs report as no longer valid are removed automatically, so they drop out of this list.",
        "operationId": "listDevices",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Devices",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "devices": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid",
                            "description": "The device registration's id."
                          },
                          "platform": {
                            "type": "string",
                            "description": "`fcm`, `apns` or `web`."
                          },
                          "push_token": {
                            "type": "string",
                            "description": "The registered push token."
                          },
                          "app_id": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "The directory app this token was registered under — selects the FCM/APNs credentials used to reach it. Null for tokens registered before per-app credentials existed."
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When the token was last registered or refreshed."
                          }
                        }
                      },
                      "description": "The caller's registered push devices."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      },
      "post": {
        "summary": "Register or refresh a device push token",
        "description": "Idempotent per token: registering a token the caller already has updates its `platform` and `app_id` and refreshes `updated_at`. It does not add a second row. The token is tied to the directory app that issued the caller's session. That app's credentials, set with `PUT /v1/account/directory-apps/{app_id}/push`, sign its pushes, and nothing is sent until the app has credentials for the platform. Native pushes go only to `fcm` and `apns` tokens. `platform` must be `fcm`, `apns` or `web`, and `push_token` must be 1 to 4096 characters after trimming. Otherwise the call is a 400.",
        "operationId": "registerDevice",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "platform",
                  "push_token"
                ],
                "additionalProperties": false,
                "properties": {
                  "platform": {
                    "type": "string",
                    "enum": [
                      "fcm",
                      "apns",
                      "web"
                    ],
                    "description": "`fcm` (Android), `apns` (iOS) or `web`. The backend sends native pushes to `fcm` and `apns` tokens."
                  },
                  "push_token": {
                    "type": "string",
                    "maxLength": 4096,
                    "description": "The device's push token, up to 4096 characters. Registering the same token again refreshes it."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Registered device",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The device registration's id."
                    },
                    "platform": {
                      "type": "string",
                      "description": "The platform as stored."
                    },
                    "push_token": {
                      "type": "string",
                      "description": "The token as stored."
                    },
                    "app_id": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The directory app this token was registered under — selects the FCM/APNs credentials used to reach it."
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When it was registered or refreshed."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "`platform` is not `fcm`, `apns` or `web`, or `push_token` is empty or over 4096 characters after trimming.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      },
      "delete": {
        "summary": "Unregister a device push token",
        "description": "Removes the token only from the caller's own registrations. The token must match exactly as registered. A token the caller does not have answers 404, so a repeated delete can be treated as done. Signing out with `POST /v1/directory/logout` does not remove device tokens. Delete the token first, while the session still works, or the device keeps receiving that user's pushes.",
        "operationId": "deleteDevice",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "push_token"
                ],
                "additionalProperties": false,
                "properties": {
                  "push_token": {
                    "type": "string",
                    "description": "The token to unregister. Call this on sign-out."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean",
                      "description": "True if the token was registered to the caller and is now removed."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "The caller has no such push token registered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/directory/inbox": {
      "get": {
        "summary": "The caller's conversations as a chat list (peer/name, last message, unread)",
        "description": "Conversations are ordered newest first by the time of their latest message, or by their creation time if they have no messages. `limit` is clamped to 1–200 rather than rejected. There is no cursor: the response holds only the first `limit` conversations. `unread_count` counts messages after the caller's read cursor, excluding the caller's own messages and deleted ones. `last_message` is null for a conversation with no messages. A deleted latest message shows as kind `deleted`, a text preview is cut at 120 characters, and media messages get a fixed label. Direct conversations carry the other member as `peer`. `muted_until` is null unless the caller's mute is still in force.",
        "operationId": "listInbox",
        "tags": [
          "directory"
        ],
        "security": [
          {
            "directory_session": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            },
            "description": "Maximum conversations, 1–200. Defaults to 50."
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "Inbox",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "room_id": {
                            "type": "string",
                            "format": "uuid",
                            "description": "The conversation's room."
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "direct",
                              "group"
                            ],
                            "description": "`direct` or `group`."
                          },
                          "name": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "The group's name; null for a direct conversation (use `peer`)."
                          },
                          "description": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "maxLength": 1024,
                            "description": "The group's description, up to 1024 characters; null when there is none."
                          },
                          "icon_attachment_id": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "uuid",
                            "description": "The group's icon: an image attachment of the group's own room, or null. Fetch it like any picture posted there — `GET /v1/rooms/{room_id}/attachments/{attachment_id}/download` with a room token — and cache it by id; a changed icon is a new id."
                          },
                          "peer": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "properties": {
                              "user_id": {
                                "type": "string",
                                "format": "uuid",
                                "description": "The other person's user id."
                              },
                              "display_name": {
                                "type": [
                                  "string",
                                  "null"
                                ],
                                "description": "Their display name, or null."
                              },
                              "phone": {
                                "type": "string",
                                "description": "Their phone number, E.164."
                              },
                              "avatar_version": {
                                "type": [
                                  "string",
                                  "null"
                                ],
                                "format": "date-time",
                                "description": "When the other person's current profile photo was set; null when there is none. The value to cache the picture by: fetch `GET /v1/directory/users/{user_id}/avatar` once per version (its `ETag` is this value)."
                              }
                            },
                            "description": "For a direct conversation, the other person; null for a group."
                          },
                          "unread_count": {
                            "type": "integer",
                            "description": "Messages after the caller's read cursor."
                          },
                          "muted_until": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "Until when you have muted this conversation; null when not muted (a lapsed mute reads as null). `9999-12-31T00:00:00Z` stands for \"until unmuted\". Private: only your own mute is ever shown."
                          },
                          "last_message": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "properties": {
                              "event_seq": {
                                "type": "integer",
                                "description": "Its position in the room."
                              },
                              "sender_id": {
                                "type": [
                                  "string",
                                  "null"
                                ],
                                "description": "Who sent it."
                              },
                              "kind": {
                                "type": "string",
                                "description": "Its kind."
                              },
                              "preview": {
                                "type": "string",
                                "description": "A short label for the chat list: text up to 120 characters, or a label such as \"📷 Photo\" for media and \"Message deleted\" for a deleted message."
                              },
                              "created_at": {
                                "type": [
                                  "string",
                                  "null"
                                ],
                                "format": "date-time",
                                "description": "When it was sent."
                              }
                            },
                            "description": "The newest message, for the preview line; null if the conversation has none."
                          }
                        }
                      },
                      "description": "The caller's conversations as a chat list, most recent activity first."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/account/directory-apps/{app_id}/push": {
      "put": {
        "summary": "Set a directory app's native-push (FCM/APNs) credentials",
        "description": "Replaces both sides: an omitted or null `fcm` or `apns` clears what was stored, so send both to keep both. `fcm.service_account` needs `project_id`, `client_email` and `private_key`. A `token_uri`, if present, must be `https://oauth2.googleapis.com/token`. `apns` needs a PEM `p8`, `key_id`, `team_id` and `topic` (the bundle id), with `env` set to `sandbox` or `production` (default `production`). Credentials are validated here and are write-only: no call returns them. They sign pushes to the `fcm` and `apns` tokens registered under this app.",
        "operationId": "setPush",
        "tags": [
          "account"
        ],
        "security": [
          {
            "account_session": []
          }
        ],
        "parameters": [
          {
            "name": "app_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The directory app's public id."
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "description": "Full-replace: an omitted side clears it. Secrets are write-only.",
                "properties": {
                  "fcm": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "required": [
                      "service_account"
                    ],
                    "properties": {
                      "service_account": {
                        "type": "object",
                        "description": "Google service-account JSON (project_id, client_email, private_key, ...)"
                      }
                    },
                    "description": "FCM credentials: `service_account` is the Google service-account JSON. Omit or null to clear FCM, because this call replaces both sides."
                  },
                  "apns": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "required": [
                      "p8",
                      "key_id",
                      "team_id",
                      "topic"
                    ],
                    "properties": {
                      "p8": {
                        "type": "string",
                        "description": "The contents of the APNs `.p8` key file (PEM)."
                      },
                      "key_id": {
                        "type": "string",
                        "description": "The APNs key's 10-character id."
                      },
                      "team_id": {
                        "type": "string",
                        "description": "Your Apple developer team id."
                      },
                      "topic": {
                        "type": "string",
                        "description": "The app's bundle id."
                      },
                      "env": {
                        "type": "string",
                        "enum": [
                          "sandbox",
                          "production"
                        ],
                        "default": "production",
                        "description": "`production` (TestFlight and App Store) or `sandbox` (debug builds). Defaults to `production`. The wrong one fails silently."
                      }
                    },
                    "description": "APNs credentials. Omit or null to clear APNs, because this call replaces both sides."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Which push transports are now configured",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "push_fcm": {
                      "type": "boolean",
                      "description": "Whether FCM credentials are now stored."
                    },
                    "push_apns": {
                      "type": "boolean",
                      "description": "Whether APNs credentials are now stored."
                    }
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "Invalid push config",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No directory app with this id in your tenant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          }
        }
      }
    },
    "/v1/rooms/{room_id}/recordings": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The camera's room id. It must be the room the session token was minted for (403 otherwise), and the caller must still be an active member."
        }
      ],
      "get": {
        "summary": "A camera's stored recordings",
        "description": "DEPRECATED: the camera-feeds media sidecar that produced recordings has been decommissioned, so this returns an empty list. Responses carry `Deprecation` and `Sunset` headers; it may be removed on or after 2027-01-05. See docs/API-STABILITY.md.\n\nSegments of a recorded camera, newest first, each a fragmented MP4 playable on its own or in order. A camera is recorded by publishing to `rec/{room_id}` instead of `{room_id}`; nothing here turns recording on. Whoever may watch the camera may watch what it recorded: the same `rtc:subscribe` permission and the same live membership. Footage is deleted on the deployment’s retention schedule and by every erasure path, so treat these as a secondary copy of what the camera’s own recorder holds.",
        "operationId": "listRecordings",
        "tags": [
          "rooms"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only segments starting at or after this instant."
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only segments starting before this instant."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            },
            "description": "Maximum segments, counted back from the newest."
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "The camera’s stored segments",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordingsResponse"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "deprecated": true
      }
    },
    "/v1/server/rooms/{room_id}/rtc/connections/{client_id}": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. A room in another tenant, or one being erased, is 404."
        },
        {
          "name": "client_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^[0-9]+$"
          },
          "description": "The connection's id as a decimal string, as a roster (`peers`) or a forwarding read reports it. One connection, not a person: a member's other devices are untouched."
        },
        {
          "$ref": "#/components/parameters/XRequestId"
        }
      ],
      "delete": {
        "summary": "Close one RTC connection, keeping the membership",
        "description": "Closes exactly one of a member's RTC connections. The connection is closed with WebSocket code `4413` reason `connection_closed`, and the answer is 204 only once its media has been retired from the SFU (nothing it published is forwarded, nothing is forwarded to it) — not merely once a close was requested. 503 `not_ready` if that has not happened within three seconds: retry, and a 404 then means it finished. 404 when the id is not a live connection of this room. The member stays a member and the same token can join again; to stop a member altogether, remove them. Requires `members:write`.",
        "operationId": "closeRtcConnection",
        "tags": [
          "server"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "responses": {
          "204": {
            "description": "Closed, and its media retired.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "`client_id` is not a decimal connection id.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "The connection is closing but its media is not retired yet (`not_ready`). Retry.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/server/rooms/{room_id}/rtc/forwarding": {
      "parameters": [
        {
          "name": "room_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "The room's id. A room in another tenant, or one being erased, is 404."
        },
        {
          "$ref": "#/components/parameters/XRequestId"
        }
      ],
      "get": {
        "summary": "Who receives whose media in the room's call",
        "description": "The SFU's forwarding table for the room right now: each publishing connection, its tracks, and every connection each track is forwarded to (with the simulcast layer). Lets your server reconcile what its RTC policies intend against what is actually flowing. Read from the room's media thread at request time; `in_call: false` with no publishers when nobody is in the call. 503 `not_ready` if the media thread does not answer within two seconds — retry. Requires `rooms:read`.",
        "operationId": "getRoomForwarding",
        "tags": [
          "server"
        ],
        "security": [
          {
            "customer_key": []
          }
        ],
        "responses": {
          "200": {
            "description": "The forwarding state.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RtcForwardingState"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "The media thread did not answer in time (`not_ready`). Retry.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    }
  }
}
