openapi: 3.1.0
info:
  title: Flappy Board API
  description: |
    REST API for controlling a Flappy Board split-flap display.

    All endpoints use HTTPS on port **8443** with a self-signed TLS certificate.
    The certificate's SHA-256 fingerprint is shown in the app's Settings screen
    and should be used for certificate pinning by clients.

    Protected endpoints require a Bearer token, also visible in Settings.
  version: 2.0.0

servers:
  - url: https://{host}:8443/v1
    variables:
      host:
        default: localhost
        description: IP address or hostname of the device running Flappy Board.

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Bearer token shown in the app's Settings screen.

  schemas:
    MessageCell:
      type: object
      required: [text]
      properties:
        text:
          type: string
          description: |
            Text to display in this cell. Supports inline style tags:

            - `<color='red'>text</color>` or `<color='#RRGGBB'>text</color>` — set character color.
            - `<super>text</super>` — render characters in the top half of the cell only (superscript).
            - `<sub>text</sub>` — render characters in the bottom half of the cell only (subscript).

            `<color>` tags may be nested inside `<super>`/`<sub>` to combine color and position,
            e.g. `14<super><color='cyan'>:32</color></super>`.

            If text is longer than the column's `width` it is truncated
            and a warning is returned in the response.
          example: "GATE 12"
        alignment:
          type: string
          enum: [left, center, right]
          description: |
            Horizontal alignment of the text within the column. Overrides
            the column's `horizontal_alignment_override` and the board's
            `horizontal_alignment_default`. Defaults to the column or board setting.
          example: right

    MessageRows:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            type: array
            items:
              $ref: '#/components/schemas/MessageCell'
          description: |
            An array of rows. Each row is an array of cells — one cell per **text**
            column (gap columns are implicit from the board config and are not
            represented here). Rows beyond the board's configured row count are
            ignored and a warning is returned in the response.

            In single-column mode (no columns configured, or only one column),
            each row should have exactly one cell.
        vertical_alignment:
          type: string
          enum: [top, center, bottom]
          description: |
            Vertical placement of the message rows within the board's grid.
            Overrides the board's `vertical_alignment_default` for this message.
            - `top` (default): rows start at the first row.
            - `center`: rows are vertically centered, with equal blank rows above and below.
            - `bottom`: rows are pushed to the last rows of the grid.
          example: center

    MessageBody:
      type: object
      required: [rows]
      properties:
        rows:
          $ref: '#/components/schemas/MessageRows'

    MessagePayload:
      type: object
      required: [message]
      properties:
        message:
          $ref: '#/components/schemas/MessageBody'
        charset:
          type: array
          items:
            type: string
          minItems: 1
          description: |
            Override the board's character set for this message only.
            Each item should be a single character. Characters in the message
            not present in this set will be replaced with a space and a warning
            is returned in the response. Must not be empty.
          example: [" ", "A", "B", "C", "1", "2", "3"]
        font:
          type: string
          description: |
            Font name override for this message only. Must be a font available
            on the device. Use `GET /v1/fonts` to list available fonts.
          example: "Courier New"
        flip_speed:
          type: number
          format: double
          description: |
            Speed multiplier for this message only. Overrides the board's configured
            `flip_speed` for the duration of this message. Higher values flip faster
            (`2.0` = twice as fast, `0.5` = half speed). If omitted, the board's
            configured `flip_speed` is used. Non-positive values are clamped to `0.1`
            and a warning is returned in the response.
          example: 2.0
        transition_animation:
          type: string
          enum: [none, staggered_start, top_to_bottom, bottom_to_top, left_to_right, right_to_left, row_by_row, dissolve, diagonal, middle_out, outside_in, spiral, hatch, snake_up, matrix, random]
          description: |
            Transition animation for this message only. Overrides the board's configured
            `transition_animation` for this message only.

            - `none` — all cells flip simultaneously with no stagger
            - `staggered_start` — rows flip sequentially top to bottom
            - `top_to_bottom` — like `staggered_start` with pre-flip cycles before revealing the target
            - `bottom_to_top` — rows flip sequentially bottom to top, each cell doing three full charset spins before landing
            - `left_to_right` — columns flip sequentially left to right, each cell doing three full charset spins before landing
            - `right_to_left` — columns flip sequentially right to left, each cell doing three full charset spins before landing
            - `row_by_row` — reading-order sweep left-to-right, top-to-bottom; each cell doing two full charset spins before landing
            - `dissolve` — each cell starts at a random time across a ~12 s window; each cell doing two full charset spins before landing
            - `diagonal` — NW→SE diagonal sweep; cells on the same diagonal fire together; each cell doing one full charset spin before landing
            - `middle_out` — center column(s) fire first, expanding outward to the edges; each cell doing one full charset spin before landing
            - `outside_in` — edge columns fire first, converging inward to the center; each cell doing one full charset spin before landing
            - `spiral` — cells flip in a clockwise spiral from the upper-left corner
            - `hatch` — alternating rows sweep from opposite sides (even rows right→left, odd rows left→right), each cell doing one full charset spin before landing
            - `snake_up` — boustrophedon sweep from bottom-right upward; bottom row goes right→left, next row left→right, etc., each cell doing two full charset spins before landing
            - `matrix` — each column starts at a random offset and sweeps top to bottom, each cell doing three full charset spins before landing
            - `random` — picks one of the above (except `none`) at random each time

            If omitted, the board's configured `transition_animation` is used.
          example: staggered_start

    ColumnDefinition:
      type: object
      required: [width, type]
      properties:
        width:
          type: integer
          minimum: 1
          description: |
            Width of the column in character units. The sum of all column widths
            determines the board's total column count (`cols`).
          example: 5
        type:
          type: string
          enum: [text, gap, image]
          description: |
            - `text`: a normal text column that receives cells from message rows.
            - `gap`: an empty spacer column. Gap positions render raw background
              with no flap tile. Gap columns are not referenced in message rows.
            - `image`: an image column that displays uploaded images keyed by
              label. Each row in an image column receives one cell whose `text`
              is the image label. Images are uploaded via `POST /v1/images` and
              scale to fit the cell area preserving aspect ratio. Set
              `charset_override` to a list of valid labels for validation.
          example: text
        charset_override:
          type: array
          items:
            type: string
          description: |
            Overrides the board's default charset for this column only. Omit to
            use the board's charset. For `image` columns, this is the list of
            valid image labels (used for validation only).
        font_override:
          type: object
          properties:
            name:
              type: string
              description: Font name override (deferred — currently ignored).
            color:
              type: string
              description: |
                Default character color for this column as a `#RRGGBB` hex string.
                Overrides the board's `font_default.color` for this column.
              example: "#FF6600"
        vertical_alignment_override:
          type: string
          enum: [top, center, bottom]
          description: Default vertical alignment for cells in this column.
        horizontal_alignment_override:
          type: string
          enum: [left, center, right]
          description: Default horizontal alignment for cells in this column.

    BoardBodyConfig:
      type: object
      properties:
        rows:
          type: integer
          description: Number of rows on the board.
          minimum: 1
          maximum: 50
          example: 6
        cols:
          type: integer
          description: |
            Number of character columns on the board (single-column mode only).
            Ignored when `columns` contains 2 or more entries, since the effective
            width is derived from the sum of column widths. Included in `GET /v1/config`
            responses so the full board configuration can be round-tripped.
          minimum: 1
          maximum: 40
          example: 22
        charset_default:
          type: array
          items:
            type: string
          description: |
            The set of characters the display can render, in flip order.
            Each item is a single character. Characters not in this set
            are replaced with a space.
          example: [" ", "A", "B", "C"]
        font_default:
          type: object
          properties:
            name:
              type: string
              description: |
                Default display font. Must be available on the device.
                Use `GET /v1/fonts` to list available fonts.
              example: "Helvetica Neue"
            color:
              type: string
              description: |
                Default character color as a `#RRGGBB` hex string.
              example: "#FFF2CC"
        flap_color:
          type: string
          description: Cell/flap background color as a `#RRGGBB` hex string.
          example: "#141414"
        row_spacing_ratio:
          type: number
          format: double
          description: |
            Gap between rows expressed as a ratio of cell height.
            `0.0` = no gap, `0.15` = gap equal to 15% of cell height.
          minimum: 0.0
          example: 0.15
        column_spacing_pts:
          type: number
          format: double
          description: |
            Horizontal gap between cells in points. `0` = no gap.
          minimum: 0.0
          default: 6
          example: 6
        flip_speed:
          type: number
          format: double
          description: |
            Speed multiplier relative to the 0.24s base flip duration.
            Higher values flip faster (`2.0` = twice as fast, `0.5` = half speed).
          example: 1.0
        horizontal_alignment_default:
          type: string
          enum: [left, center, right]
          description: Default horizontal alignment for all cells, when no column or cell override is set.
          default: left
          example: left
        vertical_alignment_default:
          type: string
          enum: [top, center, bottom]
          description: Default vertical alignment for messages, when no per-message override is set.
          default: top
          example: top
        flip_easing_threshold:
          type: integer
          description: |
            Minimum number of flip steps required to activate sin-bell easing.
            Sequences at or below this length animate at constant speed. Set to `0`
            to always apply easing regardless of sequence length.
          minimum: 0
          default: 8
          example: 8
        flip_easing_max_speedup:
          type: number
          format: double
          description: |
            Peak speed multiplier applied at the midpoint of a long sequence (when
            `flip_easing_threshold` is exceeded). A value of `3.0` means the middle
            flips run at 3× normal speed; `1.0` disables easing entirely.
          minimum: 1.0
          default: 3.0
          example: 3.0
        transition_animation:
          type: string
          enum: [none, staggered_start, top_to_bottom, bottom_to_top, left_to_right, right_to_left, row_by_row, dissolve, diagonal, middle_out, outside_in, spiral, hatch, snake_up, matrix]
          default: staggered_start
          description: |
            Controls how cell flipping is sequenced when a message arrives.
            `none` starts all cells simultaneously (original behavior).
            `staggered_start` delays each successive row by 0.5 s, so the
            top row begins first and the animation cascades downward.
            `top_to_bottom` works like `staggered_start` but each cell also
            spins through the full charset before stopping at its target character.
            `bottom_to_top` sweeps rows sequentially from the bottom row upward,
            each cell doing three full charset spins before landing.
            `left_to_right` sweeps columns sequentially from left to right,
            each cell doing three full charset spins before landing.
            `right_to_left` sweeps columns sequentially from right to left,
            each cell doing three full charset spins before landing.
            `row_by_row` sweeps in reading order (left-to-right, top-to-bottom),
            each cell doing two full charset spins before landing.
            `dissolve` assigns each cell a random start time within a ~12 s window,
            each cell doing two full charset spins before landing.
            `diagonal` sweeps in NW→SE diagonal bands; cells on the same diagonal
            fire together, each doing one full charset spin before landing.
            `middle_out` fires center column(s) first and expands outward to the
            edges, each cell doing one full charset spin before landing.
            `outside_in` fires edge columns first and converges inward to the
            center, each cell doing one full charset spin before landing.
            `spiral` fires cells one step apart in a clockwise spiral starting
            from the upper-left corner, with a full charset spin before landing.
            `hatch` sweeps alternating rows from opposite sides — even rows
            flow right→left, odd rows left→right — each cell doing one full
            charset spin before landing.
            `snake_up` sweeps in boustrophedon order from the bottom-right corner
            upward — bottom row right→left, next row left→right, etc. — each cell
            doing two full charset spins before landing.
            `matrix` gives each column a random start offset and sweeps top to
            bottom, each cell doing three full charset spins before landing.
        columns:
          type: array
          items:
            $ref: '#/components/schemas/ColumnDefinition'
          description: |
            Column layout definitions. When 2 or more columns are defined,
            multi-column mode is active and the board's effective width (`cols`)
            is derived as the sum of all column widths. Gap columns render as
            raw background with no flap tile.

            When 0 or 1 columns are defined, single-column mode is used and
            the whole board is treated as one full-width text area.

            Sending an empty array clears the column layout and returns to
            single-column mode.

    BoardHeaderFooterConfig:
      type: object
      properties:
        image:
          type: string
          format: byte
          description: |
            Branding image encoded as Base64 (PNG or JPEG).
            Send a non-empty string to set the image. Send `""` to clear.
            Omit to leave the current image unchanged.
            Decoded image data must not exceed 10 MB.
          example: ""
        height:
          type: number
          format: double
          description: |
            Height in points of the reserved image band. Set to `0` to disable the
            band entirely. To suppress the app's default branding footer, set
            `board.footer.height` to `0` (requires Professional tier or above).
          minimum: 0
          default: 0
          example: 80
        alignment:
          type: string
          enum: [left, center, right]
          description: Horizontal alignment of the image within the band.
          default: center
          example: center

    BoardConfig:
      type: object
      properties:
        show_startup_hint:
          type: boolean
          description: |
            When `true`, the board displays a platform-appropriate hint on launch
            that clears automatically after 10 seconds.
          default: true
          example: false
        show_debug_overlay:
          type: boolean
          description: |
            When `true`, renders an FPS and flip-speed debug overlay in the
            top-right corner of the board. Not surfaced in the Settings UI.
          default: false
          example: true
        disable_settings_ui:
          type: boolean
          description: |
            When `true`, hides the on-device Settings UI — the long-press gesture
            (iOS/iPadOS) and Play/Pause command (tvOS) that open Settings are
            silently ignored. Use this to prevent on-device changes on a deployed
            display. API access is unaffected regardless of this setting.
          default: false
          example: true
        board:
          type: object
          properties:
            background_color:
              type: string
              description: Background color behind all flaps, as a `#RRGGBB` hex string.
              example: "#0D0D0D"
            header:
              $ref: '#/components/schemas/BoardHeaderFooterConfig'
            body:
              $ref: '#/components/schemas/BoardBodyConfig'
            footer:
              $ref: '#/components/schemas/BoardHeaderFooterConfig'

    FontFamily:
      type: object
      required: [family, fonts]
      properties:
        family:
          type: string
          example: "Helvetica Neue"
        fonts:
          type: array
          items:
            type: string
          example: ["HelveticaNeue", "HelveticaNeue-Bold", "HelveticaNeue-Italic"]

    BoardStateCell:
      type: object
      required: [text]
      properties:
        text:
          type: string
          description: Character currently displayed in this cell. A space when the cell is blank.
          example: "H"
        color:
          type: string
          description: |
            Current character color as a `#RRGGBB` hex string. Omitted when the cell
            is using the board's default color.
          example: "#FF6600"

    AcceptedResponse:
      type: object
      properties:
        status:
          type: string
          example: accepted
        warnings:
          type: array
          items:
            type: string
          description: |
            Present only when the request was accepted but adjustments were made.
            Each entry describes one adjustment (e.g. text truncation, charset
            substitution, extra rows ignored). Absent when no adjustments were needed.
          example:
            - "Row 2, column 1: text truncated from 8 to 5 characters"
            - "Row 1: single cell sent to multi-column board — text placed in column 1, remaining columns blank"

    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: string
          example: "Font 'Unknown' is not available on this device"

    UpgradeRequiredResponse:
      type: object
      required: [error, minimum_tier]
      properties:
        error:
          type: string
          enum: [upgrade_required]
          example: upgrade_required
        minimum_tier:
          type: string
          description: The minimum tier required to use this endpoint or field.
          enum: [hobbyist, professional, studio]
          example: professional

