{
  "openapi": "3.1.0",
  "info": {
    "title": "Velo API",
    "version": "v1",
    "summary": "The project API for Velo, a real-time video platform.",
    "description": "Every endpoint a project API key can reach. Requests and responses are JSON, bodies use snake_case, and timestamps are RFC 3339 in UTC. Creation answers 201, deletion answers 204, everything else answers 200. Unsafe calls accept an Idempotency-Key header; replaying the same key with a different body fails with 409 idempotency_key_reuse.",
    "license": {
      "name": "Apache-2.0",
      "identifier": "Apache-2.0"
    }
  },
  "servers": [
    {
      "url": "https://api.usevelo.xyz",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Rooms",
      "description": "A room is created before anyone joins and holds its name, capacity, metadata and template for the life of the call."
    },
    {
      "name": "Participants and data",
      "description": "Who is in the room right now, and the calls that act on them."
    },
    {
      "name": "Tokens",
      "description": "One endpoint. It admits one identity to one room, optionally as a named role."
    },
    {
      "name": "Templates",
      "description": "A reusable bundle of roles and settings that a room points at."
    },
    {
      "name": "Roles",
      "description": "Roles live inside a template and can also be addressed one at a time."
    },
    {
      "name": "Template destinations",
      "description": "Named recording and streaming targets, configured once on a template and referenced by name when a recording or broadcast starts."
    },
    {
      "name": "Room codes",
      "description": "A short, shareable string bound to one room and one role, for putting people into a call without running a token backend."
    },
    {
      "name": "Recordings",
      "description": "Recording is started against a room and then tracked by its egress id."
    },
    {
      "name": "Ingress",
      "description": "Publish into a room from OBS Studio or any RTMP or WHIP encoder. The stream joins as an ordinary participant."
    },
    {
      "name": "Broadcasts",
      "description": "Push a room out to RTMP destinations such as YouTube or Twitch."
    },
    {
      "name": "Sessions",
      "description": "One row per participant per join, with the role they joined as and how long they stayed."
    },
    {
      "name": "Webhooks",
      "description": "Register receivers, then read every delivery attempt and re-queue the ones that failed."
    },
    {
      "name": "Usage and plan",
      "description": "What the project has consumed and what its plan allows."
    }
  ],
  "paths": {
    "/v1/rooms": {
      "post": {
        "tags": [
          "Rooms"
        ],
        "operationId": "createRoom",
        "summary": "Create a room",
        "description": "Creates the room before anyone joins. The room holds its name, capacity, metadata and template for the life of the call. Without template_id the project's default template is used, if it has one.",
        "x-velo-auth": "project",
        "x-velo-idempotent": true,
        "responses": {
          "201": {
            "description": "The room was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Room"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "pattern": "^[a-zA-Z0-9_-]{1,128}$",
                    "description": "Unique within the project."
                  },
                  "max_participants": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "0 means the plan limit applies. Defaults to 0."
                  },
                  "empty_timeout_seconds": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 0,
                    "description": "How long the room survives with nobody in it. Defaults to 300."
                  },
                  "metadata": {
                    "description": "Any JSON. Returned unchanged on every read of the room."
                  },
                  "template_id": {
                    "type": "string",
                    "pattern": "^tpl_[0-9a-f]{12}$",
                    "description": "Which template the room resolves roles against."
                  }
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Rooms"
        ],
        "operationId": "listRooms",
        "summary": "List rooms",
        "description": "Every room in the project, newest first, including rooms that have ended.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "A page of rooms.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, between 1 and 200. Defaults to 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "How many rows to skip. Defaults to 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ]
      }
    },
    "/v1/rooms/{name}": {
      "get": {
        "tags": [
          "Rooms"
        ],
        "operationId": "getRoom",
        "summary": "Read one room",
        "description": "Reads the room record held by the control plane. It does not call the media plane, so it answers for a room nobody has joined yet.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "The room.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Room"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The room name as it was created, without the project prefix.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          }
        ]
      },
      "delete": {
        "tags": [
          "Rooms"
        ],
        "operationId": "deleteRoom",
        "summary": "End the room and disconnect everyone",
        "description": "Closes the room on the media plane and marks it ended. No further tokens can be minted for it, so start another call with a new room.",
        "x-velo-auth": "moderation",
        "x-velo-idempotent": false,
        "responses": {
          "204": {
            "description": "Deleted. No body."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The room name as it was created, without the project prefix.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          }
        ]
      }
    },
    "/v1/rooms/{name}/participants": {
      "get": {
        "tags": [
          "Participants and data"
        ],
        "operationId": "listParticipants",
        "summary": "List everyone currently connected",
        "description": "Asks the media plane who is in the room right now, then pages the answer. Nobody in the room means an empty list, not a 404.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "A page of participants.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ParticipantPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The room name as it was created, without the project prefix.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, between 1 and 200. Defaults to 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "How many rows to skip. Defaults to 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ]
      }
    },
    "/v1/rooms/{name}/participants/{identity}": {
      "get": {
        "tags": [
          "Participants and data"
        ],
        "operationId": "getParticipant",
        "summary": "Read one participant and their tracks",
        "description": "Reads one participant's live state, including the attributes Velo set on them and every track they publish.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "The participant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Participant"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The room name as it was created, without the project prefix.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          },
          {
            "name": "identity",
            "in": "path",
            "required": true,
            "description": "The participant identity carried by the token they joined with.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_.@-]{1,128}$"
            }
          }
        ]
      },
      "delete": {
        "tags": [
          "Participants and data"
        ],
        "operationId": "removeParticipant",
        "summary": "Remove a participant",
        "description": "Disconnects one participant. Their token remains valid, so they can rejoin unless you also disable whatever issued it.",
        "x-velo-auth": "moderation",
        "x-velo-idempotent": false,
        "responses": {
          "204": {
            "description": "Deleted. No body."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The room name as it was created, without the project prefix.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          },
          {
            "name": "identity",
            "in": "path",
            "required": true,
            "description": "The participant identity carried by the token they joined with.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_.@-]{1,128}$"
            }
          }
        ]
      }
    },
    "/v1/rooms/{name}/participants/{identity}/mute": {
      "post": {
        "tags": [
          "Participants and data"
        ],
        "operationId": "muteTrack",
        "summary": "Mute or unmute one track",
        "description": "Muting needs the mute_others permission and unmuting needs unmute_others, so a role can be allowed to quieten someone without being allowed to switch them back on.",
        "x-velo-auth": "moderation",
        "x-velo-idempotent": true,
        "responses": {
          "200": {
            "description": "The track in its new state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "track"
                  ],
                  "properties": {
                    "track": {
                      "$ref": "#/components/schemas/Track"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The room name as it was created, without the project prefix.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          },
          {
            "name": "identity",
            "in": "path",
            "required": true,
            "description": "The participant identity carried by the token they joined with.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_.@-]{1,128}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "track_sid",
                  "muted"
                ],
                "properties": {
                  "track_sid": {
                    "type": "string",
                    "description": "The track to act on, from the participant's tracks list."
                  },
                  "muted": {
                    "type": "boolean",
                    "description": "True to mute, false to unmute. There is no default."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/rooms/{name}/send-data": {
      "post": {
        "tags": [
          "Participants and data"
        ],
        "operationId": "sendData",
        "summary": "Send a data payload into the room",
        "description": "Delivers an arbitrary payload to everyone in the room, or to named identities. The payload is opaque to Velo.",
        "x-velo-auth": "moderation",
        "x-velo-idempotent": true,
        "responses": {
          "200": {
            "description": "The payload was handed to the media plane.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "sent",
                    "bytes"
                  ],
                  "properties": {
                    "sent": {
                      "type": "boolean",
                      "description": "Always true when the call succeeds."
                    },
                    "bytes": {
                      "type": "integer",
                      "description": "The decoded payload size."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The room name as it was created, without the project prefix.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "data"
                ],
                "properties": {
                  "data": {
                    "type": "string",
                    "format": "byte",
                    "description": "The payload, standard base64."
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "reliable",
                      "lossy"
                    ],
                    "description": "Defaults to reliable."
                  },
                  "destination_identities": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Who receives it. An empty list means everyone."
                  },
                  "topic": {
                    "type": "string",
                    "description": "An optional topic the client can filter on."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/rooms/{name}/participants/{identity}/role": {
      "post": {
        "tags": [
          "Participants and data"
        ],
        "operationId": "changeParticipantRole",
        "summary": "Move a participant to another role mid-call",
        "description": "Rewrites the participant's grants and velo.role attribute in place, and the media server pushes a refreshed token to them. max_peer_count is not re-checked for someone already in the room. Emits participant.role_changed.",
        "x-velo-auth": "moderation",
        "x-velo-idempotent": true,
        "responses": {
          "200": {
            "description": "The change, the participant's new state, and a note on what is deferred.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoleChangeResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The room name as it was created, without the project prefix.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          },
          {
            "name": "identity",
            "in": "path",
            "required": true,
            "description": "The participant identity carried by the token they joined with.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_.@-]{1,128}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "role"
                ],
                "properties": {
                  "role": {
                    "type": "string",
                    "pattern": "^[a-zA-Z0-9_-]{1,64}$",
                    "description": "A role that exists in the room's template."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/rooms/{name}/role-changes": {
      "get": {
        "tags": [
          "Participants and data"
        ],
        "operationId": "listRoleChanges",
        "summary": "Read the role change audit trail",
        "description": "Every role change made in this room, newest first, with who made it.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "A page of role changes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoleChangePage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The room name as it was created, without the project prefix.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          },
          {
            "name": "identity",
            "in": "query",
            "required": false,
            "description": "Only changes affecting this participant.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_.@-]{1,128}$"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, between 1 and 200. Defaults to 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "How many rows to skip. Defaults to 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ]
      }
    },
    "/v1/tokens": {
      "post": {
        "tags": [
          "Tokens"
        ],
        "operationId": "mintToken",
        "summary": "Mint a join token",
        "description": "Admits one identity to one room, optionally as a named role. Naming a role replaces permissions with the role's grants, attaches the velo.role and velo.publish.max_kbps attributes, and returns the publish profile. A room that has ended is refused.",
        "x-velo-auth": "project",
        "x-velo-idempotent": true,
        "responses": {
          "201": {
            "description": "The token, the connection url and when it expires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Token"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "room",
                  "identity"
                ],
                "properties": {
                  "room": {
                    "type": "string",
                    "pattern": "^[a-zA-Z0-9_-]{1,128}$",
                    "description": "An existing room in this project."
                  },
                  "identity": {
                    "type": "string",
                    "pattern": "^[a-zA-Z0-9_.@-]{1,128}$",
                    "description": "Who the token is for. Two live participants cannot share an identity."
                  },
                  "name": {
                    "type": "string",
                    "description": "A display name carried into the room."
                  },
                  "metadata": {
                    "type": "string",
                    "description": "An opaque string handed to every other participant."
                  },
                  "ttl_seconds": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "How long the token is valid. 0 takes the server default."
                  },
                  "role": {
                    "type": "string",
                    "pattern": "^[a-zA-Z0-9_-]{1,64}$",
                    "description": "A role in the room's template. Omit it to fall back to the permissions block."
                  },
                  "permissions": {
                    "type": "object",
                    "description": "Used only when no role is named. Each field defaults to true.",
                    "properties": {
                      "can_publish": {
                        "type": [
                          "boolean",
                          "null"
                        ]
                      },
                      "can_subscribe": {
                        "type": [
                          "boolean",
                          "null"
                        ]
                      },
                      "can_publish_data": {
                        "type": [
                          "boolean",
                          "null"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/templates": {
      "post": {
        "tags": [
          "Templates"
        ],
        "operationId": "createTemplate",
        "summary": "Create a template with its roles",
        "description": "Creates a template and every role in one call. Each role is normalised against the documented defaults and then validated, so a role sent as an empty object is still complete.",
        "x-velo-auth": "project",
        "x-velo-idempotent": true,
        "responses": {
          "201": {
            "description": "The template with its roles resolved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Template"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "pattern": "^[a-zA-Z0-9_-]{1,64}$"
                  },
                  "settings": {
                    "$ref": "#/components/schemas/TemplateSettings"
                  },
                  "roles": {
                    "type": "object",
                    "additionalProperties": {
                      "$ref": "#/components/schemas/RoleInput"
                    },
                    "description": "Roles keyed by name. Names that appear here can be referenced from subscribe.to_roles."
                  }
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Templates"
        ],
        "operationId": "listTemplates",
        "summary": "List templates",
        "description": "Every template in the project, each with its roles.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "A page of templates.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplatePage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, between 1 and 200. Defaults to 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "How many rows to skip. Defaults to 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ]
      }
    },
    "/v1/templates/{id}": {
      "get": {
        "tags": [
          "Templates"
        ],
        "operationId": "getTemplate",
        "summary": "Read a template and every role in it",
        "description": "Returns the template with its settings and the full, normalised body of every role.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "The template.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Template"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The template id.",
            "schema": {
              "type": "string",
              "pattern": "^tpl_[0-9a-f]{12}$"
            }
          }
        ]
      },
      "patch": {
        "tags": [
          "Templates"
        ],
        "operationId": "updateTemplate",
        "summary": "Update the name, settings or named roles",
        "description": "Only the fields you send are touched. Roles sent here are upserted whole; roles you leave out are untouched. Deleting a role needs the role endpoint.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "The template after the update.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Template"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The template id.",
            "schema": {
              "type": "string",
              "pattern": "^tpl_[0-9a-f]{12}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "pattern": "^[a-zA-Z0-9_-]{1,64}$"
                  },
                  "settings": {
                    "$ref": "#/components/schemas/TemplateSettings"
                  },
                  "roles": {
                    "type": "object",
                    "additionalProperties": {
                      "$ref": "#/components/schemas/RoleInput"
                    },
                    "description": "Roles keyed by name, each replaced whole."
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Templates"
        ],
        "operationId": "deleteTemplate",
        "summary": "Delete a template",
        "description": "Refused with 409 if the template is the project default, or if any room still points at it.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "204": {
            "description": "Deleted. No body."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The template id.",
            "schema": {
              "type": "string",
              "pattern": "^tpl_[0-9a-f]{12}$"
            }
          }
        ]
      }
    },
    "/v1/templates/{id}/settings": {
      "get": {
        "tags": [
          "Templates"
        ],
        "operationId": "getTemplateSettings",
        "summary": "Read the region setting and what is allowed",
        "description": "Returns the stored region together with every region the API accepts and a note on how far region is honoured today.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "The settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateSettingsDetail"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The template id.",
            "schema": {
              "type": "string",
              "pattern": "^tpl_[0-9a-f]{12}$"
            }
          }
        ]
      },
      "patch": {
        "tags": [
          "Templates"
        ],
        "operationId": "updateTemplateSettings",
        "summary": "Update the region setting",
        "description": "Region is stored and validated. Velo runs one media region today, so it does not yet change where traffic goes.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "The settings after the update.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateSettingsDetail"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The template id.",
            "schema": {
              "type": "string",
              "pattern": "^tpl_[0-9a-f]{12}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TemplateSettings"
              }
            }
          }
        }
      }
    },
    "/v1/templates/{id}/roles/{name}": {
      "put": {
        "tags": [
          "Roles"
        ],
        "operationId": "putRole",
        "summary": "Replace one role whole",
        "description": "Creates the role if it is new and replaces it otherwise. The body is normalised against the defaults first, so anything you leave out reverts to its default rather than keeping the old value.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "The role was replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Role"
                }
              }
            }
          },
          "201": {
            "description": "The role was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Role"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The template id.",
            "schema": {
              "type": "string",
              "pattern": "^tpl_[0-9a-f]{12}$"
            }
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The role name.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,64}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RoleInput"
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Roles"
        ],
        "operationId": "getRole",
        "summary": "Read one role",
        "description": "Returns one role from the template, fully resolved.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "The role.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Role"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The template id.",
            "schema": {
              "type": "string",
              "pattern": "^tpl_[0-9a-f]{12}$"
            }
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The role name.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,64}$"
            }
          }
        ]
      },
      "delete": {
        "tags": [
          "Roles"
        ],
        "operationId": "deleteRole",
        "summary": "Delete one role",
        "description": "Refused with 409 if another role in the template lists this one in subscribe.to_roles. Remove the reference first.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "204": {
            "description": "Deleted. No body."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The template id.",
            "schema": {
              "type": "string",
              "pattern": "^tpl_[0-9a-f]{12}$"
            }
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The role name.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,64}$"
            }
          }
        ]
      }
    },
    "/v1/templates/{id}/destinations": {
      "post": {
        "tags": [
          "Template destinations"
        ],
        "operationId": "createDestination",
        "summary": "Create a named destination",
        "description": "Stores an upload target or a set of RTMP urls under a name. Credentials are encrypted at rest, and the call fails with 503 if the deployment has no encryption key. A recording destination needs a plan that includes recording, and an rtmp destination needs a plan that includes live streaming.",
        "x-velo-auth": "project",
        "x-velo-idempotent": true,
        "responses": {
          "201": {
            "description": "The destination, with secrets redacted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Destination"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The template id.",
            "schema": {
              "type": "string",
              "pattern": "^tpl_[0-9a-f]{12}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "kind",
                  "config"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "pattern": "^[a-zA-Z0-9_-]{1,64}$",
                    "description": "How a recording or broadcast will refer to it."
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "recording",
                      "rtmp"
                    ],
                    "description": "Immutable once created."
                  },
                  "config": {
                    "description": "A recording config when kind is recording, an rtmp config when kind is rtmp.",
                    "oneOf": [
                      {
                        "$ref": "#/components/schemas/RecordingDestinationConfig"
                      },
                      {
                        "$ref": "#/components/schemas/RtmpDestinationConfig"
                      }
                    ]
                  }
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Template destinations"
        ],
        "operationId": "listDestinations",
        "summary": "List destinations on a template",
        "description": "Returns every destination on the template. This list is not paged.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "The destinations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DestinationList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The template id.",
            "schema": {
              "type": "string",
              "pattern": "^tpl_[0-9a-f]{12}$"
            }
          }
        ]
      }
    },
    "/v1/templates/{id}/destinations/{name}": {
      "get": {
        "tags": [
          "Template destinations"
        ],
        "operationId": "getDestination",
        "summary": "Read one destination, with credentials redacted",
        "description": "The stored secret is never returned. An S3 secret reads back as ***, and an RTMP stream key path reads back as REDACTED.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "The destination.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Destination"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The template id.",
            "schema": {
              "type": "string",
              "pattern": "^tpl_[0-9a-f]{12}$"
            }
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The destination name.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,64}$"
            }
          }
        ]
      },
      "patch": {
        "tags": [
          "Template destinations"
        ],
        "operationId": "updateDestination",
        "summary": "Replace a destination config",
        "description": "config is required and replaces the stored configuration whole. Leaving credentials out of an S3 config keeps the stored ones. kind cannot be changed. A destination whose stored kind is recording needs a plan that includes recording, and one whose kind is rtmp needs a plan that includes live streaming.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "The destination after the update.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Destination"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The template id.",
            "schema": {
              "type": "string",
              "pattern": "^tpl_[0-9a-f]{12}$"
            }
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The destination name.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,64}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "config"
                ],
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "recording",
                      "rtmp"
                    ],
                    "description": "If sent it must equal the stored kind."
                  },
                  "config": {
                    "description": "The replacement configuration.",
                    "oneOf": [
                      {
                        "$ref": "#/components/schemas/RecordingDestinationConfig"
                      },
                      {
                        "$ref": "#/components/schemas/RtmpDestinationConfig"
                      }
                    ]
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Template destinations"
        ],
        "operationId": "deleteDestination",
        "summary": "Delete a destination",
        "description": "Refused with 409 while a recording or broadcast is still running against it.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "204": {
            "description": "Deleted. No body."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The template id.",
            "schema": {
              "type": "string",
              "pattern": "^tpl_[0-9a-f]{12}$"
            }
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The destination name.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,64}$"
            }
          }
        ]
      }
    },
    "/v1/rooms/{name}/codes": {
      "post": {
        "tags": [
          "Room codes"
        ],
        "operationId": "createRoomCode",
        "summary": "Create a code",
        "description": "Issues a short string bound to one room and one role. Send either expires_at or ttl_seconds, never both. The code is returned in full here and nowhere else.",
        "x-velo-auth": "project",
        "x-velo-idempotent": true,
        "responses": {
          "201": {
            "description": "The code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomCode"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The room name as it was created, without the project prefix.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "role"
                ],
                "properties": {
                  "role": {
                    "type": "string",
                    "pattern": "^[a-zA-Z0-9_-]{1,64}$",
                    "description": "A role that exists in the room's template."
                  },
                  "expires_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Between 60 seconds and 30 days from now."
                  },
                  "ttl_seconds": {
                    "type": "integer",
                    "minimum": 60,
                    "maximum": 2592000,
                    "description": "An alternative to expires_at. Defaults to 24 hours."
                  },
                  "max_uses": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "description": "Omit for no cap."
                  },
                  "identity": {
                    "type": "string",
                    "pattern": "^[a-zA-Z0-9_.@-]{1,128}$",
                    "description": "Locks the code to one identity, ignoring whatever the holder sends."
                  }
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Room codes"
        ],
        "operationId": "listRoomCodes",
        "summary": "List codes for a room",
        "description": "Every code issued for the room, including expired and disabled ones.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "A page of codes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomCodePage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The room name as it was created, without the project prefix.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, between 1 and 200. Defaults to 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "How many rows to skip. Defaults to 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ]
      }
    },
    "/v1/codes/{code}": {
      "delete": {
        "tags": [
          "Room codes"
        ],
        "operationId": "disableRoomCode",
        "summary": "Disable a code",
        "description": "Takes the code out of use immediately. Tokens already minted from it keep working until they expire.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "204": {
            "description": "Deleted. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "description": "The code string.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/codes/{code}/exchange": {
      "post": {
        "tags": [
          "Room codes"
        ],
        "operationId": "exchangeRoomCode",
        "summary": "Exchange a code for a room token",
        "description": "The one route in the API that takes no credential, because the whole point of a code is that the holder has none. It is rate limited per code and per client address. A disabled, expired, exhausted or unknown code all fail identically with 404 code_not_found, so the code space cannot be probed.",
        "x-velo-auth": "public",
        "x-velo-idempotent": false,
        "responses": {
          "201": {
            "description": "The same body as POST /v1/tokens, ready to hand straight to a client.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Token"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "description": "The code string.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "identity": {
                    "type": "string",
                    "pattern": "^[a-zA-Z0-9_.@-]{1,128}$",
                    "description": "Who to mint for. Ignored when the code is locked to an identity."
                  }
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/rooms/{name}/recordings": {
      "post": {
        "tags": [
          "Recordings"
        ],
        "operationId": "startRecording",
        "summary": "Start recording a room",
        "description": "Starts an egress against the room and returns the recording immediately, before the file exists. Poll the recording or listen for recording.ended. Without a destination the file lands in the project's local recording directory. Recording is not on every plan: a project whose plan does not include it is refused with 402 feature_not_in_plan, naming recording in field. Listing, reading and stopping recordings stay available on every plan.",
        "x-velo-auth": "moderation",
        "x-velo-idempotent": true,
        "responses": {
          "201": {
            "description": "The recording, in its starting state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recording"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The room name as it was created, without the project prefix.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "type"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "room_composite",
                      "track",
                      "participant"
                    ],
                    "description": "What to record."
                  },
                  "track_id": {
                    "type": "string",
                    "description": "Required when type is track, rejected otherwise."
                  },
                  "identity": {
                    "type": "string",
                    "description": "Required when type is participant, rejected otherwise."
                  },
                  "layout": {
                    "type": "string",
                    "enum": [
                      "speaker",
                      "grid"
                    ],
                    "description": "room_composite only. Defaults to speaker."
                  },
                  "audio_only": {
                    "type": "boolean",
                    "description": "room_composite only. Mutually exclusive with video_only."
                  },
                  "video_only": {
                    "type": "boolean",
                    "description": "room_composite only."
                  },
                  "filepath": {
                    "type": "string",
                    "description": "A relative name inside the project recording directory. Defaults to a timestamped .mp4."
                  },
                  "destination": {
                    "type": "string",
                    "description": "The name of a recording destination on the room's template."
                  }
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Recordings"
        ],
        "operationId": "listRoomRecordings",
        "summary": "List recordings for one room",
        "description": "Every recording started against this room, newest first.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "A page of recordings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordingPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The room name as it was created, without the project prefix.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, between 1 and 200. Defaults to 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "How many rows to skip. Defaults to 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ]
      }
    },
    "/v1/recordings": {
      "get": {
        "tags": [
          "Recordings"
        ],
        "operationId": "listRecordings",
        "summary": "List recordings across the project",
        "description": "Every recording in the project, filterable by room, status and start time.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "A page of recordings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordingPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "room",
            "in": "query",
            "required": false,
            "description": "Only recordings for this room.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "One of starting, active, ending, complete, failed, aborted, limit_reached.",
            "schema": {
              "type": "string",
              "enum": [
                "starting",
                "active",
                "ending",
                "complete",
                "failed",
                "aborted",
                "limit_reached"
              ]
            }
          },
          {
            "name": "after",
            "in": "query",
            "required": false,
            "description": "Only rows started at or after this RFC 3339 timestamp.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "before",
            "in": "query",
            "required": false,
            "description": "Only rows started before this RFC 3339 timestamp.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, between 1 and 200. Defaults to 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "How many rows to skip. Defaults to 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ]
      }
    },
    "/v1/recordings/{egress_id}": {
      "get": {
        "tags": [
          "Recordings"
        ],
        "operationId": "getRecording",
        "summary": "Read one recording",
        "description": "Reads the recording. While it is still running Velo asks the media plane for fresh status and writes it back, so the answer is current rather than cached.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "The recording.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recording"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "parameters": [
          {
            "name": "egress_id",
            "in": "path",
            "required": true,
            "description": "The egress id returned when the recording or broadcast started.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          }
        ]
      }
    },
    "/v1/recordings/{egress_id}/stop": {
      "post": {
        "tags": [
          "Recordings"
        ],
        "operationId": "stopRecording",
        "summary": "Stop a recording",
        "description": "Asks the media plane to end the egress. The status moves to ending at least, and the file is finalised shortly after.",
        "x-velo-auth": "moderation",
        "x-velo-idempotent": true,
        "responses": {
          "200": {
            "description": "The recording after the stop.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recording"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "parameters": [
          {
            "name": "egress_id",
            "in": "path",
            "required": true,
            "description": "The egress id returned when the recording or broadcast started.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          }
        ]
      }
    },
    "/v1/rooms/{name}/ingress": {
      "post": {
        "tags": [
          "Ingress"
        ],
        "operationId": "createIngress",
        "summary": "Create an ingress and get its stream key",
        "description": "Gives you a publish url and a stream key for OBS Studio or any RTMP or WHIP encoder. The stream joins the room as an ordinary participant under the identity you name. The key is returned once and only a hash is kept. Live streaming is not on every plan: a project whose plan does not include it is refused with 402 feature_not_in_plan, naming streaming in field. Listing, reading and deleting an ingress stay available on every plan.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "201": {
            "description": "The ingress, including the stream key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngressCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The room name as it was created, without the project prefix.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "input_type",
                  "participant_identity"
                ],
                "properties": {
                  "input_type": {
                    "type": "string",
                    "enum": [
                      "rtmp",
                      "whip"
                    ]
                  },
                  "participant_identity": {
                    "type": "string",
                    "pattern": "^[a-zA-Z0-9_.@-]{1,128}$",
                    "description": "The identity the stream joins as."
                  },
                  "participant_name": {
                    "type": "string",
                    "maxLength": 128,
                    "description": "A display name for the stream."
                  },
                  "enable_transcoding": {
                    "type": [
                      "boolean",
                      "null"
                    ],
                    "description": "Leave unset to take the media plane's default for the input type."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/ingress": {
      "get": {
        "tags": [
          "Ingress"
        ],
        "operationId": "listIngresses",
        "summary": "List ingresses",
        "description": "Every ingress in the project, filterable by room, status and input type.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "A page of ingresses.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngressPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "room",
            "in": "query",
            "required": false,
            "description": "Only ingresses for this room.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "One of inactive, buffering, publishing, error, complete, deleted.",
            "schema": {
              "type": "string",
              "enum": [
                "inactive",
                "buffering",
                "publishing",
                "error",
                "complete",
                "deleted"
              ]
            }
          },
          {
            "name": "input_type",
            "in": "query",
            "required": false,
            "description": "rtmp or whip.",
            "schema": {
              "type": "string",
              "enum": [
                "rtmp",
                "whip"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, between 1 and 200. Defaults to 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "How many rows to skip. Defaults to 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ]
      }
    },
    "/v1/ingress/{ingress_id}": {
      "get": {
        "tags": [
          "Ingress"
        ],
        "operationId": "getIngress",
        "summary": "Read one ingress",
        "description": "Reads the ingress and, unless it has been deleted, refreshes its status from the media plane first.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "The ingress.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ingress"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "parameters": [
          {
            "name": "ingress_id",
            "in": "path",
            "required": true,
            "description": "The ingress id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          }
        ]
      },
      "delete": {
        "tags": [
          "Ingress"
        ],
        "operationId": "deleteIngress",
        "summary": "Delete an ingress",
        "description": "Retires the endpoint and invalidates its stream key. The row stays with status deleted so history is not lost.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "204": {
            "description": "Deleted. No body."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "parameters": [
          {
            "name": "ingress_id",
            "in": "path",
            "required": true,
            "description": "The ingress id.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          }
        ]
      }
    },
    "/v1/rooms/{name}/broadcasts": {
      "post": {
        "tags": [
          "Broadcasts"
        ],
        "operationId": "startBroadcast",
        "summary": "Start a broadcast",
        "description": "Pushes the composited room out to RTMP destinations such as YouTube or Twitch. Send urls inline or name a template destination, never both. Stream keys are redacted everywhere they are read back. Live streaming is not on every plan: a project whose plan does not include it is refused with 402 feature_not_in_plan, naming streaming in field. Listing, reading and stopping broadcasts stay available on every plan.",
        "x-velo-auth": "moderation",
        "x-velo-idempotent": true,
        "responses": {
          "201": {
            "description": "The broadcast, in its starting state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Broadcast"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The room name as it was created, without the project prefix.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "urls": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Between one and eight rtmp:// or rtmps:// urls. Required unless destination is given."
                  },
                  "destination": {
                    "type": "string",
                    "description": "The name of an rtmp destination on the room's template."
                  },
                  "layout": {
                    "type": "string",
                    "enum": [
                      "grid",
                      "speaker"
                    ],
                    "description": "Defaults to grid."
                  },
                  "audio_only": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/broadcasts": {
      "get": {
        "tags": [
          "Broadcasts"
        ],
        "operationId": "listBroadcasts",
        "summary": "List broadcasts",
        "description": "Every broadcast in the project, filterable by room, status and start time.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "A page of broadcasts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BroadcastPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "room",
            "in": "query",
            "required": false,
            "description": "Only broadcasts for this room.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "One of starting, active, ending, complete, failed, aborted, limit_reached.",
            "schema": {
              "type": "string",
              "enum": [
                "starting",
                "active",
                "ending",
                "complete",
                "failed",
                "aborted",
                "limit_reached"
              ]
            }
          },
          {
            "name": "after",
            "in": "query",
            "required": false,
            "description": "Only rows started at or after this RFC 3339 timestamp.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "before",
            "in": "query",
            "required": false,
            "description": "Only rows started before this RFC 3339 timestamp.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, between 1 and 200. Defaults to 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "How many rows to skip. Defaults to 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ]
      }
    },
    "/v1/broadcasts/{egress_id}": {
      "get": {
        "tags": [
          "Broadcasts"
        ],
        "operationId": "getBroadcast",
        "summary": "Read one broadcast",
        "description": "Reads the broadcast, refreshing its status from the media plane while it is still running.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "The broadcast.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Broadcast"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "parameters": [
          {
            "name": "egress_id",
            "in": "path",
            "required": true,
            "description": "The egress id returned when the recording or broadcast started.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          }
        ]
      }
    },
    "/v1/broadcasts/{egress_id}/stop": {
      "post": {
        "tags": [
          "Broadcasts"
        ],
        "operationId": "stopBroadcast",
        "summary": "Stop a broadcast",
        "description": "Ends the egress. The status moves to ending at least, and settles once the media plane confirms.",
        "x-velo-auth": "moderation",
        "x-velo-idempotent": true,
        "responses": {
          "200": {
            "description": "The broadcast after the stop.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Broadcast"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "parameters": [
          {
            "name": "egress_id",
            "in": "path",
            "required": true,
            "description": "The egress id returned when the recording or broadcast started.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          }
        ]
      }
    },
    "/v1/sessions": {
      "get": {
        "tags": [
          "Sessions"
        ],
        "operationId": "listSessions",
        "summary": "List sessions",
        "description": "One row per participant per join, with the role they joined as, how long they stayed and how many tracks they published.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "A page of sessions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "room",
            "in": "query",
            "required": false,
            "description": "Only sessions in this room.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "identity",
            "in": "query",
            "required": false,
            "description": "Only sessions for this participant identity.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "role",
            "in": "query",
            "required": false,
            "description": "Only sessions joined under this role.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "active",
            "in": "query",
            "required": false,
            "description": "true for sessions still connected, false for finished ones.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "after",
            "in": "query",
            "required": false,
            "description": "Only sessions joined at or after this RFC 3339 timestamp.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "before",
            "in": "query",
            "required": false,
            "description": "Only sessions joined before this RFC 3339 timestamp.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, between 1 and 200. Defaults to 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "How many rows to skip. Defaults to 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ]
      }
    },
    "/v1/sessions/{id}": {
      "get": {
        "tags": [
          "Sessions"
        ],
        "operationId": "getSession",
        "summary": "Read one session and the tracks it published",
        "description": "The session with every track the participant published, each with when it started and when it ended.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "The session and its tracks.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionDetail"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The session id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ]
      }
    },
    "/v1/rooms/{name}/sessions": {
      "get": {
        "tags": [
          "Sessions"
        ],
        "operationId": "listRoomSessions",
        "summary": "List sessions for one room",
        "description": "The same filters as the project-wide list, scoped to one room.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "A page of sessions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The room name as it was created, without the project prefix.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,128}$"
            }
          },
          {
            "name": "identity",
            "in": "query",
            "required": false,
            "description": "Only sessions for this participant identity.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "role",
            "in": "query",
            "required": false,
            "description": "Only sessions joined under this role.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "active",
            "in": "query",
            "required": false,
            "description": "true for sessions still connected, false for finished ones.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "after",
            "in": "query",
            "required": false,
            "description": "Only sessions joined at or after this RFC 3339 timestamp.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "before",
            "in": "query",
            "required": false,
            "description": "Only sessions joined before this RFC 3339 timestamp.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, between 1 and 200. Defaults to 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "How many rows to skip. Defaults to 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ]
      }
    },
    "/v1/webhook-endpoints": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "createWebhookEndpoint",
        "summary": "Register an endpoint and receive its signing secret once",
        "description": "Registers a receiver. The url is resolved and checked before it is stored, so an address on a private network is refused. The signing secret is in this response and nowhere else.",
        "x-velo-auth": "project",
        "x-velo-idempotent": true,
        "responses": {
          "201": {
            "description": "The endpoint and its signing secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpointCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "An http or https url that resolves to a public address."
                  }
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "listWebhookEndpoints",
        "summary": "List endpoints",
        "description": "Every registered receiver. Secrets are not included, because Velo cannot show them again.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "A page of endpoints.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpointPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, between 1 and 200. Defaults to 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "How many rows to skip. Defaults to 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ]
      }
    },
    "/v1/webhook-endpoints/{id}": {
      "delete": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "deleteWebhookEndpoint",
        "summary": "Delete an endpoint",
        "description": "Stops delivery to this receiver. Deliveries already queued for it are dropped.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "204": {
            "description": "Deleted. No body."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The endpoint id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ]
      }
    },
    "/v1/webhook-deliveries": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "listWebhookDeliveries",
        "summary": "List delivery attempts",
        "description": "Every delivery Velo has queued for this project, newest first, with the exact payload, the attempt count and the last error. This is where you look when a receiver stopped hearing from Velo.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "A page of deliveries.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDeliveryPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "pending, delivered or failed.",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "delivered",
                "failed"
              ]
            }
          },
          {
            "name": "event_type",
            "in": "query",
            "required": false,
            "description": "An event name such as recording.ended.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "endpoint_id",
            "in": "query",
            "required": false,
            "description": "Only deliveries for one endpoint.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, between 1 and 200. Defaults to 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "How many rows to skip. Defaults to 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ]
      }
    },
    "/v1/webhook-deliveries/{id}/retry": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "retryWebhookDelivery",
        "summary": "Re-queue a failed delivery",
        "description": "Resets the delivery to pending with the attempt count back at zero, so the full retry schedule runs again. Only a delivery whose status is failed can be retried.",
        "x-velo-auth": "project",
        "x-velo-idempotent": true,
        "responses": {
          "200": {
            "description": "The delivery, back in the queue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDelivery"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The delivery id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ]
      }
    },
    "/v1/usage": {
      "get": {
        "tags": [
          "Usage and plan"
        ],
        "operationId": "getUsage",
        "summary": "Participant minutes per day for a period",
        "description": "Daily participant minutes and session counts, with the plan status attached. Defaults to the last 30 days.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "Usage for the period.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "First day, YYYY-MM-DD. Defaults to 29 days before to.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Last day, YYYY-MM-DD. Defaults to today in UTC.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/plan": {
      "get": {
        "tags": [
          "Usage and plan"
        ],
        "operationId": "getPlan",
        "summary": "The plan, quota state and current limits",
        "description": "What the project is on, what it has used this period and how many rooms are open right now. features is a sibling of limits and says whether the plan includes recording and live streaming.",
        "x-velo-auth": "project",
        "x-velo-idempotent": false,
        "responses": {
          "200": {
            "description": "The plan status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "A project API key, prefixed vk_, in the Authorization header. Endpoints marked as moderation also accept a room token minted by POST /v1/tokens, in which case the role the token carries must hold the matching permission."
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request was malformed or a field failed validation.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "The credential was missing, malformed or not accepted.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "A plan limit was reached, the plan does not include the feature named in field, the project is suspended or exceeded, or the wallet is below the floor needed to start a session. Recording and live streaming are not on every plan: starting a recording, starting a broadcast, creating an ingress and creating or updating a template destination are refused with feature_not_in_plan when the project's plan does not include the feature. Listing, reading, stopping and deleting are never refused for that reason.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The credential was accepted but the acting role lacks the permission named in field.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "NotFound": {
        "description": "The addressed resource does not exist in this project.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "Conflict": {
        "description": "The request collided with the current state of the resource.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "The request body exceeds 65536 bytes.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "The rate limit was exceeded. Retry-After carries the wait in seconds.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "InternalError": {
        "description": "Velo failed to handle the request.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "ServiceUnavailable": {
        "description": "A dependency Velo needs for this call is not configured or not reachable.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "A stable machine-readable code such as room_not_found or quota_exceeded."
          },
          "message": {
            "type": "string",
            "description": "A sentence explaining what went wrong, safe to log."
          },
          "field": {
            "type": "string",
            "description": "The request field or permission the failure is attributed to. Absent when the failure is not about one field."
          }
        }
      },
      "ErrorEnvelope": {
        "type": "object",
        "required": [
          "error"
        ],
        "description": "Every 4xx and 5xx response in the API uses this envelope and nothing else.",
        "properties": {
          "error": {
            "$ref": "#/components/schemas/Error"
          }
        }
      },
      "Room": {
        "type": "object",
        "required": [
          "id",
          "project_id",
          "name",
          "namespaced_name",
          "max_participants",
          "empty_timeout_seconds",
          "status",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Velo's internal identifier for the room row."
          },
          "project_id": {
            "type": "string",
            "description": "The project the room belongs to."
          },
          "name": {
            "type": "string",
            "description": "The name you created the room with."
          },
          "namespaced_name": {
            "type": "string",
            "description": "The name the media plane sees, which is the project id, a dot, then the room name."
          },
          "max_participants": {
            "type": "integer",
            "description": "The cap requested at creation. Zero means the plan limit applies."
          },
          "empty_timeout_seconds": {
            "type": "integer",
            "description": "How long the room stays open with nobody in it. Defaults to 300."
          },
          "metadata": {
            "description": "Whatever JSON you attached at creation, returned unchanged."
          },
          "status": {
            "type": "string",
            "description": "Either open or ended. An ended room cannot mint new tokens."
          },
          "template_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The template the room resolves roles against, or null if the project has no default template."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the first participant joined. Absent until then."
          },
          "ended_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the room finished. Absent while it is open."
          }
        }
      },
      "Track": {
        "type": "object",
        "required": [
          "sid",
          "type",
          "source",
          "name",
          "muted"
        ],
        "properties": {
          "sid": {
            "type": "string",
            "description": "The track identifier assigned by the media server."
          },
          "type": {
            "type": "string",
            "description": "audio, video or data."
          },
          "source": {
            "type": "string",
            "description": "camera, microphone, screen_share, screen_share_audio or unknown."
          },
          "name": {
            "type": "string",
            "description": "The name the publisher gave the track."
          },
          "muted": {
            "type": "boolean"
          }
        }
      },
      "Participant": {
        "type": "object",
        "required": [
          "sid",
          "identity",
          "name",
          "state",
          "kind",
          "metadata",
          "is_publisher",
          "tracks"
        ],
        "properties": {
          "sid": {
            "type": "string",
            "description": "The participant identifier assigned by the media server for this join."
          },
          "identity": {
            "type": "string",
            "description": "The identity the token was minted for."
          },
          "name": {
            "type": "string",
            "description": "The display name carried by the token."
          },
          "state": {
            "type": "string",
            "description": "joining, joined, active or disconnected."
          },
          "kind": {
            "type": "string",
            "description": "standard, ingress, egress, sip or agent."
          },
          "metadata": {
            "type": "string",
            "description": "The metadata string carried by the token."
          },
          "attributes": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Server-set attributes, including velo.role and velo.publish.max_kbps."
          },
          "is_publisher": {
            "type": "boolean"
          },
          "joined_at": {
            "type": "string",
            "format": "date-time"
          },
          "tracks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Track"
            },
            "description": "Every track this participant currently publishes."
          }
        }
      },
      "AudioProfile": {
        "type": "object",
        "required": [
          "bitrate",
          "codec"
        ],
        "properties": {
          "bitrate": {
            "type": "integer",
            "description": "Audio bitrate in kbps, between 6 and 510. Defaults to 32."
          },
          "codec": {
            "type": "string",
            "enum": [
              "opus"
            ],
            "description": "Only opus is accepted."
          }
        }
      },
      "VideoProfile": {
        "type": "object",
        "required": [
          "width",
          "height",
          "bitrate",
          "framerate",
          "codec"
        ],
        "properties": {
          "width": {
            "type": "integer",
            "description": "Between 16 and 7680. Defaults to 1280."
          },
          "height": {
            "type": "integer",
            "description": "Between 16 and 4320. Defaults to 720."
          },
          "bitrate": {
            "type": "integer",
            "description": "Between 8 and 20000 kbps. Defaults to 1200."
          },
          "framerate": {
            "type": "integer",
            "description": "Between 1 and 120. Defaults to 30."
          },
          "codec": {
            "type": "string",
            "enum": [
              "vp8",
              "vp9",
              "h264",
              "av1"
            ],
            "description": "Defaults to vp8."
          }
        }
      },
      "ScreenProfile": {
        "type": "object",
        "required": [
          "width",
          "height",
          "bitrate",
          "framerate"
        ],
        "properties": {
          "width": {
            "type": "integer",
            "description": "Defaults to 1920."
          },
          "height": {
            "type": "integer",
            "description": "Defaults to 1080."
          },
          "bitrate": {
            "type": "integer",
            "description": "Defaults to 1500 kbps."
          },
          "framerate": {
            "type": "integer",
            "description": "Defaults to 10."
          }
        }
      },
      "SimulcastLayer": {
        "type": "object",
        "required": [
          "rid",
          "scale_down_by",
          "max_bitrate",
          "max_framerate"
        ],
        "properties": {
          "rid": {
            "type": "string",
            "enum": [
              "f",
              "h",
              "q"
            ],
            "description": "The layer identifier, full, half or quarter."
          },
          "scale_down_by": {
            "type": "number",
            "description": "1 on the first layer, and strictly larger on each layer after it."
          },
          "max_bitrate": {
            "type": "integer",
            "description": "Between 8 and 20000 kbps."
          },
          "max_framerate": {
            "type": "integer",
            "description": "Between 1 and 120."
          }
        }
      },
      "Simulcast": {
        "type": "object",
        "required": [
          "layers"
        ],
        "properties": {
          "layers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SimulcastLayer"
            },
            "description": "At most three layers, largest first."
          }
        }
      },
      "SubscribeDegradation": {
        "type": "object",
        "required": [
          "packet_loss_threshold",
          "degrade_grace_seconds",
          "recover_grace_seconds"
        ],
        "properties": {
          "packet_loss_threshold": {
            "type": "integer",
            "description": "Loss percentage, 0 to 100, at which the client should drop a layer. Defaults to 25."
          },
          "degrade_grace_seconds": {
            "type": "integer",
            "description": "How long loss must persist before degrading. Defaults to 1."
          },
          "recover_grace_seconds": {
            "type": "integer",
            "description": "How long the link must be clean before recovering. Defaults to 4."
          }
        }
      },
      "RolePublish": {
        "type": "object",
        "required": [
          "allowed",
          "audio",
          "video",
          "screen",
          "simulcast"
        ],
        "properties": {
          "allowed": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "audio",
                "video",
                "screen"
              ]
            },
            "description": "Which sources the role may publish. Defaults to audio and video."
          },
          "audio": {
            "$ref": "#/components/schemas/AudioProfile"
          },
          "video": {
            "$ref": "#/components/schemas/VideoProfile"
          },
          "screen": {
            "$ref": "#/components/schemas/ScreenProfile"
          },
          "simulcast": {
            "$ref": "#/components/schemas/Simulcast"
          }
        }
      },
      "RoleSubscribe": {
        "type": "object",
        "required": [
          "enabled",
          "to_roles",
          "max_bitrate",
          "degradation"
        ],
        "properties": {
          "enabled": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether the role receives media at all. Defaults to true."
          },
          "to_roles": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The roles this role subscribes to. Every entry must name a role in the same template."
          },
          "max_bitrate": {
            "type": "integer",
            "description": "Total downstream ceiling in kbps, between 8 and 100000. Defaults to 3200."
          },
          "degradation": {
            "$ref": "#/components/schemas/SubscribeDegradation"
          }
        }
      },
      "RolePermissions": {
        "type": "object",
        "required": [
          "end_room",
          "remove_others",
          "mute_others",
          "unmute_others",
          "change_role",
          "start_recording",
          "stop_recording",
          "send_data",
          "update_own_metadata"
        ],
        "description": "The moderation actions a room token minted for this role may take. All default to false.",
        "properties": {
          "end_room": {
            "type": "boolean"
          },
          "remove_others": {
            "type": "boolean"
          },
          "mute_others": {
            "type": "boolean"
          },
          "unmute_others": {
            "type": "boolean"
          },
          "change_role": {
            "type": "boolean"
          },
          "start_recording": {
            "type": "boolean"
          },
          "stop_recording": {
            "type": "boolean"
          },
          "send_data": {
            "type": "boolean"
          },
          "update_own_metadata": {
            "type": "boolean"
          }
        }
      },
      "Role": {
        "type": "object",
        "required": [
          "name",
          "publish",
          "subscribe",
          "permissions",
          "priority",
          "max_peer_count",
          "hidden"
        ],
        "properties": {
          "name": {
            "type": "string",
            "pattern": "^[a-zA-Z0-9_-]{1,64}$",
            "description": "Unique within the template."
          },
          "publish": {
            "$ref": "#/components/schemas/RolePublish"
          },
          "subscribe": {
            "$ref": "#/components/schemas/RoleSubscribe"
          },
          "permissions": {
            "$ref": "#/components/schemas/RolePermissions"
          },
          "priority": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5,
            "description": "1 is the most important. Defaults to 3."
          },
          "max_peer_count": {
            "type": "integer",
            "description": "-1 admits nobody, 0 is unlimited, a positive number caps concurrent holders. Defaults to 0."
          },
          "hidden": {
            "type": "boolean",
            "description": "A hidden participant is not visible to others in the room."
          }
        }
      },
      "RoleInput": {
        "type": "object",
        "description": "Every field is optional. Omitted fields take the documented default, and an existing role keeps nothing it is not sent.",
        "properties": {
          "name": {
            "type": "string",
            "description": "If sent it must match the key or path segment the role is filed under."
          },
          "publish": {
            "$ref": "#/components/schemas/RolePublish"
          },
          "subscribe": {
            "$ref": "#/components/schemas/RoleSubscribe"
          },
          "permissions": {
            "$ref": "#/components/schemas/RolePermissions"
          },
          "priority": {
            "type": "integer"
          },
          "max_peer_count": {
            "type": "integer"
          },
          "hidden": {
            "type": "boolean"
          }
        }
      },
      "PublishProfile": {
        "type": "object",
        "required": [
          "role",
          "enforcement",
          "note",
          "allowed",
          "audio",
          "video",
          "screen",
          "simulcast",
          "subscribe",
          "priority",
          "max_publish_kbps"
        ],
        "description": "Client hints returned alongside a role token. Only the grants inside the token and the bitrate ceiling are checked server side.",
        "properties": {
          "role": {
            "type": "string"
          },
          "enforcement": {
            "type": "string",
            "description": "Always server_measured."
          },
          "note": {
            "type": "string",
            "description": "Prose explaining which parts of the profile are enforced."
          },
          "allowed": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "audio": {
            "$ref": "#/components/schemas/AudioProfile"
          },
          "video": {
            "$ref": "#/components/schemas/VideoProfile"
          },
          "screen": {
            "$ref": "#/components/schemas/ScreenProfile"
          },
          "simulcast": {
            "$ref": "#/components/schemas/Simulcast"
          },
          "subscribe": {
            "type": "object",
            "required": [
              "max_bitrate",
              "degradation"
            ],
            "properties": {
              "max_bitrate": {
                "type": "integer"
              },
              "degradation": {
                "$ref": "#/components/schemas/SubscribeDegradation"
              }
            }
          },
          "priority": {
            "type": "integer"
          },
          "max_publish_kbps": {
            "type": "integer",
            "description": "The measured ceiling carried in the token as velo.publish.max_kbps. Sustained overage raises participant.publish_limit_exceeded."
          }
        }
      },
      "Token": {
        "type": "object",
        "required": [
          "token",
          "url",
          "expires_at"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "The room token. Safe to hand to a browser."
          },
          "url": {
            "type": "string",
            "description": "The websocket url the client should connect to."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "role": {
            "type": "string",
            "description": "The role the token was minted for. Absent when no role was requested."
          },
          "publish_profile": {
            "$ref": "#/components/schemas/PublishProfile"
          }
        }
      },
      "TemplateSettings": {
        "type": "object",
        "required": [
          "region"
        ],
        "properties": {
          "region": {
            "type": "string",
            "enum": [
              "auto",
              "af-east",
              "eu-west",
              "us-east"
            ],
            "description": "Defaults to auto."
          }
        }
      },
      "TemplateSettingsDetail": {
        "type": "object",
        "required": [
          "region",
          "allowed_regions",
          "region_enforced",
          "region_note"
        ],
        "properties": {
          "region": {
            "type": "string",
            "description": "The region stored on this template."
          },
          "allowed_regions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Every region the API will accept."
          },
          "region_enforced": {
            "type": "boolean",
            "description": "False while Velo runs a single media region."
          },
          "region_note": {
            "type": "string",
            "description": "Prose explaining what region does and does not do today."
          }
        }
      },
      "Template": {
        "type": "object",
        "required": [
          "id",
          "project_id",
          "name",
          "is_default",
          "settings",
          "roles",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^tpl_[0-9a-f]{12}$"
          },
          "project_id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "pattern": "^[a-zA-Z0-9_-]{1,64}$"
          },
          "is_default": {
            "type": "boolean",
            "description": "The default template is used by rooms created without a template_id, and cannot be deleted."
          },
          "settings": {
            "$ref": "#/components/schemas/TemplateSettings"
          },
          "roles": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/Role"
            },
            "description": "Every role in the template, keyed by role name."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RedactedCredentials": {
        "type": "object",
        "required": [
          "access_key_id",
          "secret_access_key"
        ],
        "properties": {
          "access_key_id": {
            "type": "string"
          },
          "secret_access_key": {
            "type": "string",
            "description": "Always the literal *** on read. The stored secret is encrypted at rest and never returned."
          }
        }
      },
      "RecordingDestinationConfig": {
        "type": "object",
        "required": [
          "type"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "local",
              "s3"
            ],
            "description": "local writes to the media node's recording directory. s3 uploads to your bucket."
          },
          "bucket": {
            "type": "string",
            "description": "Required when type is s3."
          },
          "region": {
            "type": "string",
            "description": "Required when type is s3."
          },
          "prefix": {
            "type": "string",
            "description": "Key prefix inside the bucket."
          },
          "credentials": {
            "$ref": "#/components/schemas/RedactedCredentials"
          }
        }
      },
      "RtmpDestinationConfig": {
        "type": "object",
        "required": [
          "urls",
          "audio_only"
        ],
        "properties": {
          "urls": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Between one and eight rtmp:// or rtmps:// urls. Stream keys are redacted on read."
          },
          "layout": {
            "type": "string",
            "enum": [
              "grid",
              "speaker"
            ]
          },
          "audio_only": {
            "type": "boolean"
          }
        }
      },
      "Destination": {
        "type": "object",
        "required": [
          "id",
          "template_id",
          "name",
          "kind",
          "config",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^dst_[0-9a-f]{12}$"
          },
          "template_id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "pattern": "^[a-zA-Z0-9_-]{1,64}$",
            "description": "Unique within the template. This is the name a recording or broadcast refers to."
          },
          "kind": {
            "type": "string",
            "enum": [
              "recording",
              "rtmp"
            ],
            "description": "Immutable once created."
          },
          "config": {
            "description": "A RecordingDestinationConfig when kind is recording, an RtmpDestinationConfig when kind is rtmp. Secrets are redacted.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/RecordingDestinationConfig"
              },
              {
                "$ref": "#/components/schemas/RtmpDestinationConfig"
              }
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RoomCode": {
        "type": "object",
        "required": [
          "id",
          "project_id",
          "room",
          "role",
          "code",
          "expires_at",
          "uses",
          "disabled",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "project_id": {
            "type": "string"
          },
          "room": {
            "type": "string",
            "description": "The room the code admits the holder to."
          },
          "role": {
            "type": "string",
            "description": "The role the minted token carries."
          },
          "code": {
            "type": "string",
            "description": "The shareable string. This is the only place it is returned in full."
          },
          "identity": {
            "type": [
              "string",
              "null"
            ],
            "description": "When set, the code always mints for this identity and ignores the one the holder sends."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "max_uses": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Null means no cap."
          },
          "uses": {
            "type": "integer",
            "description": "How many times the code has been exchanged."
          },
          "disabled": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Recording": {
        "type": "object",
        "required": [
          "id",
          "project_id",
          "room",
          "namespaced_room",
          "egress_id",
          "type",
          "status",
          "filepath",
          "started_at",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "project_id": {
            "type": "string"
          },
          "room": {
            "type": "string"
          },
          "namespaced_room": {
            "type": "string"
          },
          "egress_id": {
            "type": "string",
            "description": "The handle every later call uses."
          },
          "type": {
            "type": "string",
            "enum": [
              "room_composite",
              "track",
              "participant"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "starting",
              "active",
              "ending",
              "complete",
              "failed",
              "aborted",
              "limit_reached"
            ]
          },
          "filepath": {
            "type": "string",
            "description": "Where the file is written, inside the project's recording directory."
          },
          "destination": {
            "type": "string",
            "description": "The template destination the upload used, if one was named."
          },
          "destination_id": {
            "type": "string",
            "description": "The id of that destination."
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "ended_at": {
            "type": "string",
            "format": "date-time"
          },
          "error": {
            "type": "string",
            "description": "Set when the status is failed."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Ingress": {
        "type": "object",
        "required": [
          "id",
          "project_id",
          "room",
          "namespaced_room",
          "ingress_id",
          "input_type",
          "participant_identity",
          "url",
          "status",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "project_id": {
            "type": "string"
          },
          "room": {
            "type": "string"
          },
          "namespaced_room": {
            "type": "string"
          },
          "ingress_id": {
            "type": "string"
          },
          "input_type": {
            "type": "string",
            "enum": [
              "rtmp",
              "whip"
            ]
          },
          "participant_identity": {
            "type": "string",
            "description": "The identity the incoming stream joins the room as."
          },
          "participant_name": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "description": "The publish url with the stream key replaced by REDACTED."
          },
          "status": {
            "type": "string",
            "enum": [
              "inactive",
              "buffering",
              "publishing",
              "error",
              "complete",
              "deleted"
            ]
          },
          "error": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "deleted_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "IngressCreated": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Ingress"
          },
          {
            "type": "object",
            "required": [
              "stream_key"
            ],
            "properties": {
              "stream_key": {
                "type": "string",
                "description": "Returned once, on creation. Velo keeps only a hash of it, so it cannot be read back."
              }
            }
          }
        ]
      },
      "Broadcast": {
        "type": "object",
        "required": [
          "id",
          "project_id",
          "room",
          "namespaced_room",
          "egress_id",
          "urls",
          "status",
          "started_at",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "project_id": {
            "type": "string"
          },
          "room": {
            "type": "string"
          },
          "namespaced_room": {
            "type": "string"
          },
          "egress_id": {
            "type": "string"
          },
          "urls": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The destination urls with the stream key path replaced by REDACTED."
          },
          "status": {
            "type": "string",
            "enum": [
              "starting",
              "active",
              "ending",
              "complete",
              "failed",
              "aborted",
              "limit_reached"
            ]
          },
          "destination": {
            "type": "string",
            "description": "The template destination used, if one was named."
          },
          "destination_id": {
            "type": "string"
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "ended_at": {
            "type": "string",
            "format": "date-time"
          },
          "error": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Session": {
        "type": "object",
        "required": [
          "id",
          "room",
          "participant_identity",
          "participant_sid",
          "role",
          "joined_at",
          "duration_seconds",
          "track_count"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "One row per participant per join."
          },
          "room": {
            "type": "string"
          },
          "participant_identity": {
            "type": "string"
          },
          "participant_sid": {
            "type": "string"
          },
          "role": {
            "type": [
              "string",
              "null"
            ],
            "description": "The role the participant joined as, or null when the token carried none."
          },
          "joined_at": {
            "type": "string",
            "format": "date-time"
          },
          "left_at": {
            "type": "string",
            "format": "date-time",
            "description": "Absent while the participant is still connected."
          },
          "duration_seconds": {
            "type": "integer",
            "description": "Measured to left_at, or to now while the session is open."
          },
          "track_count": {
            "type": "integer",
            "description": "How many tracks the participant published during the session."
          }
        }
      },
      "SessionTrack": {
        "type": "object",
        "required": [
          "track_sid",
          "kind",
          "source",
          "muted",
          "started_at"
        ],
        "properties": {
          "track_sid": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "description": "audio, video or data."
          },
          "source": {
            "type": "string",
            "description": "camera, microphone, screen_share, screen_share_audio or unknown."
          },
          "muted": {
            "type": "boolean"
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "ended_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SessionDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Session"
          },
          {
            "type": "object",
            "required": [
              "namespaced_room",
              "tracks"
            ],
            "properties": {
              "namespaced_room": {
                "type": "string"
              },
              "tracks": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/SessionTrack"
                },
                "description": "Every track published during the session, in the order they started."
              }
            }
          }
        ]
      },
      "RoleChange": {
        "type": "object",
        "required": [
          "id",
          "project_id",
          "room",
          "participant_identity",
          "from_role",
          "to_role",
          "changed_by_identity",
          "changed_by_api_key_prefix",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "project_id": {
            "type": "string"
          },
          "room": {
            "type": "string"
          },
          "participant_identity": {
            "type": "string"
          },
          "from_role": {
            "type": [
              "string",
              "null"
            ],
            "description": "Null when the participant carried no role before the change."
          },
          "to_role": {
            "type": "string"
          },
          "changed_by_identity": {
            "type": [
              "string",
              "null"
            ],
            "description": "Set when a room token made the change."
          },
          "changed_by_api_key_prefix": {
            "type": [
              "string",
              "null"
            ],
            "description": "Set when a project API key made the change."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RoleChangeResult": {
        "type": "object",
        "required": [
          "role_change",
          "participant",
          "applies_live",
          "note"
        ],
        "properties": {
          "role_change": {
            "$ref": "#/components/schemas/RoleChange"
          },
          "participant": {
            "$ref": "#/components/schemas/Participant"
          },
          "applies_live": {
            "type": "boolean",
            "description": "Always true. Grants and the velo.role attribute reach the participant immediately."
          },
          "note": {
            "type": "string",
            "description": "Prose on what does not change until the participant republishes."
          }
        }
      },
      "WebhookEndpoint": {
        "type": "object",
        "required": [
          "id",
          "project_id",
          "url",
          "active",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "project_id": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "active": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookEndpointCreated": {
        "type": "object",
        "required": [
          "id",
          "url",
          "secret",
          "active",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "url": {
            "type": "string"
          },
          "secret": {
            "type": "string",
            "description": "The signing secret, returned once and never again. Store it before you close the response."
          },
          "active": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookDelivery": {
        "type": "object",
        "required": [
          "id",
          "endpoint_id",
          "endpoint_url",
          "event_type",
          "payload",
          "status",
          "attempts",
          "created_at",
          "last_error",
          "response_status"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "endpoint_id": {
            "type": "string",
            "format": "uuid"
          },
          "endpoint_url": {
            "type": "string"
          },
          "event_type": {
            "type": "string",
            "description": "The event name, such as participant.joined."
          },
          "payload": {
            "description": "The exact JSON body that was posted, byte for byte the body the signature covers."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "delivered",
              "failed"
            ]
          },
          "attempts": {
            "type": "integer",
            "description": "How many times Velo has posted this delivery."
          },
          "next_attempt_at": {
            "type": "string",
            "format": "date-time",
            "description": "Present only while the status is pending."
          },
          "delivered_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_error": {
            "type": "string",
            "description": "The transport or status error from the most recent attempt."
          },
          "response_status": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The HTTP status the receiver returned, or null if the request never completed."
          }
        }
      },
      "UsageDay": {
        "type": "object",
        "required": [
          "day",
          "participant_minutes",
          "sessions"
        ],
        "properties": {
          "day": {
            "type": "string",
            "description": "The UTC day, YYYY-MM-DD."
          },
          "participant_minutes": {
            "type": "number"
          },
          "sessions": {
            "type": "integer"
          }
        }
      },
      "PlanLimits": {
        "type": "object",
        "required": [
          "max_concurrent_rooms",
          "max_participants_per_room",
          "monthly_participant_minutes"
        ],
        "properties": {
          "max_concurrent_rooms": {
            "type": "integer"
          },
          "max_participants_per_room": {
            "type": "integer"
          },
          "monthly_participant_minutes": {
            "type": "integer"
          }
        }
      },
      "PlanFeatures": {
        "type": "object",
        "required": [
          "recording",
          "streaming"
        ],
        "properties": {
          "recording": {
            "type": "boolean",
            "description": "Whether the plan may start room, track and participant recordings."
          },
          "streaming": {
            "type": "boolean",
            "description": "Whether the plan may start broadcasts and open RTMP or WHIP ingress."
          }
        }
      },
      "PlanStatus": {
        "type": "object",
        "required": [
          "plan_id",
          "display_name",
          "price_ugx",
          "limits",
          "features",
          "quota_state",
          "period_start",
          "participant_minutes_used",
          "participant_minutes_remaining",
          "active_rooms"
        ],
        "properties": {
          "plan_id": {
            "type": "string"
          },
          "display_name": {
            "type": "string"
          },
          "price_ugx": {
            "type": "integer",
            "description": "The monthly price in Ugandan shillings."
          },
          "limits": {
            "$ref": "#/components/schemas/PlanLimits"
          },
          "features": {
            "$ref": "#/components/schemas/PlanFeatures"
          },
          "quota_state": {
            "type": "string",
            "enum": [
              "active",
              "suspended",
              "exceeded"
            ],
            "description": "An exceeded project has used its whole monthly participant-minute block; a suspended project has been stopped for other reasons. Both are refused with 402 on anything that would start work."
          },
          "period_start": {
            "type": "string",
            "description": "The first day of the current billing period, YYYY-MM-DD."
          },
          "participant_minutes_used": {
            "type": "number"
          },
          "participant_minutes_remaining": {
            "type": "number"
          },
          "active_rooms": {
            "type": "integer"
          }
        }
      },
      "Usage": {
        "type": "object",
        "required": [
          "days",
          "total_participant_minutes",
          "plan"
        ],
        "properties": {
          "days": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UsageDay"
            },
            "description": "One row per day in the requested range."
          },
          "total_participant_minutes": {
            "type": "number"
          },
          "plan": {
            "$ref": "#/components/schemas/PlanStatus"
          }
        }
      },
      "RoomPage": {
        "type": "object",
        "required": [
          "rooms",
          "limit",
          "offset",
          "has_more"
        ],
        "properties": {
          "rooms": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Room"
            },
            "description": "The rooms in this page, newest first."
          },
          "limit": {
            "type": "integer",
            "description": "The page size that was applied."
          },
          "offset": {
            "type": "integer",
            "description": "The offset that was applied."
          },
          "has_more": {
            "type": "boolean",
            "description": "True when a further page exists."
          }
        }
      },
      "ParticipantPage": {
        "type": "object",
        "required": [
          "participants",
          "limit",
          "offset",
          "has_more"
        ],
        "properties": {
          "participants": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Participant"
            },
            "description": "Everyone currently connected."
          },
          "limit": {
            "type": "integer",
            "description": "The page size that was applied."
          },
          "offset": {
            "type": "integer",
            "description": "The offset that was applied."
          },
          "has_more": {
            "type": "boolean",
            "description": "True when a further page exists."
          }
        }
      },
      "TemplatePage": {
        "type": "object",
        "required": [
          "templates",
          "limit",
          "offset",
          "has_more"
        ],
        "properties": {
          "templates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Template"
            },
            "description": "The templates in this page."
          },
          "limit": {
            "type": "integer",
            "description": "The page size that was applied."
          },
          "offset": {
            "type": "integer",
            "description": "The offset that was applied."
          },
          "has_more": {
            "type": "boolean",
            "description": "True when a further page exists."
          }
        }
      },
      "RoomCodePage": {
        "type": "object",
        "required": [
          "codes",
          "limit",
          "offset",
          "has_more"
        ],
        "properties": {
          "codes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RoomCode"
            },
            "description": "The codes issued for this room."
          },
          "limit": {
            "type": "integer",
            "description": "The page size that was applied."
          },
          "offset": {
            "type": "integer",
            "description": "The offset that was applied."
          },
          "has_more": {
            "type": "boolean",
            "description": "True when a further page exists."
          }
        }
      },
      "RecordingPage": {
        "type": "object",
        "required": [
          "recordings",
          "limit",
          "offset",
          "has_more"
        ],
        "properties": {
          "recordings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Recording"
            },
            "description": "The recordings in this page."
          },
          "limit": {
            "type": "integer",
            "description": "The page size that was applied."
          },
          "offset": {
            "type": "integer",
            "description": "The offset that was applied."
          },
          "has_more": {
            "type": "boolean",
            "description": "True when a further page exists."
          }
        }
      },
      "IngressPage": {
        "type": "object",
        "required": [
          "ingresses",
          "limit",
          "offset",
          "has_more"
        ],
        "properties": {
          "ingresses": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Ingress"
            },
            "description": "The ingresses in this page."
          },
          "limit": {
            "type": "integer",
            "description": "The page size that was applied."
          },
          "offset": {
            "type": "integer",
            "description": "The offset that was applied."
          },
          "has_more": {
            "type": "boolean",
            "description": "True when a further page exists."
          }
        }
      },
      "BroadcastPage": {
        "type": "object",
        "required": [
          "broadcasts",
          "limit",
          "offset",
          "has_more"
        ],
        "properties": {
          "broadcasts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Broadcast"
            },
            "description": "The broadcasts in this page."
          },
          "limit": {
            "type": "integer",
            "description": "The page size that was applied."
          },
          "offset": {
            "type": "integer",
            "description": "The offset that was applied."
          },
          "has_more": {
            "type": "boolean",
            "description": "True when a further page exists."
          }
        }
      },
      "SessionPage": {
        "type": "object",
        "required": [
          "sessions",
          "limit",
          "offset",
          "has_more"
        ],
        "properties": {
          "sessions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Session"
            },
            "description": "The sessions in this page."
          },
          "limit": {
            "type": "integer",
            "description": "The page size that was applied."
          },
          "offset": {
            "type": "integer",
            "description": "The offset that was applied."
          },
          "has_more": {
            "type": "boolean",
            "description": "True when a further page exists."
          }
        }
      },
      "RoleChangePage": {
        "type": "object",
        "required": [
          "role_changes",
          "limit",
          "offset",
          "has_more"
        ],
        "properties": {
          "role_changes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RoleChange"
            },
            "description": "The role changes for this room, newest first."
          },
          "limit": {
            "type": "integer",
            "description": "The page size that was applied."
          },
          "offset": {
            "type": "integer",
            "description": "The offset that was applied."
          },
          "has_more": {
            "type": "boolean",
            "description": "True when a further page exists."
          }
        }
      },
      "WebhookEndpointPage": {
        "type": "object",
        "required": [
          "webhook_endpoints",
          "limit",
          "offset",
          "has_more"
        ],
        "properties": {
          "webhook_endpoints": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookEndpoint"
            },
            "description": "The endpoints registered on this project."
          },
          "limit": {
            "type": "integer",
            "description": "The page size that was applied."
          },
          "offset": {
            "type": "integer",
            "description": "The offset that was applied."
          },
          "has_more": {
            "type": "boolean",
            "description": "True when a further page exists."
          }
        }
      },
      "WebhookDeliveryPage": {
        "type": "object",
        "required": [
          "webhook_deliveries",
          "limit",
          "offset",
          "has_more"
        ],
        "properties": {
          "webhook_deliveries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookDelivery"
            },
            "description": "The delivery attempts in this page, newest first."
          },
          "limit": {
            "type": "integer",
            "description": "The page size that was applied."
          },
          "offset": {
            "type": "integer",
            "description": "The offset that was applied."
          },
          "has_more": {
            "type": "boolean",
            "description": "True when a further page exists."
          }
        }
      },
      "DestinationList": {
        "type": "object",
        "required": [
          "destinations"
        ],
        "description": "Destinations are returned whole rather than paged, because a template holds few of them.",
        "properties": {
          "destinations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Destination"
            },
            "description": "Every destination on the template."
          }
        }
      }
    }
  }
}
