WorshipKit Billboard API
A guide to the Billboard REST API — for church admins who want to automate their signage from a script, for the AI assistants helping them, and for the WorshipKit team.
Public URL: https://worshipkit.com/billboard-api Raw markdown at: https://worshipkit.com/billboard-api.md
Everything on this page is scoped to the authenticated user's Organization — you can't see or change another org's playlists, devices, or media.
What Billboard is
Billboard is WorshipKit's digital-signage system. A church admin puts media (images, videos, live streams, Flickr album refs, countdowns) into playlists, assigns those playlists to devices — a Roku, an Android TV, a webOS TV, a browser tab on a hallway display — and the devices loop the playlist until told otherwise.
Core objects:
- Media item (
BillboardMediaItem) — an image, video, or embed. Optionally scoped to a date window (start_date..end_date). - Playlist (
BillboardPlaylist) — an ordered list of media items, played on a loop. - Device (
BillboardOutput) — a physical / virtual display that plays a playlist. Devices are paired to the org via a short code. - Device group (
BillboardDeviceGroup) — many devices playing the same playlist; changing the group's playlist changes all its devices at once.
Authentication
Every request needs a Bearer token in the Authorization header. Two token types work:
- Personal Access Token (PAT) — created by a signed-in user in the browser at /settings/api-tokens (accessible from the user menu → Your Profile → *"Manage API tokens →"*). The raw token is shown ONCE at creation time; copy it into your agent's config immediately, and revoke old tokens from the same page. Scripted alternative:
POST /api_tokens { "name": "…" }returns the same JSON envelope, but the browser flow is the recommended path for humans. Scope: everything the user can do. - Doorkeeper OAuth token — issued by the standard OAuth2 flow at
/oauth/authorize. Scoped.
Authorization: Bearer wk_pat_abc123…
Content-Type: application/json
Billboard access by role
A valid token is not enough — the token's user must also have access to the Billboard app in their organization. Every /api/v2/billboard/* endpoint enforces this and returns 403 with { "error": "Insufficient permissions", "hint": "Ask your Admin to grant you access to Billboard." } when it fails.
- Owner and Admin — always have Billboard access.
- Editor — only if an Owner/Admin granted them the Billboard feature (and the org is subscribed to it).
- User — no Billboard access; every Billboard call returns
403.
If you get a 403 on an endpoint whose scope looks correct, the user's role or feature grant is the cause — ask them to have an Admin grant Billboard access.
All examples below assume the base URL https://worshipkit.com and that header is set. Anything you POST / PUT uses a JSON body.
Playlists
Create a playlist
POST /api/v2/playlists
{
"playlist": {
"title": "Sunday morning welcome",
"is_active": true,
"seconds_per_slide": 10
}
}
Response: the created playlist as JSON. Save the id.
List / read / update / delete
GET /api/v2/playlists
GET /api/v2/playlists/:id
PUT /api/v2/playlists/:id
DELETE /api/v2/playlists/:id
Updatable fields (all optional): title, is_active, billboard_flickr_album_id, service_type_id, countdown_leading_minutes, seconds_per_slide, series_art_enabled, series_art_source, series_art_service_type_id, series_art_uploaded_url, series_art_uploaded_filename.
Series art (per-playlist, #482)
Each playlist can render a "series art" tile as the first preview in its rotation. Two sources are supported:
- Planning Center — set
series_art_sourceto"pco"andseries_art_service_type_idto one of the caller org'sServiceTypes. The playlist tile shows the artwork attached to the nearest current/upcoming plan for that service type, refreshed by the org's normal PCO sync (no per-request PCO calls). - Manual upload — set
series_art_sourceto"upload"andseries_art_uploaded_url(S3 URL from the standard/billboard/signed_urls/v2flow) +series_art_uploaded_filename(for display).
The tile only renders when both series_art_enabled is true AND the source resolves to an image URL. Send "" on any of the four series_art_* string fields to clear it (Disconnect / Remove flows).
Cross-org protection: passing a series_art_service_type_id that belongs to another org is rejected with 422 on both POST /playlists and PATCH /playlists/:id.
The playlist JSON also exposes these read-only projections:
per_playlist_series_art_active?— true when the tile will rendereffective_per_playlist_series_art_url— the resolved image URLeffective_per_playlist_series_art_title— filename (upload) or cached PCO series title, falling back to"Series Art"
GET /api/v2/playlists/series_art_service_types
Returns the caller org's ServiceTypes as [{ id, name }, …] for the SPA's Connect picker. Org-scoped.
Add a media item to a playlist
POST /api/v2/playlists/:playlist_id/add_media_item
{ "media_item_id": 42 }
The item lands at the end of the playlist. Response is the full playlist including billboard_playlist_media_items.
Remove a media item
DELETE /api/v2/playlists/:playlist_id/remove_media_item
{ "item_id": 17 }
item_id is the playlist-item id (from billboard_playlist_media_items), NOT the underlying media_item_id. When a playlist has an item added multiple times, each has its own playlist-item id.
Reorder items
PUT /api/v2/playlists/:playlist_id/reorder_items
{ "items": [17, 42, 88, 12] }
Array of playlist-item ids in the order they should play.
Publish (push updates to devices)
POST /api/v2/playlists/:playlist_id/publish
Enqueues an async BillboardPlaylistExportJob (204 → 202 with a poll id). Devices refresh via Pusher when it lands.
Per-item display mode
PUT /api/v2/playlists/:playlist_id/items/:item_id/display_mode
{ "display_mode": "scale_to_fit" }
Valid modes are documented in docs/billboard_image_display_modes.md.
Media items
Upload / register
POST /api/v2/media/add_upload
{
"upload_details": {
"title": "Autumn sunrise",
"credit": "Photo by Sam L.",
"url": "https://s3.amazonaws.com/org-42/autumn.jpg"
}
}
The file must already exist at url (usually an S3 presigned upload URL — see POST /api/v2/signed_urls for how the WK web UI obtains them). A background job generates thumbnails afterwards.
List / read / update / delete
GET /api/v2/media
GET /api/v2/media/:id
PUT /api/v2/media/:id
DELETE /api/v2/media/:id
Updatable fields: title, credit, start_date, end_date, url.
Scheduling — start / end dates
start_date and end_date on a media item are global — they control when the item appears on any playlist it's part of. The Billboard player filters items out of the running loop whenever the current time is outside a scheduled item's window.
Both fields accept ISO-8601 timestamps in UTC ("2026-12-24T18:00:00Z") or a bare date ("2026-12-24", interpreted as 00:00Z). Omit or null a field to remove that bound.
Example — a Christmas Eve slide that only appears the last week of December:
{
"billboard_media_item": {
"start_date": "2026-12-19T00:00:00Z",
"end_date": "2026-12-26T00:00:00Z"
}
}
PUT to /api/v2/media/:id.
Note: date windows are attached to the media item itself, not to a playlist_media_item junction. If you want the same photo to run in two different windows on two different playlists, register it as two media items with different date windows.
Devices
Devices in the schema are BillboardOutput rows — one row per physical display.
List / read / update / delete
GET /api/v2/devices
GET /api/v2/devices/:id
POST /api/v2/devices
PUT /api/v2/devices/:id
DELETE /api/v2/devices/:id
Updatable fields: title, notes, campus_id, pairing_code, billboard_playlist_id, billboard_device_group_id.
Assign a device to a playlist
Two ways depending on scale:
Directly on the device — the device plays this exact playlist:
PUT /api/v2/devices/:id
{ "device": { "billboard_playlist_id": 123 } }
Via a device group — many devices share one playlist:
PUT /api/v2/devices/:id
{ "device": { "billboard_device_group_id": 7 } }
Then set the group's playlist:
PUT /api/v2/device_groups/7
{ "device_group": { "billboard_playlist_id": 123 } }
Now every device in group 7 plays playlist 123.
Pair a new device
The device shows a short code on-screen. A signed-in user:
POST /api/v2/devices/get_code → { code: "ABCD" }
POST /api/v2/devices/validate_code
{ "code": "ABCD" }
validate_code claims the device for the current org and returns the BillboardOutput row for further configuration.
Device groups
GET /api/v2/device_groups
GET /api/v2/device_groups/:id
POST /api/v2/device_groups
PUT /api/v2/device_groups/:id
DELETE /api/v2/device_groups/:id
POST /api/v2/device_groups/:id/add_device
DELETE /api/v2/device_groups/:id/remove_device
Updatable fields: name, billboard_playlist_id.
Add / remove devices
POST /api/v2/device_groups/:group_id/add_device
{ "device_id": 42 }
DELETE /api/v2/device_groups/:group_id/remove_device
{ "device_id": 42 }
Devices removed from a group lose the group's playlist unless you also set billboard_playlist_id directly on the device.
Common workflows
1. Create a playlist, add two items, publish
POST /api/v2/playlists
{ "playlist": { "title": "Foyer loop", "seconds_per_slide": 8 } }
→ { id: 501, ... }
POST /api/v2/playlists/501/add_media_item { "media_item_id": 88 }
POST /api/v2/playlists/501/add_media_item { "media_item_id": 91 }
POST /api/v2/playlists/501/publish
2. Assign the lobby TV to that playlist
GET /api/v2/devices → find the "Lobby TV" row
PUT /api/v2/devices/12
{ "device": { "billboard_playlist_id": 501 } }
3. Group four hallway TVs and swap their playlist for the summer
POST /api/v2/device_groups
{ "device_group": { "name": "Hallways", "billboard_playlist_id": 501 } }
→ { id: 7, ... }
POST /api/v2/device_groups/7/add_device { "device_id": 12 }
POST /api/v2/device_groups/7/add_device { "device_id": 13 }
POST /api/v2/device_groups/7/add_device { "device_id": 14 }
POST /api/v2/device_groups/7/add_device { "device_id": 15 }
# Later:
PUT /api/v2/device_groups/7
{ "device_group": { "billboard_playlist_id": 604 } }
4. Add a Christmas-only slide
POST /api/v2/media/add_upload
{ "upload_details": { "title": "Christmas eve",
"credit": "Photo: Kim P.",
"url": "https://s3.example/christmas.jpg" } }
→ { id: 130, ... }
PUT /api/v2/media/130
{ "billboard_media_item": {
"start_date": "2026-12-19T00:00:00Z",
"end_date": "2026-12-26T00:00:00Z"
} }
POST /api/v2/playlists/501/add_media_item { "media_item_id": 130 }
POST /api/v2/playlists/501/publish
Between Dec 19 and Dec 26 the slide plays; outside that window the device silently skips it.
5. Remove an item from a playlist without deleting the media
GET /api/v2/playlists/501 # find the target playlist-item id
DELETE /api/v2/playlists/501/remove_media_item { "item_id": 4402 }
POST /api/v2/playlists/501/publish
4402 is the BillboardPlaylistMediaItem.id, not the underlying media id — same media item can be present multiple times in a playlist and each has its own row.
Errors
Standard REST responses:
200— success (returns the affected object as JSON)201— created (POST that made something new)202— accepted (async job started; poll for status)204— success, no body401— no token, expired token, or missingAuthorizationheader403— token lacks the required scope, or the token's user does not have access to Billboard (see below)404— object not found or not in your org422— validation failed; body has{ "error": "message" }
For AI agents
If a WorshipKit user asks you to configure Billboard for them:
1. Ask the user for a PAT (or link them to https://worshipkit.com/settings/api-tokens to create one). Do not ask for their WorshipKit password. 2. Use GET /api/v2/devices and GET /api/v2/playlists first so you're proposing edits against IDs the org actually has. 3. When adding many items to a playlist, add them first, then POST /publish once at the end — publish enqueues a job that pushes to devices, so batching saves updates. 4. Media-item start_date / end_date is global. If the user says "show this only on our Sanctuary TV during Advent," they mean start_date / end_date on the media item — there is no per-playlist scheduling; put date-windowed items in a dedicated seasonal playlist if you need per-device isolation. 5. Devices identify themselves in the payload by their id. If a user names a device ("Lobby TV"), fetch GET /api/v2/devices and match against title — don't guess IDs.
Where this lives
- Source of truth:
docs/billboard_api.mdin the WorshipKit web repo. - Served publicly at:
/billboard-api(HTML) and/billboard-api.md(raw markdown). - Discovered by AI agents via:
/llms.txtat the site root. - Kept up to date by: the "Billboard API docs stay current" contributor rule in
CLAUDE.md.
If you add, rename, or remove a Billboard endpoint, field, or wire shape in app/controllers/api/v2/billboard/* or the underlying schema, update this file in the same commit.