REST API
Quick start
List your screens, then tell one of them to reload its content:
export DS_KEY="ds_live_…"
BASE="https://app.digitalsign.co/api/v1"
# 1. Find the screen
curl -s "$BASE/screens?q=lobby" \
-H "Authorization: Bearer $DS_KEY"
# 2. Send it a refresh command (use the id from step 1)
curl -s -X POST "$BASE/screens/scr_4k8m2q7v9x1c3b5n6p0w/commands" \
-H "Authorization: Bearer $DS_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"refresh"}'The second call returns the queued command:
{ "id": "cmd_7t2r9w4y6u1i3o5p8a0s", "type": "refresh", "status": "pending" }Base URL
https://app.digitalsign.co/api/v1Requests and responses are JSON. Send Content-Type: application/json with a body on POST requests. The API accepts GET, POST and DELETE. Responses are sent with Cache-Control: no-store.
Authentication
Every request needs an API key in the Authorization header:
Authorization: Bearer ds_live_…- Create keys in the dashboard under Settings → Alerts & API, in the API keys section. Owners and admins (anyone with the Manage organization settings permission) can create and revoke keys.
- The full key is shown once, when you create it. DigitalSign stores only a hash, so copy it somewhere safe. The list shows the first characters, who created it and when it was last used.
- A key acts with its creator's permissions at the time of each request. If the creator is demoted, limited to certain locations or removed from the organization, the key changes or loses access with them.
- Revoking a key takes effect on the next request.
- The API sends
Access-Control-Allow-Origin: *, so browsers can call it, but a key in browser code is visible to anyone who loads the page. Call the API from a server.
Permissions and plans
The API, like the dashboard, checks the key creator's role. Read endpoints return what that person can see: someone limited to certain locations only sees screens and locations there. Endpoints that change things need the permission listed with them below. By default:
| Permission | Roles that have it |
|---|---|
| Create designs, Edit designs | Owner, Admin, Designer, Publisher, Location manager |
| Publish to screens, Manage schedules | Owner, Admin, Publisher, Location manager (at their locations) |
| Manage screens | Owner, Admin, Location manager (at their locations) |
| View analytics | Owner, Admin, Publisher, Location manager, Viewer |
| Manage organization settings | Owner, Admin |
If the organization is not on the Pro plan or higher, every request returns 402 upgrade_required. The audit log endpoint also needs the Enterprise plan.
Errors
Errors use one shape:
{
"error": {
"code": "invalid",
"message": "type must be one of: refresh, restart_player, reboot_device, screenshot, clear_cache, set_volume, display_off, display_on, update_software, send_logs, identify"
}
}The message is written for people and may change; branch on the HTTP status and code.
| Status | code | When |
|---|---|---|
| 400 | invalid | A missing or invalid field, a body that is not a JSON object, a body over 256,000 characters, or a malformed %-escape in the path. |
| 401 | unauthorized | No Authorization: Bearer header, or the key is malformed, unknown or revoked. |
| 402 | upgrade_required | The organization's plan does not include the API (Pro) or the feature the endpoint needs (Enterprise for the audit log). |
| 403 | forbidden | The key's creator lacks the permission, or the target is outside their locations. |
| 404 | not_found | No such route, or the screen, playlist, schedule or template does not exist in your organization. |
| 409 | conflict | The request conflicts with the current state of the resource. |
| 429 | rate_limited | More than 120 requests in a minute with one key. |
| 500 | internal | An unexpected error on our side. |
Rate limit
Each key can make up to 120 requests per minute, counted in fixed one-minute windows. Request 121 in a window returns 429 with code rate_limited and these headers:
Retry-After: 60
X-RateLimit-Limit: 120Wait the number of seconds in Retry-After, then retry. Requests with a malformed key are rejected with 401 and do not count toward any key's limit.
Conventions
- IDs are strings with a type prefix:
scr_screens,pls_playlists,scn_designs,ast_media,sch_schedules,loc_locations,grp_groups,dsr_data sources,cmd_commands. - Times are ISO 8601 strings in UTC, for example
2026-10-02T14:03:11.512Z. Fields with no value arenull. - List endpoints return
{ "data": [ … ] }. Only the audit log is paginated; other lists return everything (designs are capped at 200 and media at 500, most recently changed first).
Endpoint summary
| Method | Path | Purpose |
|---|---|---|
| GET | /me | Organization and key owner |
| GET | /screens | List screens |
| GET | /screens/{id} | Screen status and what is playing |
| POST | /screens/{id}/commands | Send a remote command |
| GET | /screens/{id}/commands | Recent commands and their status |
| GET | /screens/{id}/commands/{commandId} | One command's status (and screenshot link) |
| POST | /screens/pair | Pair a display to a new screen |
| GET | /playlists | List playlists |
| GET | /playlists/{id} | Playlist with items |
| POST | /playlists | Create a playlist |
| POST | /playlists/{id}/items | Add items to a playlist |
| GET | /designs | List designs |
| POST | /designs | Create a design |
| GET | /templates | List templates |
| GET | /assets | List media |
| GET | /schedules | List schedules |
| POST | /schedules | Create a schedule |
| DELETE | /schedules/{id} | Delete a schedule |
| POST | /overrides | Show a playlist now, for a set time |
| GET | /locations | List locations and regions |
| GET | /groups | List screen groups |
| POST | /groups | Create a screen group |
| GET | /data-sources | List data connections |
| GET | /playback | Proof of play records |
| GET | /campaigns | Plays and hours per playlist |
| GET | /audit-log | Audit log (Enterprise) |
Account
GET /me
Returns the organization the key belongs to and the email of the person who created it. Useful to check a key works.
{
"organization": {
"id": "org_2b6n8m1q4w7e9r3t5y0u",
"name": "Acme Coffee",
"timezone": "America/Chicago",
"plan": "pro"
},
"user": { "email": "[email protected]" }
}plan is one of free, standard, ai, pro or enterprise.
Screens
GET /screens
Lists the screens the key can see, sorted by name.
| Query | Type | Description |
|---|---|---|
q | string, optional | Case-insensitive match on the screen name or its location name. |
tag | string, optional | Only screens with this exact tag. |
{
"data": [
{
"id": "scr_4k8m2q7v9x1c3b5n6p0w",
"name": "Lobby",
"status": "online",
"location": { "id": "loc_9d3f5h7j1k2l4z6x8c0v", "name": "Downtown" },
"tags": ["lobby", "menu"],
"orientation": "landscape",
"width": null,
"height": null,
"lastHeartbeatAt": "2026-10-02T14:03:11.512Z",
"defaultPlaylistId": "pls_3e5r7t9y1u2i4o6p8a0s"
}
]
}status:online(the display checked in within the last 90 seconds),offline, orunpaired(no display paired yet).orientation:landscape,portrait,landscape_flippedorportrait_flipped.widthandheight: a canvas size set on the screen, ornullwhen none is set.defaultPlaylistId: what the screen plays when nothing is scheduled, ornull.
GET /screens/{id}
Returns a screen's status, what it should be playing now, and the next scheduled change, worked out in the screen's time zone.
{
"id": "scr_4k8m2q7v9x1c3b5n6p0w",
"name": "Lobby",
"status": "online",
"timezone": "America/Chicago",
"nowPlaying": {
"scheduleId": "sch_1q3w5e7r9t2y4u6i8o0p",
"scheduleName": "Lunch menu",
"playlistId": "pls_6h8j0k2l4z1x3c5v7b9n",
"playlistName": "Lunch"
},
"next": { "at": "2026-10-02T19:00:00.000Z", "playlistName": "Dinner" }
}When no schedule applies and the screen has a default playlist, nowPlaying is { "playlistId", "playlistName", "default": true }. When nothing applies at all it is null. next is null when no change is coming up.
POST /screens/{id}/commands
Queues a remote command for the screen's display. Needs the Manage screens permission at the screen's location, and a display paired to the screen.
| Field | Type | Description |
|---|---|---|
type | string, required | One of the command types below. |
payload | object, optional | Must be a JSON object if sent. Only used by set_volume: { "level": 0-100, "muted": true|false }. |
type | Does |
|---|---|
refresh | Refresh content |
restart_player | Restart the player app |
reboot_device | Reboot the device |
screenshot | Take a screenshot |
clear_cache | Clear cached content |
set_volume | Set volume (needs payload.level) |
display_off, display_on | Turn the display off or on |
update_software | Update the player software |
send_logs | Upload player logs |
identify | Show the screen's name on the display, to find it on site |
curl -s -X POST "$BASE/screens/scr_4k8m2q7v9x1c3b5n6p0w/commands" \
-H "Authorization: Bearer $DS_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"set_volume","payload":{"level":40}}'
{ "id": "cmd_7t2r9w4y6u1i3o5p8a0s", "type": "set_volume", "status": "pending" }The display picks the command up on its next check-in. A command that is not picked up within 10 minutes expires. Sending the same command type again while one is still pending replaces the older one. Not every device supports every command; check the result with the endpoints below.
GET /screens/{id}/commands and GET /screens/{id}/commands/{commandId}
The screen's recent commands (newest first, ?limit= up to 100, default 20), or one command. status moves from pending to sent when the display picks it up, then to done, failed, unsupported or expired. A finished screenshot command includes screenshotUrl, an image link valid for at least an hour.
{
"id": "cmd_7t2r9w4y6u1i3o5p8a0s",
"type": "screenshot",
"status": "done",
"message": null,
"createdAt": "2026-10-02T14:03:11.000Z",
"sentAt": "2026-10-02T14:03:15.000Z",
"completedAt": "2026-10-02T14:03:17.000Z",
"createdBy": "[email protected]",
"screenshotUrl": "https://…"
}POST /screens/pair
Creates a screen and pairs it with the display showing a pairing code. Needs the Manage screens permission (at locationId, if given).
| Field | Type | Description |
|---|---|---|
code | string, required | The 7-character code on the display, with or without the dash, any case. Example: 8FJ2-KP9. |
name | string, optional | Up to 120 characters. Defaults to Screen. |
locationId | string, optional | A location id from GET /locations. |
{ "id": "scr_0z2x4c6v8b1n3m5q7w9e", "name": "Drive-thru" }Returns 400 if no display is currently showing that code.
Playlists
GET /playlists
Lists playlists, most recently updated first. items is the number of items.
{
"data": [
{
"id": "pls_6h8j0k2l4z1x3c5v7b9n",
"name": "Lunch",
"items": 6,
"shuffle": false,
"updatedAt": "2026-10-01T16:42:05.118Z"
}
]
}GET /playlists/{id}
Returns a playlist and its items in play order.
{
"id": "pls_6h8j0k2l4z1x3c5v7b9n",
"name": "Lunch",
"shuffle": false,
"loop": true,
"totalMs": 40000,
"items": [
{
"id": "pli_5g7h9j1k3l2z4x6c8v0b",
"kind": "scene",
"title": "Soup of the day",
"enabled": true,
"durationMs": 10000,
"designId": "scn_8u0i2o4p6a1s3d5f7g9h",
"assetId": null,
"playlistId": null
},
{
"id": "pli_2w4e6r8t0y1u3i5o7p9a",
"kind": "asset",
"title": "promo.mp4",
"enabled": true,
"durationMs": 30000,
"designId": null,
"assetId": "ast_3s5d7f9g1h2j4k6l8z0x",
"playlistId": null
}
]
}kind:scene(a design),asset(media),playlist(a nested playlist) orurl(a web page).durationMs: how long the item shows: its own duration if set, else the video's length for videos, else the playlist default.nullfor nested playlists and videos of unknown length.totalMs: the sum ofdurationMsfor enabled items.
POST /playlists
Creates an empty playlist. Needs the Create designs permission.
| Field | Type | Description |
|---|---|---|
name | string, optional | 1 to 120 characters. Defaults to Untitled playlist. |
New playlists use the default settings: 10-second items, fade transition, loop on, shuffle off. Change these in the dashboard.
{ "id": "pls_1a3s5d7f9g2h4j6k8l0z", "name": "Fall promos" }POST /playlists/{id}/items
Appends 1 to 100 items to the end of a playlist. A playlist holds up to 500 items. Needs the Edit designs permission. If any item is invalid (an unknown kind, or a missing id), the request returns 400 and nothing is added.
| Item | Fields |
|---|---|
| Design | { "kind": "scene", "designId": "scn_…" }. sceneId is accepted as another name for designId. |
| Media | { "kind": "asset", "assetId": "ast_…" } |
| Nested playlist | { "kind": "playlist", "playlistId": "pls_…" }. Refused if it would make a loop. |
| Web page | { "kind": "url", "url": "https://…", "title": "…" }. The URL must start with https://; title is optional (up to 120 characters, defaults to the host name). |
curl -s -X POST "$BASE/playlists/pls_1a3s5d7f9g2h4j6k8l0z/items" \
-H "Authorization: Bearer $DS_KEY" \
-H "Content-Type: application/json" \
-d '{"items":[{"kind":"scene","designId":"scn_8u0i2o4p6a1s3d5f7g9h"},{"kind":"url","url":"https://status.acme.example"}]}'
{ "added": 2 }Designs and templates
GET /designs
Lists up to 200 designs, most recently updated first.
| Query | Type | Description |
|---|---|---|
q | string, optional | Case-insensitive match on the design name. |
{
"data": [
{
"id": "scn_8u0i2o4p6a1s3d5f7g9h",
"name": "Soup of the day",
"width": 1920,
"height": 1080,
"updatedAt": "2026-09-30T11:20:44.903Z"
}
]
}POST /designs
Creates a design from a template, or from a design document. Needs the Create designs permission.
| Field | Type | Description |
|---|---|---|
name | string, optional | Up to 120 characters. Defaults to the template name, or Untitled design. |
templateId | string, optional | An id from GET /templates. When set, document is ignored. |
document | object, optional | A design document in the editor's JSON format. It is validated, and any media it references must be in your library. Without templateId or document you get a blank 1920 × 1080 design. |
curl -s -X POST "$BASE/designs" \
-H "Authorization: Bearer $DS_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Store closed","templateId":"builtin:announcement-alert"}'
{ "id": "scn_2p4o6i8u0y1t3r5e7w9q", "name": "Store closed" }GET /templates
Lists your organization's saved templates (newest first), then the built-in templates. Built-in ids start with builtin:.
| Query | Type | Description |
|---|---|---|
category | string, optional | One of menu, promo, welcome, announcement, event, corporate, retail, fitness, hospitality, healthcare, hiring, school, workplace, services, worship. |
q | string, optional | Case-insensitive match on the name (and the description, for built-ins). |
{
"data": [
{
"id": "builtin:announcement-alert",
"name": "Important notice",
"category": "announcement",
"description": "High-contrast notice for closures, weather and changes.",
"builtIn": true,
"width": 1920,
"height": 1080
}
]
}Media
GET /assets
Lists up to 500 items from the media library, newest first. Failed uploads are left out.
| Query | Type | Description |
|---|---|---|
q | string, optional | Case-insensitive match on the file name or any of its tags. |
{
"data": [
{
"id": "ast_3s5d7f9g1h2j4k6l8z0x",
"name": "promo.mp4",
"kind": "video",
"mime": "video/mp4",
"bytes": 18432011,
"status": "ready",
"createdAt": "2026-09-28T09:12:37.440Z"
}
]
}kind is image, video or web. status is uploading, processing or ready. mime and bytes can be null (for example, for web pages).
Schedules and overrides
GET /schedules
Lists schedules that have not ended, highest priority first. Blocks that belong to a daypart program are not included.
{
"data": [
{
"id": "sch_1q3w5e7r9t2y4u6i8o0p",
"name": "Lunch menu",
"kind": "standard",
"priority": 10,
"playlistId": "pls_6h8j0k2l4z1x3c5v7b9n",
"enabled": true,
"startsAt": null,
"endsAt": null,
"startDate": "2026-10-01",
"endDate": null,
"condition": null
}
]
}condition is the rule set on the schedule in the dashboard, or null.
POST /schedules
Creates a schedule and publishes it. Needs the Publish to screens permission, plus Manage schedules over every target: targeting all, a group or a tag needs organization-wide rights; location managers can target their own locations and the screens in them.
| Field | Type | Description |
|---|---|---|
name | string, required | 1 to 120 characters. |
playlistId | string, required | The playlist to play. |
targets | array, required | 1 to 200 targets (see below). |
rules | array, optional | Up to 50 recurring time windows (see below). You need at least one rule or a startsAt. |
startsAt, endsAt | ISO time, optional | Exact start and end. endsAt must be after startsAt. |
startDate, endDate | YYYY-MM-DD, optional | Date range the rules apply in. endDate must not be before startDate. |
exceptDates | array of YYYY-MM-DD, optional | Up to 366 dates to skip. |
kind | string, optional | standard (default), daypart, override, emergency, takeover or holiday. |
priority | integer, optional | 1 to 100, default 10. Higher wins when schedules overlap. |
enabled | boolean, optional | Default true. |
color | string, optional | Calendar color as #rrggbb. |
Targets. Each target is one of:
{ "kind": "all" }: every screen{ "kind": "location", "refId": "loc_…" }: a location or region, including everything under it{ "kind": "group", "refId": "grp_…" }: a screen group{ "kind": "screen", "refId": "scr_…" }: one screen{ "kind": "tag", "tag": "drive-thru" }: screens with a tag (up to 40 characters)
Rules. Each rule is { "recurrence": …, "start": "HH:MM", "end": "HH:MM" }. Times are 24-hour (00:00 to 24:00) in each screen's local time zone, and start and end must differ. recurrence is one of:
{ "type": "weekly", "days": [1, 2, 3, 4, 5] }: days of the week, 0 = Sunday to 6 = Saturday{ "type": "monthly", "weekday": 2, "nth": 1 }: the nth weekday of the month;nthis 1 to 5, or -1 for the last{ "type": "dates", "dates": ["2026-12-24", "2026-12-31"] }: specific dates (up to 366)
curl -s -X POST "$BASE/schedules" \
-H "Authorization: Bearer $DS_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Weekday lunch",
"playlistId": "pls_6h8j0k2l4z1x3c5v7b9n",
"rules": [{ "recurrence": { "type": "weekly", "days": [1,2,3,4,5] }, "start": "11:00", "end": "14:00" }],
"targets": [{ "kind": "tag", "tag": "menu" }]
}'
{ "id": "sch_9o7i5u3y1t2r4e6w8q0a" }DELETE /schedules/{id}
Deletes a schedule. Needs the same rights as creating it, over every place it currently targets.
{ "deleted": true }POST /overrides
Plays a playlist right away for a set time, above normal schedules. Creates a schedule of kind override with priority 70, starting now. Same permissions as POST /schedules.
| Field | Type | Description |
|---|---|---|
playlistId | string, required | The playlist to show. |
minutes | number, optional | Default 60. Kept between 5 and 10,080 (7 days). |
targets | array, optional | Same format as schedules. Defaults to [{ "kind": "all" }]. |
name | string, optional | Defaults to a name like Override (1 h). |
{ "id": "sch_4r6t8y0u2i1o3p5a7s9d" }The id is a schedule id. To end the override early, delete it with DELETE /schedules/{id}.
Locations and groups
GET /locations
Lists regions and locations as a flat list, each region followed by what is under it. People limited to certain locations see only those and what is under them.
{
"data": [
{
"id": "loc_7b9n1m3q5w2e4r6t8y0u",
"name": "Texas",
"kind": "region",
"parentId": null,
"timezone": "America/Chicago",
"city": null
},
{
"id": "loc_9d3f5h7j1k2l4z6x8c0v",
"name": "Downtown",
"kind": "location",
"parentId": "loc_7b9n1m3q5w2e4r6t8y0u",
"timezone": "America/Chicago",
"city": "Austin"
}
]
}timezone is the effective time zone: the location's own, else its region's, else the organization's.
GET /groups
Lists screen groups, sorted by name.
{
"data": [
{
"id": "grp_5v7b9n1m3q2w4e6r8t0y",
"name": "Drive-thru boards",
"kind": "smart",
"description": null
}
]
}kind is manual (screens added by hand) or smart (screens matched by a rule).
POST /groups
Creates a screen group. Needs the Manage screens permission.
| Field | Type | Description |
|---|---|---|
name | string, required | 1 to 80 characters. |
description | string, optional | Up to 300 characters. |
screenIds | array of strings, optional | Makes a manual group with these screens (up to 5,000). Every id must be a screen in your organization. |
rule | object, optional | Makes a smart group (see below). Send rule or screenIds, not both. With neither, you get an empty manual group. |
A rule has at least one of:
locationIds: array of location ids (up to 200). Matches screens at any of them, including sub-locations.tags: array of tags (up to 30). Matches screens with any of them.orientation:landscapeorportrait(flipped orientations match too).
A screen joins the group when it matches every part of the rule that is set. New screens join automatically.
curl -s -X POST "$BASE/groups" \
-H "Authorization: Bearer $DS_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Portrait menus","rule":{"tags":["menu"],"orientation":"portrait"}}'
{ "id": "grp_8q0w2e4r6t1y3u5i7o9p" }A manual group from a list of screens:
curl -s -X POST "$BASE/groups" \
-H "Authorization: Bearer $DS_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Front counter","screenIds":["scr_4k8m2q7v9x1c3b5n6p0w","scr_0z2x4c6v8b1n3m5q7w9e"]}'
{ "id": "grp_1m3n5b7v9c2x4z6l8k0j" }Data sources
GET /data-sources
Lists data connections (the feeds that power data-driven designs), sorted by name. Connection settings and secrets are not returned.
{
"data": [
{
"id": "dsr_6y8u0i2o4p1a3s5d7f9g",
"name": "Menu prices",
"kind": "csv",
"rows": 42,
"fields": ["item", "price", "available"],
"lastFetchedAt": "2026-10-02T13:55:00.000Z",
"lastError": null
}
]
}kind is rest, rss, csv, webhook, square or shopify. rows and fields describe the most recent fetch.
Proof of play and campaigns
Both endpoints need the View analytics permission. Ranges are rolling: days=7 means the 7 × 24 hours before now.
GET /playback
Returns proof-of-play records: one row per item shown, oldest first.
| Query | Type | Description |
|---|---|---|
days | integer, optional | 1 to 365. Default 7. |
playlist | string, optional | Only plays from this playlist id. |
{
"data": [
{
"started_at": "2026-10-02T12:00:04.210Z",
"ended_at": "2026-10-02T12:00:14.215Z",
"seconds": "10",
"completed": "yes",
"screen": "Lobby",
"location": "Downtown",
"playlist": "Lunch",
"content": "Soup of the day"
}
],
"truncated": false
}- All values are strings.
completedisyesorno;contentis the design or media name. - At most 10,000 rows are returned. If the range has more, you get the most recent 10,000 plays (still oldest first) and
truncated: true. Ask for fewerdaysto see the whole range.
GET /campaigns
Plays, hours on screen and reach for each playlist in the range, most plays first.
| Query | Type | Description |
|---|---|---|
days | integer, optional | 1 to 365. Default 30. |
{
"data": [
{
"playlistId": "pls_6h8j0k2l4z1x3c5v7b9n",
"name": "Lunch",
"plays": 18240,
"hours": 50.7,
"screens": 12,
"locations": 4,
"first": "2026-09-02T15:00:02.004Z",
"last": "2026-10-02T14:02:51.330Z"
}
]
}hours is rounded to one decimal. first and last are the first and last plays in the range. A playlist that has since been deleted shows as Deleted playlist.
Audit log
GET /audit-log
Returns audit log entries, newest first. Needs the Enterprise plan and the Manage organization settings permission.
| Query | Type | Description |
|---|---|---|
limit | integer, optional | Entries per page. Default 100, maximum 1,000. |
since | ISO time, optional | Only entries at or after this time. |
category | string, optional | Only actions starting with this prefix, for example schedule matches schedule.created and schedule.deleted. |
before | string, optional | The next value from the previous page. |
{
"data": [
{
"id": "aud_0p2o4i6u8y1t3r5e7w9q",
"at": "2026-10-02T13:41:09.871Z",
"actor": "[email protected]",
"action": "schedule.created",
"target": "sch_9o7i5u3y1t2r4e6w8q0a",
"details": { "name": "Weekday lunch", "kind": "standard" }
}
],
"next": "2026-10-02T13:41:09.871Z|aud_0p2o4i6u8y1t3r5e7w9q"
}Keep requesting with before=<next> (URL-encoded) until next is null.
See also
Webhooks: get a signed HTTP request when screens go offline, schedules publish, start or end, or content plays.