paths:
  /status:
    get:
      summary: Health check
      description: Returns the running status of the server. No authentication required.
      operationId: getStatus
      responses:
        '200':
          description: Server is running.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: running

  /fonts:
    get:
      summary: List available fonts
      description: |
        Returns all fonts installed on the device, grouped by family.
        Use the font names when setting a font via `PUT /v1/config`
        or the per-message `font` field. No authentication required.
      operationId: getFonts
      responses:
        '200':
          description: Font list.
          content:
            application/json:
              schema:
                type: object
                required: [families]
                properties:
                  families:
                    type: array
                    items:
                      $ref: '#/components/schemas/FontFamily'

  /message:
    post:
      summary: Display a message
      description: |
        Queues a message to be displayed on the board. Rows are applied in order;
        any row not included keeps its current content.

        Per-message overrides for `font`, `charset`, and `flip_speed` are applied
        for this message only and do not affect the persistent board configuration.

        Each row in `items` contains one cell per **text** or **image** column, in
        column order. Gap columns are defined in the board config and are not
        referenced in message rows. For image columns, the cell's `text` field is
        the image label (e.g. `"AA"` for American Airlines).
      operationId: postMessage
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MessagePayload'
            examples:
              single_column:
                summary: Simple single-column message
                value:
                  message:
                    rows:
                      items:
                        - - text: "GATE 12"
                        - - text: "BOARDING"
                      vertical_alignment: center
              multi_column:
                summary: Three-column train departure board
                value:
                  message:
                    rows:
                      items:
                        - - text: "12:34"
                            alignment: right
                          - text: "CHICAGO"
                          - text: "ON TIME"
                        - - text: "13:00"
                            alignment: right
                          - text: "NEW YORK"
                          - text: "+15 MIN"
                      vertical_alignment: top
      responses:
        '200':
          description: Message accepted and queued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AcceptedResponse'
        '400':
          description: |
            Invalid request body. Possible causes: empty body, malformed JSON,
            unavailable font, empty charset override, or request body exceeding 64 KB.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid bearer token.
        '402':
          description: |
            Tier upgrade required. Returned when the caller's purchased tier is below
            the minimum required for this endpoint or field. Check `minimum_tier` to
            determine which tier to offer the user.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpgradeRequiredResponse'

  /images:
    get:
      summary: List uploaded images
      description: |
        Returns the list of labels for all images currently stored in the image
        library. Pass `?include_data=true` to include the raw base64 image data.
      operationId: getImages
      security:
        - bearerAuth: []
      parameters:
        - name: include_data
          in: query
          required: false
          schema:
            type: boolean
          description: When true, includes base64-encoded image data in the response.
      responses:
        '200':
          description: Image library listing.
          content:
            application/json:
              schema:
                type: object
                required: [labels]
                properties:
                  labels:
                    type: array
                    items:
                      type: string
                    description: Normalized labels of all stored images.
                    example: ["AA", "DL", "UA"]
                  images:
                    type: object
                    additionalProperties:
                      type: object
                      properties:
                        image:
                          type: string
                          description: Base64-encoded image data.
                        background_color:
                          type: string
                          description: Stored background color, if one was provided on upload.
                    description: |
                      Map of label → `{image, background_color?}`.
                      Only present when `include_data=true`.
        '401':
          description: Missing or invalid bearer token.
        '402':
          description: |
            Tier upgrade required. Returned when the caller's purchased tier is below
            the minimum required for this endpoint or field. Check `minimum_tier` to
            determine which tier to offer the user.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpgradeRequiredResponse'
    post:
      summary: Upload an image
      description: |
        Uploads a PNG or JPEG image and associates it with a label. Labels are
        normalized to uppercase alphanumeric (A-Z, 0-9, `-`, `_`) before storage.
        Maximum image size is 5 MB. Uploading with an existing label replaces it.
      operationId: postImage
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [label, image]
              properties:
                label:
                  type: string
                  description: |
                    Identifier for this image (1–10 chars, letters/digits/`-`/`_`).
                  example: "AA"
                image:
                  type: string
                  format: byte
                  description: Base64-encoded PNG or JPEG image data.
                background_color:
                  type: string
                  description: |
                    Optional fill color (`#RRGGBB`) used behind the image when the board
                    renders it. The board scales the image to fit within the cell span
                    (aspect-fit / letterbox) and fills any remaining space with this color.
                    When omitted the board falls back to the flap background color.
                  example: "#F0F0F0"
            example:
              label: "AA"
              image: "<base64-encoded PNG>"
              background_color: "#F0F0F0"
      responses:
        '200':
          description: Image accepted and stored.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: accepted
                  label:
                    type: string
                    description: The normalized label under which the image was stored.
                    example: "AA"
                  background_color:
                    type: string
                    description: The stored background color, echoed back if one was provided.
                    example: "#F0F0F0"
        '400':
          description: Invalid request (bad label, invalid image data, or too large).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid bearer token.
        '402':
          description: |
            Tier upgrade required. Returned when the caller's purchased tier is below
            the minimum required for this endpoint or field. Check `minimum_tier` to
            determine which tier to offer the user.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpgradeRequiredResponse'

  /images/{label}:
    delete:
      summary: Delete an image
      description: Removes the image associated with `label` from the library.
      operationId: deleteImage
      security:
        - bearerAuth: []
      parameters:
        - name: label
          in: path
          required: true
          schema:
            type: string
          description: The label of the image to delete.
          example: "AA"
      responses:
        '200':
          description: Image deleted (or label did not exist).
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: accepted
        '400':
          description: Missing or empty label.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid bearer token.
        '402':
          description: |
            Tier upgrade required. Returned when the caller's purchased tier is below
            the minimum required for this endpoint or field. Check `minimum_tier` to
            determine which tier to offer the user.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpgradeRequiredResponse'

  /config:
    get:
      summary: Get board configuration
      description: Returns the current persistent board configuration.
      operationId: getConfig
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Current board configuration.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BoardConfig'
              example:
                show_startup_hint: true
                show_debug_overlay: false
                disable_settings_ui: false
                board:
                  background_color: "#0D0D0D"
                  header:
                    image: ""
                    height: 0
                    alignment: center
                  body:
                    rows: 6
                    cols: 22
                    charset_default: [" ", "A", "B", "C", "0", "1", "2"]
                    font_default:
                      name: "Helvetica Neue"
                      color: "#FFF2CC"
                    flap_color: "#141414"
                    row_spacing_ratio: 0.15
                    column_spacing_pts: 6
                    flip_speed: 1.0
                    horizontal_alignment_default: left
                    vertical_alignment_default: top
                    flip_easing_threshold: 8
                    flip_easing_max_speedup: 3.0
                    transition_animation: staggered_start
                    columns: []
                  footer:
                    image: ""
                    height: 0
                    alignment: center
        '401':
          description: Missing or invalid bearer token.
        '402':
          description: |
            Tier upgrade required. Returned when the caller's purchased tier is below
            the minimum required for this endpoint or field. Check `minimum_tier` to
            determine which tier to offer the user.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpgradeRequiredResponse'

    delete:
      summary: Reset board configuration to device defaults
      description: |
        Resets all persistent board configuration to the adaptive defaults for the
        host device (rows, cols, charset, font, colors, flip speed, columns,
        header/footer images, etc.). Changes take effect immediately on the live
        display and are persisted across app restarts.

        Use `GET /v1/config` first if you want to capture the current state before
        resetting.
      operationId: deleteConfig
      security:
        - bearerAuth: []
      responses:
        '202':
          description: Configuration reset to device defaults.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AcceptedResponse'
              example:
                status: accepted
        '401':
          description: Missing or invalid bearer token.
        '402':
          description: |
            Tier upgrade required. Returned when the caller's purchased tier is below
            the minimum required for this endpoint or field. Check `minimum_tier` to
            determine which tier to offer the user.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpgradeRequiredResponse'

    put:
      summary: Update board configuration
      description: |
        Updates the persistent board configuration. All fields are optional —
        only supplied fields are changed. Changes take effect immediately on the
        live display and are persisted across app restarts.

        When `columns` is provided with 2+ entries, multi-column mode is enabled
        and the board's effective column width is derived as the sum of all column
        widths. Sending `columns: []` returns to single-column mode.
      operationId: putConfig
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BoardConfig'
            examples:
              disable_settings_ui:
                summary: Hide the on-device Settings UI
                value:
                  disable_settings_ui: true
              enable_settings_ui:
                summary: Show the on-device Settings UI
                value:
                  disable_settings_ui: false
              resize:
                summary: Change board row count
                value:
                  board:
                    body:
                      rows: 8
              font_and_speed:
                summary: Change font and flip speed
                value:
                  board:
                    body:
                      font_default:
                        name: "Courier New"
                      flip_speed: 0.5
              flip_easing:
                summary: Tune flip easing for large charsets
                value:
                  board:
                    body:
                      flip_easing_threshold: 6
                      flip_easing_max_speedup: 4.0
              colors:
                summary: Customize board colors
                value:
                  board:
                    background_color: "#1A0A00"
                    body:
                      flap_color: "#2A1500"
                      font_default:
                        color: "#FFB347"
              train_columns:
                summary: Configure a 3-column train departure layout
                value:
                  board:
                    body:
                      columns:
                        - width: 5
                          type: text
                          horizontal_alignment_override: right
                        - width: 1
                          type: gap
                        - width: 14
                          type: text
                        - width: 1
                          type: gap
                        - width: 5
                          type: text
              clear_columns:
                summary: Return to single-column mode
                value:
                  board:
                    body:
                      columns: []
              branding:
                summary: Set a header logo (Base64 PNG) left-aligned in an 80 pt band
                value:
                  board:
                    header:
                      image: "<base64-encoded PNG or JPEG>"
                      height: 80
                      alignment: left
      responses:
        '200':
          description: Configuration accepted and applied.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AcceptedResponse'
        '400':
          description: |
            Invalid field value (e.g. unavailable font, invalid hex color, out-of-range rows,
            or invalid column definition).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid bearer token.
        '402':
          description: |
            Tier upgrade required. Returned when the caller's purchased tier is below
            the minimum required for this endpoint or field. Check `minimum_tier` to
            determine which tier to offer the user.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpgradeRequiredResponse'

  /board:
    get:
      summary: Get current board display state
      description: |
        Returns a snapshot of the board's currently displayed content — the character
        and color shown in each cell at the moment of the request.

        Use this endpoint to pre-fill a grid editor with the board's live content
        before composing a new message. Cells using the board's default color omit
        the `color` field.
      operationId: getBoard
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Current board display state.
          content:
            application/json:
              schema:
                type: object
                required: [rows]
                properties:
                  rows:
                    type: array
                    description: |
                      2D array of cells. Outer array is rows (top to bottom), inner
                      array is columns (left to right). Dimensions match the board's
                      configured `rows` and `cols`.
                    items:
                      type: array
                      items:
                        $ref: '#/components/schemas/BoardStateCell'
              example:
                rows:
                  - - text: "H"
                    - text: "E"
                      color: "#FF6600"
                    - text: "L"
                    - text: "L"
                    - text: "O"
                  - - text: " "
                    - text: " "
                    - text: " "
                    - text: " "
                    - text: " "
        '401':
          description: Missing or invalid bearer token.
        '402':
          description: |
            Tier upgrade required. Returned when the caller's purchased tier is below
            the minimum required for this endpoint or field. Check `minimum_tier` to
            determine which tier to offer the user.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpgradeRequiredResponse'
    delete:
      summary: Clear the board
      description: Clears all cells on the board, returning it to a blank state.
      operationId: deleteBoard
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Board cleared.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AcceptedResponse'
              example:
                status: accepted
        '401':
          description: Missing or invalid bearer token.
        '402':
          description: |
            Tier upgrade required. Returned when the caller's purchased tier is below
            the minimum required for this endpoint or field. Check `minimum_tier` to
            determine which tier to offer the user.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpgradeRequiredResponse'
