{
  "openapi": "3.0.3",
  "info": {
    "title": "VideoRayan Control API (public + tenant surfaces)",
    "version": "1.0.0",
    "description": "Control-plane REST API for the self-hosted VideoRayan realtime communication service. Two surfaces: an open Public API for browser-created free rooms (no key) and a Tenant API (Bearer key, server-side only) for owned rooms. Tokens returned are short-lived LiveKit JWTs scoped to a single room. Media signaling: wss://rtc.videorayan.ir. Rate limit: 120 req/min per key/IP. Live machine-readable contract for the tenant surface: GET /v1/openapi.yaml.",
    "contact": { "name": "VideoRayan", "email": "info@videorayan.ir", "url": "https://en.videorayan.ir" }
  },
  "servers": [
    { "url": "https://api.videorayan.ir/v1", "description": "Production API" },
    { "url": "https://en.videorayan.ir/api/v1", "description": "Same-origin mirror on the English portal" }
  ],
  "tags": [
    { "name": "Public", "description": "Free browser-created rooms. No key required; rate-limited per client IP." },
    { "name": "Guest", "description": "Join-token issuance for public rooms. The room link is the credential." },
    { "name": "Tenant", "description": "Owned rooms for production products. Requires Authorization: Bearer <tenant key> (rtck_...)." },
    { "name": "Ops", "description": "Health, readiness, and the live OpenAPI contract." }
  ],
  "paths": {
    "/public/rooms": {
      "post": {
        "tags": ["Public"],
        "summary": "Create (or fetch) a free public room",
        "description": "No key required. The room name is the public handle and creation is idempotent: creating a room with an existing name returns the same room. Optionally password-protected (verified server-side at join time). Rate-limited per client IP.",
        "operationId": "createPublicRoom",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/PublicRoomCreate" },
              "example": { "name": "weekly-standup", "password": "s3cret" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Room created (or existing room returned)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PublicRoomCreated" },
                "example": { "roomId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "name": "weekly-standup" }
              }
            }
          },
          "400": { "description": "Validation failed (name/password length, unknown fields)", "content": { "application/json": { "example": { "error": "invalid_input" } } } },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/public/rooms/{ref}": {
      "get": {
        "tags": ["Public"],
        "summary": "Resolve a public room by ref (UUID or name)",
        "description": "Call before joining to learn whether the room requires a password.",
        "operationId": "resolvePublicRoom",
        "parameters": [{ "name": "ref", "in": "path", "required": true, "schema": { "type": "string", "minLength": 1, "maxLength": 64 } }],
        "responses": {
          "200": {
            "description": "Room resolved",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PublicRoomResolved" },
                "example": { "roomId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "name": "weekly-standup", "needsPassword": true }
              }
            }
          },
          "404": { "description": "Room not found or expired", "content": { "application/json": { "example": { "error": "room_not_found" } } } }
        }
      }
    },
    "/guest/rooms/{ref}/tokens": {
      "post": {
        "tags": ["Guest"],
        "summary": "Issue a guest join token for a public room",
        "description": "The participant cap, room expiry, and password (if set) are enforced server-side. ref may be the room UUID or the public room name. Every issuance is audited and heavily rate-limited.",
        "operationId": "issueGuestToken",
        "parameters": [{ "name": "ref", "in": "path", "required": true, "schema": { "type": "string", "minLength": 1, "maxLength": 64 } }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/GuestTokenRequest" },
              "example": { "identity": "sara", "password": "s3cret" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Token issued — connect to wss://rtc.videorayan.ir with it",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/GuestTokenResponse" },
                "example": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "identity": "sara", "roomName": "weekly-standup", "expiresAt": "2026-10-26T12:40:00.000Z" }
              }
            }
          },
          "403": { "description": "Wrong or missing room password" },
          "404": { "description": "Room not found" },
          "409": { "description": "Room full or already ended" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/rooms": {
      "post": {
        "tags": ["Tenant"],
        "summary": "Create a tenant-owned room",
        "description": "Requires a tenant API key. Send an Idempotency-Key header (unique string, <=128 chars): retrying with the same key returns the original room with 200 instead of creating a duplicate.",
        "operationId": "createRoom",
        "parameters": [
          { "name": "Idempotency-Key", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 128 } }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RoomCreate" },
              "example": { "externalId": "appointment-123", "maxParticipants": 3, "expiresInSeconds": 3600 }
            }
          }
        },
        "responses": {
          "201": { "description": "Room created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Room" } } } },
          "200": { "description": "Idempotent replay — existing room returned", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Room" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/rooms/{roomId}": {
      "parameters": [{ "name": "roomId", "in": "path", "required": true, "schema": { "type": "string", "maxLength": 128 } }],
      "get": {
        "tags": ["Tenant"],
        "summary": "Get room details (owner tenant only)",
        "operationId": "getRoom",
        "responses": {
          "200": { "description": "Room", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Room" } } } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "delete": {
        "tags": ["Tenant"],
        "summary": "End the room — closes it in LiveKit and blocks re-join (owner tenant only)",
        "operationId": "deleteRoom",
        "responses": {
          "204": { "description": "Room ended and cleaned up" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/rooms/{roomId}/tokens": {
      "post": {
        "tags": ["Tenant"],
        "summary": "Issue a join token (server-enforced participant cap)",
        "operationId": "issueRoomToken",
        "parameters": [{ "name": "roomId", "in": "path", "required": true, "schema": { "type": "string", "maxLength": 128 } }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RoomTokenRequest" },
              "example": { "identity": "user-42", "name": "Sara", "role": "guest", "ttlSeconds": 600 }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Token issued",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RoomTokenResponse" },
                "example": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "wsUrl": "wss://rtc.videorayan.ir", "identity": "user-42", "expiresAt": "2026-10-26T10:10:00.000Z" }
              }
            }
          },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "description": "Room full or already ended" }
        }
      }
    },
    "/rooms/{roomId}/participants": {
      "get": {
        "tags": ["Tenant"],
        "summary": "List current participants (owner tenant only)",
        "operationId": "listParticipants",
        "parameters": [{ "name": "roomId", "in": "path", "required": true, "schema": { "type": "string", "maxLength": 128 } }],
        "responses": {
          "200": {
            "description": "Participants",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "participants": { "type": "array", "items": { "$ref": "#/components/schemas/Participant" } } }
                }
              }
            }
          },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/rooms/{roomId}/participants/{participantId}": {
      "delete": {
        "tags": ["Tenant"],
        "summary": "Disconnect a participant (owner tenant only)",
        "operationId": "removeParticipant",
        "parameters": [
          { "name": "roomId", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "participantId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "204": { "description": "Participant disconnected" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/health": {
      "get": {
        "tags": ["Ops"],
        "summary": "Liveness — service is up; no sensitive info",
        "operationId": "getHealth",
        "security": [],
        "responses": { "200": { "description": "Service is running" } }
      }
    },
    "/ready": {
      "get": {
        "tags": ["Ops"],
        "summary": "Readiness — dependencies (DB, LiveKit) checked",
        "operationId": "getReady",
        "security": [],
        "responses": { "200": { "description": "Ready" }, "503": { "description": "Dependencies not ready" } }
      }
    },
    "/whoami": {
      "get": {
        "tags": ["Ops"],
        "summary": "Identify the caller's tenant key",
        "description": "Zero-side-effect key check: returns the tenant name behind the provided Bearer key, so integrations can verify credentials before doing real work.",
        "operationId": "getWhoami",
        "responses": {
          "200": {
            "description": "Key is valid",
            "content": {
              "application/json": {
                "schema": { "type": "object", "additionalProperties": false, "required": ["tenant"], "properties": { "tenant": { "type": "string" } } },
                "example": { "tenant": "acme" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/openapi.yaml": {
      "get": {
        "tags": ["Ops"],
        "summary": "Live OpenAPI 3.0 contract for the tenant surface (served by the API itself)",
        "operationId": "getOpenapi",
        "security": [],
        "responses": { "200": { "description": "OpenAPI YAML document" } }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "TenantApiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Tenant API key issued by the operator (rtck_...). Server-side only — never expose to browsers or apps."
      }
    },
    "responses": {
      "Unauthorized": { "description": "Missing/invalid tenant credentials" },
      "Forbidden": { "description": "Authenticated, but not the room owner / not allowed" },
      "NotFound": { "description": "Resource not found (cross-tenant access returns 404 too, to avoid probing)" },
      "RateLimited": { "description": "Rate limit exceeded (120 req/min per key or IP)" }
    },
    "schemas": {
      "PublicRoomCreate": {
        "type": "object",
        "additionalProperties": false,
        "required": ["name"],
        "properties": {
          "name": { "type": "string", "minLength": 1, "maxLength": 64, "description": "Public room handle. Idempotent: same name returns the same room." },
          "password": { "type": "string", "minLength": 1, "maxLength": 64, "description": "Optional join password, verified server-side." }
        }
      },
      "PublicRoomCreated": {
        "type": "object",
        "additionalProperties": false,
        "required": ["roomId", "name"],
        "properties": {
          "roomId": { "type": "string" },
          "name": { "type": "string" }
        }
      },
      "PublicRoomResolved": {
        "type": "object",
        "additionalProperties": false,
        "required": ["roomId", "name", "needsPassword"],
        "properties": {
          "roomId": { "type": "string" },
          "name": { "type": "string" },
          "needsPassword": { "type": "boolean" }
        }
      },
      "GuestTokenRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["identity"],
        "properties": {
          "identity": { "type": "string", "minLength": 1, "maxLength": 40 },
          "password": { "type": "string", "minLength": 1, "maxLength": 64 }
        }
      },
      "GuestTokenResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["token", "identity", "roomName", "expiresAt"],
        "properties": {
          "token": { "type": "string", "description": "LiveKit JWT scoped to this room; use with wss://rtc.videorayan.ir" },
          "identity": { "type": "string" },
          "roomName": { "type": "string" },
          "expiresAt": { "type": "string", "format": "date-time" }
        }
      },
      "RoomCreate": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "externalId": { "type": "string", "maxLength": 128, "description": "Caller-side correlation reference" },
          "maxParticipants": { "type": "integer", "minimum": 1, "maximum": 3, "default": 3 },
          "expiresInSeconds": { "type": "integer", "minimum": 60, "maximum": 86400, "default": 7200 },
          "metadata": { "type": "string", "maxLength": 512 }
        }
      },
      "Room": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "roomId": { "type": "string" },
          "tenantId": { "type": "string" },
          "externalId": { "type": "string", "nullable": true },
          "status": { "type": "string", "enum": ["active", "ended", "expired"] },
          "maxParticipants": { "type": "integer" },
          "createdAt": { "type": "string", "format": "date-time" },
          "expiresAt": { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "RoomTokenRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": ["identity"],
        "properties": {
          "identity": { "type": "string", "minLength": 1, "maxLength": 128 },
          "name": { "type": "string", "maxLength": 128 },
          "role": { "type": "string", "enum": ["host", "guest"], "default": "guest" },
          "ttlSeconds": { "type": "integer", "minimum": 30, "maximum": 3600, "default": 600 }
        }
      },
      "RoomTokenResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["token", "wsUrl", "identity", "expiresAt"],
        "properties": {
          "token": { "type": "string", "description": "LiveKit JWT scoped to this room" },
          "wsUrl": { "type": "string", "example": "wss://rtc.videorayan.ir" },
          "identity": { "type": "string" },
          "expiresAt": { "type": "string", "format": "date-time" }
        }
      },
      "Participant": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "id": { "type": "string" },
          "identity": { "type": "string" },
          "name": { "type": "string", "nullable": true },
          "joinedAt": { "type": "string", "format": "date-time" }
        }
      }
    }
  },
  "security": [{ "TenantApiKey": [] }],
  "externalDocs": {
    "description": "Documentation and guides",
    "url": "https://en.videorayan.ir/docs"
  }
}
