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.
Enabling the local server
The REST API is served by the Arrivals local server. Make sure it is enabled in Settings > Sources > Local Server before making requests. See Local Server for setup details.
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_messageagainst 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_secondsandqueue_depthconfirm liveness without needing a screen on the device. - Is sound supposed to be playing right now? When
sound_hours.enabledis true, comparecurrent_minutesagainststart_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. |
Note
These endpoints are only available on Apple TV and are used exclusively for the QR code sign-in flow. You do not need to call them directly.