{
  "openapi": "3.1.0",
  "info": {
    "title": "AdScaled LinkBoard API",
    "version": "1.1.0",
    "summary": "Read a temporary LinkBoard room (texts, links, files) that a person shared with you, and follow it live.",
    "description": "AdScaled LinkBoard is a temporary board for moving links, text and files between devices. A person creates a room\nin the browser (https://linkboard.adscaled.media/new), gets a link such as `https://linkboard.adscaled.media/r/J7K4-P2` and everything sent to the room appears on every\ndevice. Rooms expire after 1 hour, 6 hours, 24 hours or 7 days.\n\nWith this API an agent can read a room the person gave it: the room state with all items (`getRoom`), the stored\nfiles and previews (`getRoomItemAsset`) and new items as they arrive (`streamRoomEvents`, Server-Sent Events).\nCreating rooms and sending items is done in the web interface only: write requests must come from LinkBoard pages\n(same-site check) and answer `403 forbidden_origin` to other clients.\n\nNo account or API key exists. Every room request needs an `X-Device-Id` header (any random id you keep). A room\nwith a password additionally needs its room token in `X-Room-Token` (or `?t=`); the token is issued only to the\nweb interface after the password is entered, so password rooms are normally out of reach for agents.\n\nRate limits per client IP (IPv6 per /64): 300 API requests per minute (`flood`) and 40 unknown room codes per\n10 minutes (`room-lookup`). Responses carry the IETF `RateLimit-Policy` and `RateLimit` headers\n(draft-ietf-httpapi-ratelimit-headers); a `429` also carries `Retry-After` in seconds. At most 20 live streams per\nroom and 30 per IP. Errors are always `{ \"error\": { \"code\", \"message\" } }` (`application/json`).\n\n## Versioning and deprecation\n\nThe API is versioned in the path: every documented operation lives under `/api/v1/` and `info.version` follows semantic\nversioning (a minor version adds fields or operations, a patch version fixes descriptions). Within `v1` changes are only\nadditive: new optional fields, new event types, new operations. Clients must ignore unknown fields and events.\nA breaking change (removing or renaming a field, changing a type, a status code or the meaning of a value) gets a new\npath version (`/api/v2/`); the old version keeps working until the announced Sunset date.\nDeprecated operations are announced in this document (`deprecated: true`), in /api/docs and in responses with the\n`Deprecation` header (RFC 9745) and the `Sunset` header with the removal date (RFC 8594), plus a `Link` with\n`rel=\"deprecation\"` to the notice. A deprecated operation stays available for at least 90 days after the deprecation\nis announced. Nothing is deprecated today. The unversioned paths (`/api/health`, `/api/rooms/...`)\nare the same `v1` handlers, used by the web interface, and stay as aliases of the current version.",
    "contact": {
      "name": "AdScaled",
      "email": "contact@adscaled.media",
      "url": "https://www.adscaled.media/en/contact"
    },
    "termsOfService": "https://linkboard.adscaled.media/terms"
  },
  "externalDocs": {
    "description": "Human-readable API documentation",
    "url": "https://linkboard.adscaled.media/api/docs"
  },
  "servers": [
    {
      "url": "https://linkboard.adscaled.media",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Status",
      "description": "Service availability."
    },
    {
      "name": "Rooms",
      "description": "Reading a room by its code."
    }
  ],
  "security": [
    {},
    {
      "roomToken": []
    }
  ],
  "paths": {
    "/api/v1/health": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "Status"
        ],
        "summary": "Check API availability",
        "description": "Returns `{ \"ok\": true }` while the service is running. Needs no headers.",
        "security": [
          {}
        ],
        "responses": {
          "200": {
            "description": "The service is running.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/api/v1/rooms/{code}": {
      "get": {
        "operationId": "getRoom",
        "tags": [
          "Rooms"
        ],
        "summary": "Read a room and all its items",
        "description": "Returns the room settings, every item (oldest first), the connected device numbers and your own device number. Text items carry the full text; link items the URL and a preview once the server has fetched it; file and image items the file metadata (download them with `getRoomItemAsset`). Opening a room registers your `X-Device-Id` as a device of that room.",
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "description": "Room code from the room link (https://linkboard.adscaled.media/r/J7K4-P2). Case-insensitive, with or without the dash; 6 characters of `ABCDEFGHJKMNPQRSTUVWXYZ23456789`.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9]{4}-?[A-Za-z0-9]{2}$",
              "examples": [
                "J7K4-P2"
              ]
            }
          },
          {
            "name": "X-Device-Id",
            "in": "header",
            "required": true,
            "description": "Random id of the client (16 to 64 characters of `A-Z a-z 0-9 _ -`). Generate one and reuse it: the room numbers devices by it (\"Device 2\"). It is not a secret and is never shown to other devices.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{16,64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The room state.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoomState"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_input`: `X-Device-Id` is missing or invalid.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`password_required`: the room has a password and the request has no valid room token.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`room_not_found`: no such room, or it has expired or was deleted. Unknown codes count against the `room-lookup` limit.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/api/v1/rooms/{code}/items/{id}/{asset}": {
      "get": {
        "operationId": "getRoomItemAsset",
        "tags": [
          "Rooms"
        ],
        "summary": "Download a stored file or preview image",
        "description": "`file` = the uploaded file of a `file` or `image` item (images are served inline, everything else as an attachment; add `download=1` to force an attachment); `thumb` = WebP preview of an `image` item; `icon` and `image` = WebP favicon and picture of a `link` item preview (see `preview.hasIcon` / `preview.hasImage`). Supports HTTP Range requests.",
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "description": "Room code from the room link (https://linkboard.adscaled.media/r/J7K4-P2). Case-insensitive, with or without the dash; 6 characters of `ABCDEFGHJKMNPQRSTUVWXYZ23456789`.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9]{4}-?[A-Za-z0-9]{2}$",
              "examples": [
                "J7K4-P2"
              ]
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Item id from `getRoom`.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,40}$"
            }
          },
          {
            "name": "asset",
            "in": "path",
            "required": true,
            "description": "Which stored bytes to return.",
            "schema": {
              "type": "string",
              "enum": [
                "file",
                "thumb",
                "icon",
                "image"
              ]
            }
          },
          {
            "name": "download",
            "in": "query",
            "required": false,
            "description": "`1` = send `Content-Disposition: attachment` for images too.",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            }
          },
          {
            "name": "t",
            "in": "query",
            "required": false,
            "description": "Room token of a password-protected room, for URLs that cannot carry headers (EventSource, links). Same value as `X-Room-Token`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The bytes. `Content-Type` is the type sniffed from the bytes when the file was stored (`FileMeta.mime`); HTML, SVG, XML and scripts are sent as `application/octet-stream`. Previews (`thumb`, `icon`, `image`) are always `image/webp`. The listed media types are the common ones; any other stored type is returned as it is.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "Content-Disposition": {
                "description": "`inline` for images, `attachment` (with the original file name) for everything else or with `download=1`.",
                "schema": {
                  "type": "string"
                }
              },
              "Content-Length": {
                "description": "Bytes in this response.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              },
              "Accept-Ranges": {
                "description": "Always `bytes`.",
                "schema": {
                  "type": "string",
                  "const": "bytes"
                }
              },
              "Last-Modified": {
                "description": "When the file was stored.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "image/webp": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "image/webp",
                  "description": "Raw bytes of the stored file."
                }
              },
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "image/jpeg",
                  "description": "Raw bytes of the stored file."
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "image/png",
                  "description": "Raw bytes of the stored file."
                }
              },
              "image/gif": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "image/gif",
                  "description": "Raw bytes of the stored file."
                }
              },
              "image/avif": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "image/avif",
                  "description": "Raw bytes of the stored file."
                }
              },
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "application/octet-stream",
                  "description": "Raw bytes of the stored file."
                }
              },
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "application/pdf",
                  "description": "Raw bytes of the stored file."
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "text/plain",
                  "description": "Raw bytes of the stored file."
                }
              }
            }
          },
          "206": {
            "description": "Partial content: the requested byte range of a Range request.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "Content-Disposition": {
                "description": "`inline` for images, `attachment` (with the original file name) for everything else or with `download=1`.",
                "schema": {
                  "type": "string"
                }
              },
              "Content-Length": {
                "description": "Bytes in this response.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              },
              "Accept-Ranges": {
                "description": "Always `bytes`.",
                "schema": {
                  "type": "string",
                  "const": "bytes"
                }
              },
              "Last-Modified": {
                "description": "When the file was stored.",
                "schema": {
                  "type": "string"
                }
              },
              "Content-Range": {
                "description": "e.g. `bytes 0-1023/52341`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "image/webp": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "image/webp",
                  "description": "Raw bytes of the stored file."
                }
              },
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "image/jpeg",
                  "description": "Raw bytes of the stored file."
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "image/png",
                  "description": "Raw bytes of the stored file."
                }
              },
              "image/gif": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "image/gif",
                  "description": "Raw bytes of the stored file."
                }
              },
              "image/avif": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "image/avif",
                  "description": "Raw bytes of the stored file."
                }
              },
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "application/octet-stream",
                  "description": "Raw bytes of the stored file."
                }
              },
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "application/pdf",
                  "description": "Raw bytes of the stored file."
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "text/plain",
                  "description": "Raw bytes of the stored file."
                }
              }
            }
          },
          "304": {
            "description": "Not modified (`If-Modified-Since`); no body.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            }
          },
          "401": {
            "description": "`password_required`: the room has a password and the request has no valid room token.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`room_not_found` or `item_not_found`: the room or item does not exist, or the item has no such asset.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "416": {
            "description": "`invalid_input`: the Range header asks for bytes outside the file.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/api/v1/rooms/{code}/events": {
      "get": {
        "operationId": "streamRoomEvents",
        "tags": [
          "Rooms"
        ],
        "summary": "Follow a room live (Server-Sent Events)",
        "description": "A `text/event-stream` that starts with `retry: 3000` and a `hello` event (the same `RoomState` as `getRoom`), then sends `item`, `item_updated`, `item_deleted`, `items_cleared`, `room`, `presence` and `room_deleted` events with JSON data; a `: ping` comment every 25 s keeps the connection open. While the stream is open your device counts as online in the room. The stream ends with `room_deleted` when the room expires or is deleted.",
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "description": "Room code from the room link (https://linkboard.adscaled.media/r/J7K4-P2). Case-insensitive, with or without the dash; 6 characters of `ABCDEFGHJKMNPQRSTUVWXYZ23456789`.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9]{4}-?[A-Za-z0-9]{2}$",
              "examples": [
                "J7K4-P2"
              ]
            }
          },
          {
            "name": "device",
            "in": "query",
            "required": true,
            "description": "Your device id (same rules as `X-Device-Id`).",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{16,64}$"
            }
          },
          {
            "name": "t",
            "in": "query",
            "required": false,
            "description": "Room token of a password-protected room, for URLs that cannot carry headers (EventSource, links). Same value as `X-Room-Token`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The event stream (`text/event-stream; charset=utf-8`, `Cache-Control: no-cache, no-store, no-transform`). Every event is `event: <type>` plus one `data:` line with JSON; the JSON of each event type is described by the `RoomEvent` schema.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "text/event-stream",
                  "description": "Server-Sent Events (WHATWG HTML). `retry: 3000`, then events whose `event` field is `RoomEvent.event` and whose `data` field is the JSON of `RoomEvent.data`; `: ping` comment lines every 25 s."
                },
                "x-event-schema": {
                  "$ref": "#/components/schemas/RoomEvent"
                },
                "example": "retry: 3000\n\nevent: hello\ndata: {\"room\":{...},\"items\":[...],\"online\":[1],\"you\":1}\n\nevent: item\ndata: {\"item\":{\"id\":\"k3J9xQ2mP0aa\",\"kind\":\"text\",\"createdAt\":1760400000000,\"device\":2,\"text\":\"hello\"}}\n\n"
              }
            }
          },
          "400": {
            "description": "`invalid_input`: `device` is missing or invalid.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`password_required`: the room has a password and the request has no valid room token.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`room_not_found`: no such room, or it has expired or was deleted. Unknown codes count against the `room-lookup` limit.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "roomToken": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Room-Token",
        "description": "Only for password-protected rooms: the room token the web interface receives after the password is entered."
      }
    },
    "headers": {
      "RateLimit": {
        "description": "Remaining quota per policy, e.g. `\"flood\";r=297;t=41` (r = remaining requests, t = seconds until reset).",
        "schema": {
          "type": "string"
        }
      },
      "RateLimitPolicy": {
        "description": "Quota policies that apply, e.g. `\"flood\";q=300;w=60, \"room-lookup\";q=40;w=600` (q = quota, w = window in seconds).",
        "schema": {
          "type": "string"
        }
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      }
    },
    "responses": {
      "TooManyRequests": {
        "description": "`rate_limited`: a rate limit was hit. Wait for `Retry-After` seconds.",
        "headers": {
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "rate_limited",
                "message": "Too many requests. Try again later."
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Health": {
        "type": "object",
        "required": [
          "ok"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "room_not_found",
                  "password_required",
                  "wrong_password",
                  "invalid_input",
                  "text_too_long",
                  "file_too_large",
                  "room_full",
                  "storage_full",
                  "item_not_found",
                  "rate_limited",
                  "forbidden_origin",
                  "server_error"
                ]
              },
              "message": {
                "type": "string",
                "description": "English, for humans."
              }
            }
          }
        }
      },
      "RoomInfo": {
        "type": "object",
        "required": [
          "code",
          "createdAt",
          "expiresAt",
          "expiry",
          "hasPassword",
          "bytes",
          "itemCount"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "Bare 6-character code (shown as J7K4-P2)."
          },
          "createdAt": {
            "type": "integer",
            "description": "Milliseconds since the Unix epoch."
          },
          "expiresAt": {
            "type": "integer",
            "description": "Milliseconds since the Unix epoch; the room and its files are deleted then."
          },
          "expiry": {
            "type": "string",
            "enum": [
              "1h",
              "6h",
              "24h",
              "7d"
            ]
          },
          "hasPassword": {
            "type": "boolean"
          },
          "bytes": {
            "type": "integer",
            "description": "Bytes of all stored files (max 524288000)."
          },
          "itemCount": {
            "type": "integer",
            "description": "Number of items (max 500)."
          }
        }
      },
      "LinkPreview": {
        "type": "object",
        "required": [
          "title",
          "description",
          "siteName",
          "hasIcon",
          "hasImage"
        ],
        "properties": {
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "siteName": {
            "type": [
              "string",
              "null"
            ]
          },
          "hasIcon": {
            "type": "boolean",
            "description": "A favicon is stored (asset `icon`)."
          },
          "hasImage": {
            "type": "boolean",
            "description": "A preview picture is stored (asset `image`)."
          }
        }
      },
      "FileMeta": {
        "type": "object",
        "required": [
          "name",
          "size",
          "mime"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "size": {
            "type": "integer",
            "description": "Bytes (max 104857600)."
          },
          "mime": {
            "type": "string",
            "description": "Sniffed from the bytes."
          },
          "width": {
            "type": "integer"
          },
          "height": {
            "type": "integer"
          },
          "hasThumb": {
            "type": "boolean",
            "description": "A WebP preview is stored (asset `thumb`)."
          }
        }
      },
      "Item": {
        "type": "object",
        "required": [
          "id",
          "kind",
          "createdAt",
          "device"
        ],
        "description": "One item of the room. `kind` decides which of `text`, `url`/`preview`, `file` is present.",
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "text",
              "link",
              "file",
              "image"
            ]
          },
          "createdAt": {
            "type": "integer",
            "description": "Milliseconds since the Unix epoch."
          },
          "device": {
            "type": "integer",
            "description": "Number of the device that sent it (1 = the device that created the room)."
          },
          "text": {
            "type": "string",
            "description": "kind `text`: the text (max 100000 characters)."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "kind `link`: the URL."
          },
          "previewStatus": {
            "type": "string",
            "enum": [
              "pending",
              "ready",
              "none"
            ],
            "description": "kind `link`: whether the server has fetched a preview."
          },
          "preview": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/LinkPreview"
              },
              {
                "type": "null"
              }
            ]
          },
          "file": {
            "$ref": "#/components/schemas/FileMeta"
          }
        }
      },
      "RoomEvent": {
        "description": "One Server-Sent Event of `streamRoomEvents`: `event` is the SSE event name, `data` the JSON in its `data:` line.",
        "oneOf": [
          {
            "type": "object",
            "required": [
              "event",
              "data"
            ],
            "properties": {
              "event": {
                "const": "hello"
              },
              "data": {
                "$ref": "#/components/schemas/RoomState"
              }
            },
            "description": "First event: the full room state (same as `getRoom`)."
          },
          {
            "type": "object",
            "required": [
              "event",
              "data"
            ],
            "properties": {
              "event": {
                "const": "item"
              },
              "data": {
                "type": "object",
                "required": [
                  "item"
                ],
                "properties": {
                  "item": {
                    "$ref": "#/components/schemas/Item"
                  },
                  "clientId": {
                    "type": "string"
                  }
                }
              }
            },
            "description": "A new item."
          },
          {
            "type": "object",
            "required": [
              "event",
              "data"
            ],
            "properties": {
              "event": {
                "const": "item_updated"
              },
              "data": {
                "type": "object",
                "required": [
                  "item"
                ],
                "properties": {
                  "item": {
                    "$ref": "#/components/schemas/Item"
                  }
                }
              }
            },
            "description": "An item changed (a link preview arrived)."
          },
          {
            "type": "object",
            "required": [
              "event",
              "data"
            ],
            "properties": {
              "event": {
                "const": "item_deleted"
              },
              "data": {
                "type": "object",
                "required": [
                  "id"
                ],
                "properties": {
                  "id": {
                    "type": "string"
                  }
                }
              }
            },
            "description": "An item was deleted."
          },
          {
            "type": "object",
            "required": [
              "event",
              "data"
            ],
            "properties": {
              "event": {
                "const": "items_cleared"
              },
              "data": {
                "type": "object",
                "additionalProperties": false
              }
            },
            "description": "All items were deleted."
          },
          {
            "type": "object",
            "required": [
              "event",
              "data"
            ],
            "properties": {
              "event": {
                "const": "room"
              },
              "data": {
                "type": "object",
                "required": [
                  "room"
                ],
                "properties": {
                  "room": {
                    "$ref": "#/components/schemas/RoomInfo"
                  },
                  "token": {
                    "type": "string"
                  }
                }
              }
            },
            "description": "Room settings changed."
          },
          {
            "type": "object",
            "required": [
              "event",
              "data"
            ],
            "properties": {
              "event": {
                "const": "presence"
              },
              "data": {
                "type": "object",
                "required": [
                  "online"
                ],
                "properties": {
                  "online": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    }
                  },
                  "joined": {
                    "type": "integer"
                  }
                }
              }
            },
            "description": "Connected devices changed."
          },
          {
            "type": "object",
            "required": [
              "event",
              "data"
            ],
            "properties": {
              "event": {
                "const": "room_deleted"
              },
              "data": {
                "type": "object",
                "additionalProperties": false
              }
            },
            "description": "The room expired or was deleted; the stream ends."
          }
        ]
      },
      "RoomState": {
        "type": "object",
        "required": [
          "room",
          "items",
          "online",
          "you"
        ],
        "properties": {
          "room": {
            "$ref": "#/components/schemas/RoomInfo"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Item"
            },
            "description": "Oldest first."
          },
          "online": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "Device numbers with an open live stream."
          },
          "you": {
            "type": "integer",
            "description": "Your device number in this room."
          },
          "token": {
            "type": "string",
            "description": "Only for password rooms: the current room token."
          }
        }
      }
    }
  }
}