{
  "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": "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": {
      "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."
          },
          "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",
              "audio/ogg",
              "audio/mpeg",
              "audio/mp4",
              "video/mp4",
              "video/webm",
              "application/pdf",
              "text/plain",
              "application/zip"
            ],
            "description": "The file's type. It must be on the allowlist, and the server checks the uploaded bytes match it. 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`. Restarts after a `DELETE`."
          },
          "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"
            ]
          },
          "message": {
            "type": "string"
          },
          "current_revision": {
            "type": "integer",
            "format": "int64",
            "description": "The revision stored now, 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": "True if a matching row was removed; false if there was nothing to remove."
          }
        }
      },
      "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": {
          "X-Request-Id": {
            "$ref": "#/components/headers/XRequestId"
          }
        }
      }
    },
    "headers": {
      "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": {
    "/v1/account/signup": {
      "post": {
        "summary": "Create a customer account (open self-serve) and its tenant; returns a session",
        "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"
              }
            }
          },
          "409": {
            "description": "email already registered",
            "headers": {
              "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",
        "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"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/account/logout": {
      "post": {
        "summary": "Invalidate the presented account session",
        "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)",
        "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)",
        "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"
              }
            }
          },
          "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)",
        "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",
        "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",
        "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)",
        "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)"
                    },
                    "delivery_webhook_url": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The delivery webhook, in webhook mode; null 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",
        "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)",
        "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"
              }
            }
          },
          "404": {
            "description": "unknown app",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "429": {
            "description": "rate limited",
            "headers": {
              "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",
        "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"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "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",
        "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."
      }
    },
    "/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",
        "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."
                    }
                  }
                }
              }
            },
            "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"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      },
      "get": {
        "summary": "List webhook endpoints",
        "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)",
        "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",
        "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",
        "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"
              }
            }
          },
          "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",
        "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",
        "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 second DELETE answers 404, and 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.",
            "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": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`stale_revision`: `revision` is below the stored one. `revision_conflict`: `revision` equals the stored one, 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)",
        "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/server/principals": {
      "post": {
        "summary": "Register an authoritative non-human sender identity",
        "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"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        },
        "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",
        "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"
              }
            }
          },
          "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"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/internal/v1/plans/{name}": {
      "put": {
        "summary": "Create or update a plan (rate tier + monthly usage quotas, operator only)",
        "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"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/internal/v1/api-keys": {
      "post": {
        "summary": "Provision a customer server API key (operator only)",
        "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"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "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)",
        "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"
          }
        },
        "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"
          }
        },
        "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.",
        "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"
              }
            }
          },
          "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.",
        "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.",
        "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.",
        "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"
          },
          "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.",
        "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"
              }
            }
          },
          "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": "declared size exceeds the configured limit",
            "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.",
        "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": "part list does not match the upload",
            "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"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/push/subscriptions": {
      "post": {
        "summary": "Register a Web Push subscription for the calling principal and device. Upserted by endpoint.",
        "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"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "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.",
        "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.",
        "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.",
        "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"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "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.",
        "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"
          },
          "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.",
        "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"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "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.",
        "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.",
        "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": "no shared active room with the target",
            "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.",
        "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).",
        "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.",
        "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"
              }
            }
          },
          "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.",
        "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"
              }
            }
          },
          "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.",
        "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"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "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.",
        "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"
              }
            }
          },
          "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)",
        "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)",
        "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"
              }
            }
          },
          "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"
          },
          "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 own message",
        "description": "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",
        "operationId": "receiptDelivered",
        "tags": [
          "receipts"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Whether the delivered cursor advanced",
            "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",
        "operationId": "receiptRead",
        "tags": [
          "receipts"
        ],
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Whether the read cursor advanced",
            "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",
        "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",
        "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"
              }
            }
          },
          "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": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "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)",
        "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": "Not uploaded / size mismatch",
            "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)",
        "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"
          },
          "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.",
        "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)",
        "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"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "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",
        "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"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "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)",
        "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",
        "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",
        "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)",
        "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",
        "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",
        "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"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "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",
        "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",
        "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"
          }
        }
      }
    },
    "/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",
        "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",
        "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"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      },
      "delete": {
        "summary": "Unregister a device push token",
        "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": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          }
        ]
      }
    },
    "/v1/directory/inbox": {
      "get": {
        "summary": "The caller's conversations as a chat list (peer/name, last message, unread)",
        "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",
        "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": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/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/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"
                }
              }
            }
          }
        }
      }
    }
  }
}
