REST API

Arrivals Board includes a built-in HTTP server that accepts messages via a REST API. Any device on the same local network can post messages to the board using simple HTTP requests.

Endpoint

POST /api/v1/message

The base URL depends on your device's local network address. You can find the full URL in Settings > Sources > Local Server.

Typical format: http://<device-ip>:<port>/api/v1/message

Headers

Header Value Required
Content-Type application/json Yes
X-API-Key Your API key Yes

The API key is generated automatically when you enable the local server. You can view and regenerate it in Settings > Sources > Local Server.

Request body

The request body is a JSON object with the following fields:

Field Type Required Description
text String Yes The message text to display on the board. Supports inline formatting.
sender String No The sender name shown in the header row. Defaults to "API" if omitted. Also supports inline formatting.
priority String No Controls when the message is displayed. Accepts "queue", "next", or "now". Defaults to "queue". See below.
urgent Boolean No Legacy field. When true, equivalent to "priority": "now". If both priority and urgent are provided, priority takes precedence.
indicatorColor String No Sets the indicator lamp color. Accepts "red", "amber", "green", "off", or a hex color string (e.g., "#FF6600"). Defaults to "off".
avatar String No Base64-encoded PNG or JPEG image data for a custom sender avatar. Maximum decoded size: 1 MB. Recommended dimensions: 512×512.
avatarURL String No An absolute http or https URL to a PNG or JPEG image for the sender avatar. If both avatar and avatarURL are provided, avatar takes precedence.

Priority levels

Priority Behavior
"queue" The message joins the queue in time order. It will be shown when its turn comes. This is the default.
"next" The message jumps to the front of the queue. The current message finishes its display duration, then this message flips in.
"now" The message interrupts the currently displayed message immediately. The reels retarget mid-animation to show the new content.

Example request body

{
  "text": "Build #1234 passed all tests.",
  "sender": "CI Server",
  "priority": "next",
  "indicatorColor": "green"
}

Response

A successful request returns:

{
  "status": "ok"
}

HTTP status code: 200 OK

Error codes

Status code Meaning Description
400 Bad Request The request body is missing, malformed, or the required text field is empty.
401 Unauthorized The X-API-Key header is missing or contains an invalid key.
429 Too Many Requests Rate limit exceeded. The API allows a maximum of 10 requests per minute. Wait and retry.

Error responses include a JSON body with a message field:

{
  "status": "error",
  "message": "Rate limit exceeded. Maximum 10 requests per minute."
}

Code examples

cURL

curl -X POST "http://192.168.1.42:8080/api/v1/message" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-api-key-here" \
  -d '{
    "text": "Deployment complete.",
    "sender": "Deploy Bot",
    "priority": "next",
    "indicatorColor": "green"
  }'

Python

import requests

response = requests.post(
    "http://192.168.1.42:8080/api/v1/message",
    headers={
        "Content-Type": "application/json",
        "X-API-Key": "your-api-key-here",
    },
    json={
        "text": "Deployment complete.",
        "sender": "Deploy Bot",
        "priority": "next",
        "indicatorColor": "green",
    },
)

print(response.json())  # {"status": "ok"}

JavaScript (fetch)

const response = await fetch("http://192.168.1.42:8080/api/v1/message", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Key": "your-api-key-here",
  },
  body: JSON.stringify({
    text: "Deployment complete.",
    sender: "Deploy Bot",
    priority: "next",
    indicatorColor: "green",
  }),
});

const data = await response.json();
console.log(data); // { status: "ok" }

Board layout endpoint

Automations that send messages can query the board's current layout so they can size and format text to fit. This is the same endpoint the built-in web form uses to draw its fit-ring indicator.

GET /api/v1/board-layout

No authentication required. Returns the current dimensions plus user-visible display settings as JSON.

Response fields

Field Type Description
message_columns Integer Width of the message grid in character cells.
message_rows Integer Height of the message grid. A single "page" holds message_columns × message_rows cells.
sender_columns Integer Width of the sender name row, in cells.
sender_rows Integer Height of the sender name row. Always 1 today.
clock_display Boolean Whether the clock overlay is shown.
settings_button_display Boolean Whether the settings-gear overlay is shown. Shifts the pagination indicator on the last row by one cell when visible.
reel_type String One of "strict", "unicode_uppercase", or "unicode_lowercase" — see below.
long_message_display String "truncate" (messages clipped to one page) or "complete" (messages split across pages).

Reel type values

Value Behavior
strict Only supported characters are displayed; anything else renders as .
unicode_uppercase Lowercase input is displayed using the uppercase glyphs.
unicode_lowercase Lowercase glyphs are preserved.

