Your screens, scriptable.
Manage screens, playlists and the library from the terminal, from scripts, or from an AI assistant such as Claude Code, Codex or ChatGPT. The ztalio CLI and the REST API are included with the Pro and Network plans; create a key in Settings and you are in.
Three commands to a screen.
A key from Settings, the CLI from npm, and the first push lands on the TV within seconds.
Create a key
Sign in at ztalio.com → Settings → API access → Create API key. Pick its scopes. The key is shown once; copy it.
# looks like ztk_…
Install and sign in
Node 18 or newer. login stores the key in ~/.config/ztalio/config.json (mode 600).
npm install -g ztalio && ztalio login
Look, then push
Screens and playlists go by id or by part of their name; case and punctuation do not matter.
ztalio screens
ztalio push "Happy Hour" --to Lobby# the same thing over plain HTTP curl -H "Authorization: Bearer ztk_…" https://api.ztalio.com/v1/screens
Use it with an AI assistant.
Any agent with a shell can drive the CLI: Claude Code, Codex, ChatGPT with a terminal, or your own. Install it, sign in, say what you want on which TV.
The guide an assistant reads first.
ztalio agent prints how the tool is meant to be used. Point the assistant at it, or paste it into its instructions. An excerpt:
You control a Ztalio account's TV screens. Everything is
idempotent and safe to retry; add --json to every command
and parse stdout. Typical flow:
1. ztalio screens --json → which TVs exist, online, showing
2. ztalio media --json → the library (title, mediaType, s3Key)
3. ztalio playlists --json → existing playlists
4. Make or edit a playlist:
ztalio playlists create "Lunch specials"
ztalio playlists add "Lunch specials" "Tacos.jpg" "Burger.jpg"
ztalio playlists set "Lunch specials" --speed 8 --transition dissolve
5. Put it on the screen:
ztalio push "Lunch specials" --to "Lobby" (updates in seconds)
6. New content:
ztalio upload ./menu.png --folder Promos (~1 min, then in media)
Before a push, confirm with the user which screen(s) and
which playlist. Never push to --all unless the user said
"all screens".Put the lunch menu on the lobby TV at 8 and the dinner menu at 4
The assistant finds the Lobby screen and the two playlists, gives each a schedule in your account’s timezone, and pushes. Roughly:
ztalio screens --json ztalio playlists set "Lunch menu" --schedule "8-16" ztalio playlists set "Dinner menu" --schedule "16-23" ztalio push "Lunch menu" --to Lobby
- Assistants confirm the target screen before pushing, and never use
--allunless you said all screens. - Keys are scoped (read, write, push) and revocable from Settings at any time; a read-only key cannot change a TV.
- Errors come back as JSON with a
code, so an agent knows whether to ask you to sign in, wait a minute, or upgrade.
Or connect over MCP
The same sixteen tools are an MCP server — locally through the CLI, or hosted with no install. Your API key is the only credential, so scopes, limits and the audit trail apply exactly as for the REST API.
# Claude Code, local (uses the key from `ztalio login`) claude mcp add ztalio -- ztalio mcp # Claude Code, hosted claude mcp add --transport http ztalio https://api.ztalio.com/v1/mcp --header "Authorization: Bearer ztk_…" # Claude Desktop → Settings → Developer → Edit config, under "mcpServers" "ztalio": { "command": "ztalio", "args": ["mcp"] }
claude.ai: Customize → Connectors → Add custom connector → URL https://api.ztalio.com/v1/mcp, Authentication “No sign-in”, request header authorization = Bearer ztk_…. Cursor, Codex and other MCP hosts: command ztalio, args ["mcp"]. ztalio mcp --setup prints all of this. Tools: whoami, list_screens, rename_screen, list_playlists, get_playlist, create_playlist, add_to_playlist, remove_from_playlist, update_playlist, delete_playlist, push_playlist, list_media, list_folders, list_slide_templates, create_slide (with preview the picture comes back inline), screen_analytics.
The CLI.
A zero-dependency Node client over the REST API. Source on GitHub, package on npm, MIT licence.
npm install -g ztalio ztalio login # paste a key from ztalio.com → Settings → API access ztalio screens # your TVs: online/offline, what's playing ztalio push "Happy Hour" --to Lobby
Account
ztalio login [--key ztk_…]save your API key (Settings → API access; Pro & Network)ztalio whoamiaccount, plan, usageScreens
ztalio screensyour TVs: online/offline, what's playingztalio screens rename <screen> <name>rename a screenztalio push <playlist> --to "Lobby,Bar 2"(or --all) the TVs change within secondsPlaylists
ztalio playlistslist playlistsztalio playlists show <playlist>items, speed, transition, shuffle, music, scheduleztalio playlists create <name>create a playlistztalio playlists add <playlist> <item…>items by title (see ztalio media) or keyztalio playlists remove <playlist> <item…>remove itemsztalio playlists set <playlist> --speed 7 --transition dissolve --shuffle on --music "Tokyo Dreams"speed in seconds (2–3600); transition none, dissolve, slide, flip or zoomztalio playlists set <playlist> --schedule "mon-fri 8-17"hours in your account timezone; "off" clearsztalio playlists delete <playlist>delete a playlistLibrary
ztalio media [--folder <name>] [--type image|video|audio]the library: title, type, keyztalio upload <file…> [--folder <name>]upload files; they appear in the library within a minuteztalio slide --template promo --title "Happy hour" --badge "$5 margaritas" --push Lobbya slide from text — rendered on the server with the Studio engine, saved to the library, optionally pushedztalio slide --items "Tacos=$4|street style; Burrito=$9 *Popular" --title Lunch --add-to "Lunch menu"menu board or price list from name=price pairsztalio slide --prompt "…" --preview check.pnglet the Studio assistant lay it out; preview renders without savingztalio slide --templatesthe eight templates and the fields each acceptsztalio folders | folders create <name>list or create foldersztalio musicbuilt-in background tracksRaw API & assistants
ztalio api <METHOD> </path> [--data '{…}']raw call to https://api.ztalio.com/v1ztalio agenthow an AI assistant should use this toolNames, not ids. Screens, playlists, folders and media can be named by id or by part of their name; case and punctuation do not matter (beachsunset finds “Beach Sunset”).
Machine output. Add --json to any command for JSON on stdout. Errors go to stderr as JSON with a code (NO_KEY, UPGRADE_REQUIRED, SCOPE, RATE_LIMITED, NOT_FOUND).
Environment. ZTALIO_API_KEY overrides the saved key (handy in CI and containers), ZTALIO_API_BASE changes the API base, ZTALIO_CONFIG_DIR moves the config file. Global flags: --json, --key, --base.
The REST API.
Manage your Ztalio screens, playlists and library from scripts and AI assistants. Plain JSON over HTTPS; the CLI is a thin client over it. The live document is at api.ztalio.com/v1/openapi.json.
| Status | Code | Meaning |
|---|---|---|
| 401 | NO_KEY · BAD_KEY · REVOKED_KEY | No Authorization header, the key does not exist, or it was revoked in Settings. |
| 402 | UPGRADE_REQUIRED | The plan behind the key does not include API access (Pro and Network do). |
| 403 | SCOPE | The key lacks the scope the endpoint needs (read, write or push). |
| 404 | NOT_FOUND | No such screen, playlist, folder or upload on this account. |
| 429 | RATE_LIMITED | More than 120 requests in a minute on this key. Wait a minute and retry. |
| 502 | UPSTREAM | A Ztalio service behind the API failed. Safe to retry. |
Every error body is { error, code }. Successful calls answer 200 with JSON; the shapes are listed per endpoint below and in the objects at the end.
Account
The account behind this key
- Returns
- 200
Account— The account behind this key401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope429Error— Rate limited (120/min per key)
Screens
List screens
- Returns
- 200
{ screens: Screen[] }401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope429Error— Rate limited (120/min per key)
Screen uptime analytics (same data as ztalio.com/analytics)
- Path
daysinteger · query — window in days (1–30)- Returns
- 200OK401
Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope429Error— Rate limited (120/min per key)
Rename a screen
- Path
idstring · pathrequired- Body
namestringrequired- Returns
- 200
{ success: boolean }— Renamed401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope404Error— Not found429Error— Rate limited (120/min per key)
Push a playlist (or ad-hoc items) to one screen; the TV changes within seconds
- Path
idstring · pathrequired- Body
- Either playlistId or items[]
playlistIdstringitemsobject[]items[].keystringrequireditems[].type"image" | "video"items[].titlestringtransition"none" | "dissolve" | "slide" | "flip" | "zoom"slideSecondsinteger — 2–3600 - Returns
- 200
PushResult— Pushed400Empty playlist / bad body401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope404Screen not found429Error— Rate limited (120/min per key)
Push to several screens
- Body
- Either playlistId or items[]
playlistIdstringitemsobject[]items[].keystringrequireditems[].type"image" | "video"items[].titlestringtransition"none" | "dissolve" | "slide" | "flip" | "zoom"slideSecondsinteger — 2–3600screenIdsstring[]required - Returns
- 200
PushResult— Pushed401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope429Error— Rate limited (120/min per key)
Playlists
List playlists
- Returns
- 200
Playlist[]401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope429Error— Rate limited (120/min per key)
Create a playlist
- Body
namestringrequired- Returns
- 200
{ playlistId: string, name: string }— Created401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope429Error— Rate limited (120/min per key)
Get a playlist
- Path
idstring · pathrequired- Returns
- 200
Playlist401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope404Error— Not found429Error— Rate limited (120/min per key)
Update name, speed, transition, shuffle, music or schedule
- Path
idstring · pathrequired- Body
namestringslideSecondsinteger — 2–3600transition"none" | "dissolve" | "slide" | "flip" | "zoom"shuffleEnabledbooleanbackgroundMusicS3Keystring | nullscheduleobject | null- Returns
- 200
Playlist— The updated playlist401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope404Error— Not found429Error— Rate limited (120/min per key)
Delete a playlist
- Path
idstring · pathrequired- Returns
- 200
{ deleted: boolean }— Deleted401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope404Error— Not found429Error— Rate limited (120/min per key)
Append library items (by s3Key)
- Path
idstring · pathrequired- Body
s3Keysstring[]required- Returns
- 200
{ added: integer, itemsCount: integer }— Appended401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope404Error— Not found429Error— Rate limited (120/min per key)
Remove items (by s3Key)
- Path
idstring · pathrequired- Body
s3Keysstring[]required- Returns
- 200OK401
Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope404Error— Not found429Error— Rate limited (120/min per key)
Library
The library feed: folders and items
- Returns
- 200
{ folders: { id: string, name: string, itemCount: integer }[], items: MediaItem[] }401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope429Error— Rate limited (120/min per key)
List folders
- Returns
- 200
{ items: Folder[] }— Folders you own or were granted401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope429Error— Rate limited (120/min per key)
Create a folder
- Body
namestringrequiredparentFolderIdstring- Returns
- 200
Folder— Created401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope429Error— Rate limited (120/min per key)
Items in a folder
- Path
idstring · pathrequired- Returns
- 200
{ items: MediaItem[] }— Items in the folder401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope404Error— Not found429Error— Rate limited (120/min per key)
The built-in background-music library
- Returns
- 200
{ tracks: Track[] }— The built-in tracks401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope429Error— Rate limited (120/min per key)
Slides
The eight slide templates and the fields each accepts
- Returns
- 200
{ templates: { id: string, name: string, blurb: string, size: object, fields: object }[] }— Templates401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope429Error— Rate limited (120/min per key)
Render a slide and return the PNG (base64) without saving it
- Body
- Either template + fields, or a complete Studio doc.
template"menu-board" | "price-list" | "promo" | "announcement" | "hours" | "welcome" | "event" | "portrait-menu" — default "announcement"fieldsSlideFieldsdocobject — a complete StudioDoc (as returned by /slides/propose)namestringplaylistIdstring — append the slide to this playlistscreenIdsstring[] — push the slide to these screens (needs the push scope) - Returns
- 200
{ png: string, size: object, bytes: integer, doc: object }— The PNG400Error— Bad request401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope429Error— Rate limited (120/min per key)
Let the Studio assistant lay a slide out from a sentence (same quotas as ztalio.com/studio)
- Body
promptstringrequiredtemplatestringfieldsSlideFields- Returns
- 200
{ doc: object, reply: string, turnsLeft: integer }— A complete Studio doc to pass to POST /slides as `doc`400Error— Bad request401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope422Error— The assistant did not produce a slide429Error— Rate limited (120/min per key)
Make a slide from text — a menu board, promo, announcement, hours, welcome or event — and save it to the library (folder "Studio"); optionally add it to a playlist and push it to screens
- Body
- Either template + fields, or a complete Studio doc.
template"menu-board" | "price-list" | "promo" | "announcement" | "hours" | "welcome" | "event" | "portrait-menu" — default "announcement"fieldsSlideFieldsdocobject — a complete StudioDoc (as returned by /slides/propose)namestringplaylistIdstring — append the slide to this playlistscreenIdsstring[] — push the slide to these screens (needs the push scope) - Returns
- 200
SlideResult— Saved400Error— Bad request401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope429Error— Rate limited (120/min per key)
Uploads
Start an upload (S3 multipart). Then PUT each part to its URL and call complete.
- Body
filenamestringrequiredcontentTypestringrequiredsizeBytesintegerrequiredtotalPartsintegerrequired — parts of up to 100 MBfolderIdstring — default "root"- Returns
- 200
{ uploadId: string, s3Key: string, totalParts: integer, contentType: string, folderId: string }— Upload started401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope429Error— Rate limited (120/min per key)
Presigned PUT URLs for parts
- Path
uploadIdstring · pathrequired- Body
partNumbersinteger[]required- Returns
- 200
{ uploadId: string, parts: { partNumber: integer, url: string }[] }— Presigned PUT URLs401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope404Error— Not found429Error— Rate limited (120/min per key)
Finish the upload; the file is optimised and appears in the library within a minute
- Path
uploadIdstring · pathrequired- Body
partsobject[]requiredparts[].partNumberintegerparts[].etagstring- Returns
- 200
{ uploadId: string, s3Key: string, folderId: string, location: string }— Completed; the file is optimised and appears in the library within a minute401Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope404Error— Not found429Error— Rate limited (120/min per key)
Abort an upload
- Path
uploadIdstring · pathrequired- Returns
- 200OK401
Error— Missing, unknown or revoked key402Error— Plan without API access403Error— Key lacks the scope404Error— Not found429Error— Rate limited (120/min per key)
Objects
Screen
idstringnamestringstatus"paired" | "unlinked"onlinebooleanseen in the last 3 minuteslastSeenAtstring | nulllinkedAtstring | nullnowPlayingobject | nullnowPlaying.itemCountintegernowPlaying.firstItemstring | nullnowPlaying.transitionstring | nullnowPlaying.slideSecondsinteger | nullnowPlaying.pushedAtstring | null
Playlist
playlistIdstringnamestringitemsobject[]items[].s3Keystringitems[].titlestring | nullitems[].mediaType"image" | "video" | "audio" | nullshuffleEnabledbooleanbackgroundMusicS3Keystring | nullscheduleobject | null{ rules: [{ days, startHour, endHour, … }] } — see the web apptransition"none" | "dissolve" | "slide" | "flip" | "zoom"slideSecondsinteger | null2–3600updatedAtstring
MediaItem
idstrings3Keystringuse this in playlist items and pushesthumbKeystring | nulltitlestringfolderIdstringmediaType"image" | "video" | "audio"sizeintegercreatedAtstring
PushRequest
Either playlistId or items[]
playlistIdstringitemsobject[]items[].keystring · requireditems[].type"image" | "video"items[].titlestringtransition"none" | "dissolve" | "slide" | "flip" | "zoom"slideSecondsinteger2–3600
Account
emailstringnamestring | nulluserType"primary" | "secondary"timezonestring | nullIANA zone used by playlist schedulesplan"pro" | "network" | "venue"resourcesobjectresources.storageGBnumberresources.storageUsedBytesintegerresources.displaysLinkedintegerresources.displaysLimitintegerresources.usersLinkedintegerresources.usersLimitinteger | nullkeyobjectkey.keyIdstringkey.namestringkey.scopes"read" | "write" | "push"[]
Folder
folderIdstringnamestringparentFolderIdstring | nullisSharedbooleanisOwnerboolean
Track
idstringtitlestringartiststringmoodstringdurationSecintegers3Keystringuse as backgroundMusicS3Key
PushResult
successbooleanpushedDisplaysstring[]failedDisplaysobject[]failedDisplays[].displayIdstringfailedDisplays[].reasonstring
SlideFields
Content for a template. Which fields apply depends on the template (GET /slides/templates).
titlestringsubtitlestringkickerstringsmall label above the titlebodystringparagraph; newlines allowedbadgestringtext inside the round badge (promo)itemsobject[]items[].namestring · requireditems[].pricestringitems[].descstringitems[].tagstringlogostringlibrary key (s3Key) of your logophotostringlibrary key of a photoaccentstring#RRGGBBfont"Inter" | "Playfair Display" | "Bebas Neue" | "Lora" | "Montserrat"namestringfile title in the library
SlideRequest
Either template + fields, or a complete Studio doc.
template"menu-board" | "price-list" | "promo" | "announcement" | "hours" | "welcome" | "event" | "portrait-menu"default "announcement"fieldsSlideFieldsdocobjecta complete StudioDoc (as returned by /slides/propose)namestringplaylistIdstringappend the slide to this playlistscreenIdsstring[]push the slide to these screens (needs the push scope)
SlideResult
s3Keystringthe library key — use it in playlists and pushestitlestringfolderIdstringsizeobjectsize.wintegersize.hintegerbytesintegerreadybooleantrue once the processed file is in the library (it always is within a minute)playlistobjectpushPushResult
Generated from the OpenAPI document at build time. The live copy at https://api.ztalio.com/v1/openapi.json is the source of truth and the one to feed a code generator or an assistant.
Uploading a file.
Three calls, all with the write scope: start the upload, PUT the parts straight to storage, then complete it. ztalio upload does all three.
POST /uploads
Send filename, contentType, sizeBytes and totalParts (parts of up to 100 MB), plus an optional folderId. You get an uploadId and the file’s future s3Key.
POST /uploads/{uploadId}/part-urls
Ask for presigned URLs for the part numbers you need, then HTTP PUT each part’s bytes to its URL. Keep the ETag header each PUT returns.
POST /uploads/{uploadId}/complete
Send the parts list of { partNumber, etag }. The file is optimised (videos to H.264 1080p, portrait gets blurred pillars) and appears in the library within a minute.
# or let the CLI do it ztalio upload ./menu.png --folder Promos # about a minute later ztalio media --folder Promos
A file that is not yet in GET /media is still being optimised; poll the library rather than the upload. DELETE /uploads/{uploadId} abandons an upload that will not complete.
Put a script on the TV.
Keys live in Settings. The CLI is open source. Questions, bugs and feature requests go to support.
© 2026 Cre8ive Innovations LLC · API v1.0.0