Example response

{
  "message_columns": 30,
  "message_rows": 9,
  "sender_columns": 20,
  "sender_rows": 1,
  "clock_display": true,
  "settings_button_display": true,
  "reel_type": "unicode_uppercase",
  "long_message_display": "truncate"
}

503 during board warm-up

For a brief window after the app launches — before the board view has reported its live dimensions — the endpoint returns 503 Service Unavailable:

{ "error": "Board layout not yet available. Please try again." }

Clients should retry after a short delay (e.g., 250 ms). Persisted dimensions from the previous session are restored on launch, so this window typically only appears on a truly first launch.

Diagnostics endpoint

For business installations and support troubleshooting, Arrivals exposes a read-only snapshot of internal source health.

GET /api/v1/diagnostics

Auth-gated by the same X-API-Key header as POST /api/v1/message. The response contains no message bodies, no auth tokens, and no PII — only counters and timestamps — so screenshots are safe to share with support.

Use cases

  • Did a source stall? Compare each source's seconds_since_last_message against its expected polling interval (e.g., a 5-minute RSS feed should never exceed ~600 seconds).
  • Is the app actually running and processing the queue? process_uptime_seconds and queue_depth confirm liveness without needing a screen on the device.
  • Is sound supposed to be playing right now? When sound_hours.enabled is true, compare current_minutes against start_minutes/end_minutes.

Response fields

Field Type Description
schema_version Integer Bumped if the response shape changes incompatibly.
collected_at String ISO-8601 timestamp when the snapshot was assembled.
process_uptime_seconds Integer Seconds since the message-queue service was constructed (effectively app launch).
queue_depth Integer Messages currently waiting to display. Includes items persisted from previous app launches, so a fresh process can have a non-zero queue immediately.
current_message_id String / null UUID of the message currently on the board.
sources Array One entry per active message source — see below.
sound_hours Object / null Sound-hours scheduling state. Present when configured.

sources[]

Field Type Description
id String Source UUID.
display_name String Human-readable name shown in Settings.
source_type String Source kind — e.g., rss, mastodon, bluesky.
messages_received Integer Messages this source has enqueued since the current process launched. Resets on every app launch. A 0 here does not mean the source is broken — it may simply mean the source's polling interval hasn't elapsed yet this session, while previously-received items continue to display from the persisted queue. Use last_message_at for "is this source healthy?" judgments.
stream_task_alive Boolean True while the source's consumer task is running.
stream_started_at String / null ISO-8601 timestamp when the consumer task launched.
last_message_at String / null Most recent message from this source. null until the first message arrives.
seconds_since_last_message Integer / null Convenience: seconds between collected_at and last_message_at.

sound_hours

Field Type Description
enabled Boolean Whether sound-hours scheduling is on.
start_minutes Integer Window start, minutes since midnight (e.g., 480 = 8:00 AM).
end_minutes Integer Window end, minutes since midnight.
current_minutes Integer Current local time, minutes since midnight.

Example response

{
  "schema_version": 1,
  "collected_at": "2026-04-29T14:00:00Z",
  "process_uptime_seconds": 7200,
  "queue_depth": 3,
  "current_message_id": "A1B2-...",
  "sources": [
    {
      "id": "11111111-1111-1111-1111-111111111111",
      "display_name": "Hacker News",
      "source_type": "rss",
      "messages_received": 42,
      "stream_task_alive": true,
      "stream_started_at": "2026-04-29T12:00:00Z",
      "last_message_at": "2026-04-29T13:55:00Z",
      "seconds_since_last_message": 300
    }
  ],
  "sound_hours": {
    "enabled": true,
    "start_minutes": 480,
    "end_minutes": 1140,
    "current_minutes": 840
  }
}

Error codes

Status code Meaning Description
401 Unauthorized The X-API-Key header is missing or contains an invalid key.
503 Service Unavailable The diagnostics provider is not yet wired (only during the brief window between app launch and the local server starting). Retry after a short delay.

OAuth endpoints (Apple TV)

On Apple TV, signing in to services like Mastodon requires a secondary device to complete the OAuth flow. Arrivals exposes the following endpoints on the local server to facilitate this process. These are used internally by the QR code authentication system described in QR Authentication and are not intended for direct use.

Method Endpoint Purpose
GET /auth/mastodon/setup Displays the Mastodon sign-in page on a secondary device.
POST /auth/mastodon/register Registers the app with the Mastodon instance.
POST /auth/mastodon/complete Completes the OAuth flow after the user authorizes on their secondary device.
GET /auth/callback Receives the OAuth callback redirect from the Mastodon instance.