API Reference · v1

Digital Signage API

A REST API for digital signage: create a workspace, upload or generate slides, register a TV or display, assign a schedule, and read back a live HLS stream link. It is the same API behind the InfoSlides CLI and MCP server, so an AI agent can take an account from empty to a running screen without a human touching the dashboard.

Walkthrough

Going from an empty account to a live screen using the CLI: creating a workspace, uploading a deck, registering a display, and reading back the stream link, entirely from a terminal.

Base URL

https://infoslides.app

Every path below is relative to this host. The machine-readable description of the same surface lives at /v1/openapi.json and needs no credential to fetch.

Authentication

Send a workspace API key as a bearer token on every request:

Authorization: Bearer isk_admin_…
Key formatScope
isk_admin_ Full workspace-admin credential. Reaches every authenticated endpoint the workspace's plan allows.
isk_dp_ Push-only data-provider key. A key bound to a Push source (the kind createPushKey returns) may only call POST /v1/sources/{id}/data for that source, the recommended route; a key bound to slides may only call POST /v1/slides/{id}/source for those slides. Every other route is rejected with KeyScopeViolation before routing considers it, so a leaked push key can never read or modify anything else.

A tenant session token obtained through the CLI login flow (infoslides login) works anywhere an admin key does.

POST /v1/tenants is the one anonymous endpoint. It provisions a brand-new workspace on the permanent free plan (1 screen, 4 slideshows, 2 users, 200 MB, with no credit card or expiry) and returns its admin API key exactly once. The key is never recoverable afterwards. Nothing else under /v1 is reachable without a credential.

Response envelopes

Every JSON response wraps its payload in data, with optional warnings:

{
  "data": { "deviceId": "…", "name": "Lobby screen" },
  "warnings": [ { "code": "AspectMismatch", "message": "…" } ]
}

Errors use a parallel shape, with details.fields populated on validation failures:

{
  "error": {
    "code": "ValidationFailed",
    "message": "Unknown signup source 'banana'.",
    "details": { "fields": { "source": ["Must be one of: Unknown, Web, Api, Cli, Mcp."] } }
  }
}

Three responses are deliberately not enveloped: GET /v1/slides/{id}/preview.png and POST /v1/templates/preview return raw image/png bytes, and the CLI auth legs return HTTP redirects.

Idempotency

Creating endpoints accept an Idempotency-Key request header (up to 128 characters). The first successful response for a key is stored for 24 hours; a repeat request with the same key returns that stored response verbatim, with Idempotent-Replay: true, without re-executing the action. This matters for agents, which retry more readily than humans do.

Endpoints that honour it are marked idempotent in the tables below.

Endpoints

Workspaces

Provision a workspace and read back its plan and quota.

POST /v1/tenantsidempotentno auth

Called with no credential, provisions a brand-new workspace on the free plan and returns its admin API key exactly once. ownerEmail must be an address you can open right now, never an invented one or a placeholder; your own is recommended, so you can verify it at once and then add the person with POST /v1/team/invitations. Adding a screen fails with EmailNotVerified until the address is verified: open the verification email, sign in once with Google or Microsoft using that address, or set a password. Accepts an optional source field (web, api, cli, mcp, muse) recording how the signup originated, and optional timeZone and locale (same values as PATCH /v1/tenant); otherwise the workspace starts on Atlantic/Reykjavik and is-IS. An agent should also send agent with its own product name (e.g. "Muse", "Instinct", at most 40 characters, plain text) — kept separate from source, which stays the fixed channel. Optionally send ownerName (the person's first name, at most 100 characters) so emails greet them by name instead of the workspace name; ask for it only when that is free, never invent one. Called **with** an admin API key instead, creates an additional workspace attached to that key owner's own account (ownerEmail must match the caller's own address) and returns that new workspace's own admin key — the recommended way to manage several workspaces under one identity is POST /v1/team/invitations to add teammates instead, one API key per workspace.

PATCH /v1/tenantworkspace admin

Sets the workspace time zone (timeZone, an IANA id such as Europe/London) and/or locale (locale: is-IS, en-US, en-GB, da-DK, nb-NO, sv-SE, de-DE, fr-FR, es-ES, fi-FI, fo-FO, pt-BR). The time zone decides when schedules switch, what screen clocks show and when time-based slide conditions apply; set it first for a new workspace. The locale sets number and date formats in presentations; changing it re-renders them. display sets the workspace defaults every screen starts from: {"ticker": {"enabled", "height" (20-200), "backgroundColor", "textColor" (hex such as #1a1a1a), "scrollSpeed" (10-500), "fontSize" (10-80), "maxHeadlines" (1-100)}, "clock": {"enabled", "position" (top-left, top-right, bottom-left, bottom-right), "showDate", "backgroundColor", "textColor"}, "defaultSlideDurationSeconds" (1-300)}. Send only what should change; null puts a field back to the default. Ticker and clock colours you leave alone follow the workspace's confirmed brand guide, so do not set colours unless the person asks. Out of range answers ValidationFailed naming the field (for example display.ticker.scrollSpeed). A ticker's look is for the whole workspace; a slideshow only switches its ticker on or off. Admin key only.

GET /v1/tenantany credential

"Whoami": the caller's workspace identity, plan, device-quota usage, and the scope of the credential used. display shows the workspace's own ticker, clock and slide-duration settings; a null field has no override and follows the brand guide (colours) or the system default.

GET /v1/platform/versionany credential

Which InfoSlides release is running. version is the release (for example v1.10.1; null for a build without a release tag), build identifies the exact build and changes with every deploy, deployedAt is when the service last started, and releaseNotes is a list of { title, slug, url }, the published release notes of this version (a release can have several), newest first. Call it now and then and remember version. When it has changed, tell the person an update is live and read the releaseNotes pages; an empty list means they are not written yet, so check again later. Works with any valid key.

GET /v1/platform/noticesany credential

The notices a platform admin has posted that are still active, newest first, as a list (empty when there are none). Each has kind (SystemUpdate for an update or maintenance window, NewFeature for news), title, message (plain text or Markdown), postedAt, startsAt (when the update begins; null when the notice names no time) and expiresAt. Call it now and then. When a SystemUpdate notice appears, tell the person an update is coming and when, using startsAt. Works with any valid key.

GET /v1/articlesany credential

Searches the published help articles, blog posts and release notes. Use it when you are stuck (search before telling the person something cannot be done), to find news for the person (since with a date) or to read the notes of one release (releaseVersion, such as v1.10.2, with or without the v). Filters: q (free text, best match first; without it newest first), type (help, blog or release-note), since (published on or after this date), releaseVersion, page and pageSize (1 to 100, default 25). Each item has type, title, slug, url (the public page), publishedAt, releaseVersion (release notes only, otherwise null), appRoute (the in-app page the article is about, to send the person there) and a short plain-text excerpt. Read a whole article with GET /v1/articles/{type}/{slug}. Works with any valid key.

GET /v1/articles/{type}/{slug}any credential

Reads one published article as Markdown: the same fields as a search item without excerpt, plus markdown (headings, lists, links and tables kept; scripts and styles dropped; links absolute). type is help, blog or release-note; slug comes from a search item. An unknown, hidden or unpublished article answers 404 NotFound; an unknown type answers 400 ValidationFailed. Works with any valid key.

GET /v1/workspace/healthcontent or device manager

"How are my screens?" in one call: summary, one sentence to read out ("11 of 12 screens are online; Lobby has been offline since 09:12.", times in the workspace timeZone), every screen with online and lastSeenAt (offline first), usage against the plan (storageBytes, devices, slideshows, each used and limit), and problems, most urgent first: screens offline, renders that failed in the last 24 hours, slideshows no screen plays, why a screen would show nothing (NotAssigned, NotRendered, WrongPlaybackMode, at most one), an expired plan, and plan limits or storage close to full. Each problem has a code, a message to read out, the deviceId or slideshowId it is about, and a ready-made fix (e.g. the offline screen's diagnosis). An empty problems list means all is well. Cheap enough to call every morning.

GET /v1/workspace/offline-alertsdevice manager or above

The "screen went offline" alert emails sent to workspace admins: the workspace's quietHours (a daily window, {start, end} as HH:mm in the workspace timeZone, when alerts wait until it ends; end before start spans midnight; null for none), and each screen's enabled, its own quietHours (null uses the workspace's) and followsSchedule. A screen with timed schedule entries (followsSchedule: true) only alerts inside them, and no quiet hours apply to it. At most one alert per screen per day is sent either way.

PUT /v1/workspace/offline-alertsidempotentworkspace admin

Sets the workspace's quiet hours for offline alert emails: {"quietHours": {"start": "22:00", "end": "07:00"}} for "don't email me about screens at night", {"quietHours": null} to alert at any hour. Screens with their own quiet hours keep them. Returns the same shape as GET. Workspace admins only.

PUT /v1/devices/{id}/offline-alertsidempotentdevice manager or above

Sets one screen's offline alert emails: enabled (false stops them for this screen, e.g. a TV that is switched off on purpose) and quietHours (its own window, {start, end} as HH:mm; null or omitted uses the workspace's). Send both every time: this replaces the screen's settings. Returns the screen's settings.

POST /v1/auth/resend-verificationany credential

Resends the workspace owner's verification email. Idempotent and silent — always reports success.

CLI authentication

Internal: browser-based OAuth login for the InfoSlides CLI, using PKCE and a loopback redirect. Agents authenticate with an API key instead and never call these.

GET /v1/auth/cli/startno auth

Browser entry point. Validates the PKCE and loopback parameters, then redirects into the OAuth flow for the requested provider (google, microsoft, github).

GET /v1/auth/cli/completeno auth

Completion leg after external sign-in. Issues a single-use authorization code and redirects back to the CLI's loopback listener.

POST /v1/auth/cli/exchangeno auth

Exchanges the authorization code plus its PKCE verifier for a session token.

Slideshows

The unit of content: a deck of slides that renders to one HLS stream.

POST /v1/slideshowsidempotentcontent manager or above

Creates a slideshow, optionally seeded with an ordered list of slides, each a mediaUrl (an image or video) or a templateId, with an optional durationSeconds: {"title": "Lunch menu", "resolution": {"width": 1920, "height": 1080}, "slides": [{"mediaUrl": "https://example.com/menu.jpg", "durationSeconds": 10}]}. Only title is required; add slides later with POST /v1/slideshows/{id}/slides. Returns the new slideshow (201); it plays once renderStatus is Completed.

POST /v1/slideshows/pptxidempotentcontent manager or above

Uploads a .pptx or .pdf file (multipart/form-data: a file part up to 100 MB, optional title field; the file name is used otherwise) and creates a slideshow from it, one slide per page, shown as it is. Returns the new slideshow (201) and queues its render: it plays once renderStatus is Completed, so poll GET /v1/slideshows/{id} at most every 5 seconds. To have AI Studio write new slides from a document instead, use POST /v1/slideshows/{id}/slides/ai with handler: document.

POST /v1/slideshows/pdfidempotentcontent manager or above

Identical to POST /v1/slideshows/pptx: accepts either .pptx or .pdf (multipart/form-data, a file part up to 100 MB, optional title). Kept as a separate, self-describing path for callers uploading a PDF export. A PDF skips the PowerPoint conversion and renders faster.

POST /v1/slideshows/import/googleidempotentcontent manager or above

Makes a slideshow from a Google Slides presentation: {"fileId": "..." or "url": "https://docs.google.com/presentation/d/.../edit", "accessToken": "...", "title": "..."}. InfoSlides keeps no Google connection for the workspace, so you send a short-lived Google access token with Drive read access to that file (the person's own, from a Google sign-in you run with the drive.readonly scope); it is used for this one call and never stored. title defaults to the Google file name. The presentation is exported from Google as PDF (Google allows at most 10 MB) and imported like an uploaded PDF, so it counts against the plan's slideshow limit (ValidationFailed on fileId with the upgrade text when full). Returns 201 with the slideshow, as the uploads do. A missing token, or one Google refuses (expired, or no access to the file), answers ValidationFailed on accessToken; a file that is not a Slides presentation, is gone or is too large answers ValidationFailed on fileId. When you cannot get a token, have the person download the presentation as PDF or PowerPoint from Google Slides and send it with POST /v1/slideshows/pdf. Forbidden means Google import is switched off on this system.

GET /v1/slideshowscontent manager or above

Lists the workspace's slideshows: title, playback mode, render status, and how many screens are playing each one right now (screenCount). ?q= filters by title, case- and accent-insensitively ("q1" finds "Q1 Results 2026"). Paged with ?page= (from 1) and ?pageSize= (default 50).

GET /v1/slideshows/{id}content manager or above

Returns a slideshow in full: every slide in order (with a thumbnailUrl to see what is on it, its type, duration, hidden flag, and rules: every visibility rule covering it, each with a plain-English summary), its ticker and clock settings, renderStatus (changes reach screens once it is Completed), and the screens playing it now, each with a playerUrl that shows the result in any browser. While a render is queued or in progress, renderProgress gives percent (0 to 100, so 0.85 is under one percent, and it stays below 100 until the render completes), the current phase in English (or a queued job's place in line, e.g. "3 renders ahead of yours"), and etaSeconds when it can be estimated; it is omitted once nothing is actively rendering. Poll at most every 5 seconds.

PATCH /v1/slideshows/{id}content manager or above

Changes a slideshow; send only what should change. Turn the news ticker on or off and pick its sources (ticker; its look, colours and speed are set for the whole workspace with PATCH /v1/tenant display.ticker), show, hide, move or recolour the on-screen clock (clock), set how long each slide shows (defaultDurationSeconds), share it (shared), rename it (title), change the screen size (resolution), reorder all slides (slideOrder), or switch playbackMode (Auto, VideoStream, Html, or inherit to clear the override; leave it alone unless the person asks, new workspaces choose automatically). Only changes that affect the rendered video queue a re-render; ticker, clock and title changes reach screens on their next poll.

DELETE /v1/slideshows/{id}content manager or above

Destructive: deletes a slideshow. Its schedules are removed and any screen paired to it stops showing it, so check screens on GET /v1/slideshows/{id} and confirm with the person first. POST /v1/slideshows/{id}/undelete restores it.

POST /v1/slideshows/{id}/undeletecontent manager or above

Restores a deleted slideshow with its slides and settings. Its schedules and the screens that played it are not brought back, so assign it again. Find the id of a deleted slideshow with GET /v1/audit?action=SlideshowDeleted (entityId; details.SlideshowDisplayName names it). Deleted slideshows are removed for good after the retention period (90 days by default). Answers ValidationFailed when the slideshow is not deleted or the plan's slideshow limit is full, NotFound for an unknown id. Returns the slideshow.

PUT /v1/slideshows/{id}/filecontent manager or above

Destructive: replaces the slideshow's PowerPoint or PDF with a new one (multipart/form-data, a single file part, up to 100 MB). Media slides, live data slides and slideshow settings are kept. Page settings (duration, hidden) and visibility rules stay with page numbers the new file still has; those for pages past its end are removed. Queues a re-render; watch renderStatus.

POST /v1/slideshows/{id}/cloneidempotentcontent manager or above

Copies a slideshow within the workspace: its file, every slide, and their settings and rules. No body. Returns the copy (201), which no screen plays until you schedule it, so it is a safe place to try changes before swapping it onto a screen.

GET /v1/slideshows/sharedcontent manager or above

Lists the workspace's shared slideshows by title, each with id, title, slideCount and importableSlideNumbers (the pages that can be copied; pages the owner deleted are left out). ?excludeId= leaves one out, usually the slideshow that will receive the slide. Pick a page here for POST /v1/slideshows/{id}/slides/import.

POST /v1/slideshows/{id}/slides/importidempotentcontent manager or above

Copies one page of a shared slideshow to the end of this slideshow: {"sourceSlideshowId": "...", "slideNumber": 3}. The copy is independent of the source. Both slideshows must come from a PowerPoint or PDF file and be the workspace's; the source must be shared and slideNumber one of its importableSlideNumbers. Returns slideshowId and the new slideCount (201) and queues a render; several imports in a row render once. NotFound for an unknown slideshow or one that is not shared, ValidationFailed for a page that cannot be imported. There is no undo: remove the page with DELETE /v1/slides/{id}.

POST /v1/slideshows/{id}/slidesidempotentcontent manager or above

Adds a photo or video as a slide, shown exactly as it is; nothing checks what the picture says. It is also the way to add a slide the layouts of POST /v1/slideshows/{id}/slides/design cannot make: design the slide as an image and add it here. Send exactly one of mediaUrl (downloaded and stored) or mediaAssetId (from POST /v1/media), plus optional durationSeconds and position (zero-based; appended when omitted): {"mediaUrl": "https://example.com/poster.jpg", "durationSeconds": 10, "position": 0}. A video plays its own length. An aspect-ratio mismatch attaches an AspectMismatch warning rather than failing. Queues a re-render. To have the picture turned into a designed slide instead, use POST /v1/slideshows/{id}/slides/ai.

POST /v1/slideshows/{id}/slides/designidempotentcontent manager or above

The first choice for a slide that is only words: adds a designed text slide from plain parameters, rendered as an image at the slideshow's size with no AI: {"heading": "Lunch moved to 1pm", "text": "Same menu, new time.", "backgroundColor": "#0F172A"}. Only heading is required; text and cta are optional. How much each carries depends on the layout and the screen (a heading up to 60 characters, text up to 200 with heading-text, a cta up to 55 with heading-text-cta, and a heading of 50 to 80 characters beside a picture, depending on the screen; the full table is in agents.md), and text in capitals fits about 30% fewer. The background is one of backgroundColor (hex), mediaAssetId (an image from POST /v1/media), mediaUrl (an image URL, downloaded and stored) or stockPhoto; with none it is the workspace accent colour (a dark navy when it has none), with text colour chosen to stay readable. stockPhoto is a photo from GET /v1/stock-images. A separate picture beside the text is imageMediaAssetId or imageUrl. branding: true adds the workspace logo and accent colour (nothing is added for a workspace without a logo). Once the workspace has a confirmed brand guide (GET /v1/brand), branding is on by default and the slide takes the guide's colours, font, logo and header band; send branding: false to opt out. A backgroundColor you name still wins. layout is heading, heading-text, heading-text-cta, heading-image-right or heading-image-left (the picture layouts need a picture, and take text and cta as optional), chosen from the fields when omitted. position (zero-based, appended when omitted) and durationSeconds (1 to 99, sized to the text when omitted) are optional. Returns the slide id, the designId the web app can edit, a previewUrl image link and the layout used. Text longer than the layout fits, a duration outside 1 to 99, a layout that does not fit the fields, or two backgrounds answer ValidationFailed naming the field (and, for text, the number that fits); nothing is clipped. The text is shown exactly as sent and is not checked for accuracy, and the slide is already in the slideshow when the answer arrives (undo removes it, and GET/PUT /v1/slides/{id}/design reopen and change it). When the layouts do not offer enough flexibility, add a finished image with POST /v1/slideshows/{id}/slides instead. Open on every plan.

GET /v1/slides/{id}/designcontent manager or above

Reads the design behind a slide made with POST /v1/slideshows/{id}/slides/design, in the same fields that call takes (design: heading, text, cta, backgroundColor or mediaAssetId or stockPhoto, imageMediaAssetId, branding, layout, durationSeconds), so you can change one and send it back with PUT /v1/slides/{id}/design. spec is the full stored design for anything those fields cannot express (the font, text colour, header band). A stock photo comes back with its photographer credit but without downloadLocation; sending it back unchanged keeps the same photo. A slide that was not made with the designer (an uploaded image, a presentation slide) answers NotFound and says it is not a designed slide.

PUT /v1/slides/{id}/designcontent manager or above

Changes a designed slide: send the whole design, the same fields as POST /v1/slideshows/{id}/slides/design without position (read it first with GET /v1/slides/{id}/design, change what you need, send it all back; a field left out is not kept). It is rendered again in place, so the slide keeps its position, hidden state and visibility rules, and durationSeconds is only changed when you send it. The same checks apply as when adding: text longer than the layout fits, a bad colour, a layout that does not fit the fields or two backgrounds answer ValidationFailed naming the field, and when the render fails the slide stays as it was. undo puts the old design and timing back. Open on every plan.

GET /v1/stock-imagescontent manager or above

Searches free stock photos (Unsplash): ?q=lake&orientation=landscape&page=1 (q required; orientation is landscape, portrait or squarish; 9 results a page). Each result has thumbUrl to show the person and photographerName, photographerProfileUrl and unsplashPhotoUrl to show beside it (the credit must be visible whenever a photo is shown), plus photoId, downloadLocation and fullUrl. Use a photo as a designed slide background by sending those fields as stockPhoto (with the credit fields) on POST /v1/slideshows/{id}/slides/design or PUT /v1/slides/{id}/design; that counts the download with Unsplash and puts the credit on the slide, and it is the only way to use a stock photo. Searching is rate limited per workspace (30 a minute; past that the answer is RateLimited, wait and retry). rateLimited: true with no results means the photo service itself is busy; try again later. Nothing is stored by searching.

POST /v1/slideshows/{id}/slides/dynamicidempotentcontent manager or above

Adds a live data slide built from a template: {"templateId": "...", "durationSeconds": 10, "position": 0, "createPushKey": true} (only templateId is required). For a template that takes pushed data (dataMode: push), a Push source is created for the slide and returned as sourceId; createPushKey: true also returns a push-only key for it (pushKey, shown once). Send data with POST /v1/sources/{sourceId}/data; the slide stays hidden until the first push. For a template fed by a fetched source, pick the source afterwards with PATCH /v1/slides/{id} (sourceId).

POST /v1/slideshows/{id}/slides/dynamic/weather-alert-timelineidempotentcontent manager or above

Adds a weather alert timeline: a set of 8 live data slides, one per 6-hour step over the next 48 hours, each showing the alerts for its step and hiding itself when there are none. {"sourceId": "...", "position": 0, "durationSeconds": 10, "hideWhenEmpty": true}. sourceId is required and must be a Weather alert source of the workspace (GET /v1/sources, adapter WeatherAlert; any other adapter answers ValidationFailed on sourceId, a source the workspace does not have is NotFound). templateId is optional: the timeline template is found for you, or send one that is a weather alert timeline template. position (zero-based, appended when omitted) is where the first slide goes, durationSeconds is per slide (10 when omitted), and hideWhenEmpty applies to the first slide only (the later ones always hide). Needs a plan with live data slides: EntitlementRequired with an upgradeUrl (or a note for a workspace admin) otherwise. Returns 201 with the 8 slides in order (id, templateId, durationSeconds, position, sourceId) and an undo token that removes them all.

POST /v1/slideshows/{id}/slides/aiidempotentcontent manager or above

AI Studio: designs new slides for this slideshow in the workspace's style. handler picks the input: prompt (describe the slide in prompt: headline, details, prices, times; the copy is written from it), photo (a picture of a poster, whiteboard, menu or flyer in mediaAssetId or mediaUrl; its text is read and laid out as a clean slide), url (a page in url; its key information is put on a slide) or document (a PDF or Word file in mediaAssetId, uploaded with POST /v1/media first; becomes a few slides of key points). Design options: branding (true features the logo and brand colour, false keeps branding subtle, omit to let the prompt decide; with a confirmed brand guide, omitting it means true, and the guide's colours, fonts, voice and banned phrases are applied), background (auto lets AI Studio decide, none for a plain colour, photo for a stock photo described by backgroundDescription; on prompt, a mediaAssetId image is used as the background instead). Returns at once (202) with statusUrl; poll it at most every 5 seconds, typically for 10 to 60 seconds. With insert: false (default) the job stops at Ready with previews (PNG links to show the person) and an insertUrl; with insert: true the slides are added straight away (Inserted, with insertedSlideIds). Same plan, trial and credit limits as AI Studio in the web app: EntitlementRequired with upgradeUrl when the plan or trial does not cover it or the credit limit is reached. The trial covers prompt only. To show a photo as it is, add it with POST /v1/slideshows/{id}/slides instead.

GET /v1/slideshows/{id}/slides/ai/{jobId}content manager or above

The status link from POST /v1/slideshows/{id}/slides/ai: status is Processing, Ready (see previews; insert with insertUrl), Inserted (insertedSlideIds), NeedsClarification (read question to the person, then start a new job with the answer in prompt) or Failed (error, and errorCode when there is one). Ready with an error means adding the slides stopped part-way (e.g. storage full): insertedSlideIds lists what was added, and the insert call adds the rest without doubling any. A job the service could not finish (a restart) turns Failed; start it again. Jobs are kept for 24 hours.

POST /v1/slideshows/{id}/slides/ai/{jobId}/insertidempotentcontent manager or above

Adds a Ready job's previews to the slideshow, in order, from position (zero-based; appended when omitted), each shown for a time set by its amount of text. Show the person the previews first. Repeating the call after it succeeded does not add them twice. Queues a re-render.

POST /v1/ai-studio/trialidempotentcontent manager or above

Starts the workspace's free AI Studio trial: 7 days or 3 slides, whichever comes first, making slides from a text description (handler: prompt). It can be used once per workspace, so ask the person before starting it; POST /v1/slideshows/{id}/slides/ai says trialAvailable: true in its EntitlementRequired details when it is still unused. Returns state, slidesRemaining and endsAt; calling it again while the trial runs returns the same. includedInPlan: true means the plan already has AI Studio. A used trial returns EntitlementRequired with upgradeUrl.

GET /v1/slideshows/{id}/playscontent or device manager

How much a slideshow has played: estimated minutes per day per screen over ?from=&to= (both yyyy-MM-dd, required), a workspace total, and an approximate loop count per screen from the slideshow's own duration. Minutes are estimated from device heartbeats, not exact; per-slide counts are not recorded. The range is clamped to the log retention window.

Slides

Edit, hide or delete a single slide; per-slide visibility rules, live data pushes, and self-verification previews.

PATCH /v1/slides/{id}content manager or above

Changes one slide: how long it shows (durationSeconds, 1-300) and whether it is hidden (hidden). Use it to hide or show a slide, or make it stay longer or shorter. A dynamic slide also takes templateId, sourceId, fieldMapping (generated when the template or source changes without one), overrideData (merged into the slide's own values, a null value removes one: use it to change a price or a line of text on a slide not fed by a Push source) and countdown ({overlayId, mode: fixed|daily|source, targetTime|dailyTime|sourceField}, [] clears it). Text inside an uploaded PowerPoint cannot be edited: replace the file instead. To expire or take down a slide on a date, set a date condition via PUT /v1/slides/{id}/conditions instead of scheduling a delete. Omitted fields are left unchanged.

DELETE /v1/slides/{id}content manager or above

Removes a slide from its slideshow for good; the slides after it move up. To take a slide off screen temporarily, hide it with PATCH instead.

PUT /v1/slides/{id}/conditionscontent manager or above

Replaces a slide's own visibility rule, e.g. "only show the breakfast menu before 11", "show until 6 January" (..2027-01-06), "only during December" (2026-12-01..2026-12-31), or "hide the Christmas slide after December 26" (2026-12-27..). Never promise to delete later — set a date condition instead. Each condition is a type and value: time 08:00-11:00, weekday Monday,Tuesday, date 2026-12-01..2026-12-24 (inclusive; either end may be left open, e.g. 2026-12-27.. or ..2027-01-06), or data_trigger {sourceId}, {sourceId}:empty or {sourceId}:contains:{text}. mode: show (default, show only while the conditions hold) or hide (hide while they hold). match: all (default) or any. An empty list clears the rule. Rules set on the web page appear on the slide as rules with readOnly: true and are not changed by this call.

POST /v1/slides/{id}/sourceadmin key, or bound isk_dp_ key

Pushes a JSON payload onto a dynamic slide's live data, validated against the template's data schema. When the slide is on a Push source, the data is stored on that source (shared by every slide on it; the slide stays hidden until the first push); otherwise it is stored on the slide and a re-render is queued. Data reaches a video-stream screen at most about once a minute: every push is stored at once, and the next render shows the newest payload, so pushing faster than that gains nothing. On HTML playback a live slide picks up each push without a render. Supports ?dryRun=true. One of the two endpoints a push-only isk_dp_ key may call.

GET /v1/slides/{id}/preview.pngcontent manager or above

Returns a PNG of the slide's current rendered state — raw image/png bytes, not an envelope.

Sources

Content sources: live data feeds (RSS, weather, calendar, custom text) and Push sources your system pushes into.

GET /v1/sourcescontent manager or above

Lists the workspace's content sources (id, name, adapterType, lastFetchedAt, isEnabled, and health: Healthy, Degraded after 1-2 failed fetches in a row or Failing after 3 or more, with lastError in plain words, consecutiveFailures and lastSuccessAt). Check health before building a slide on a source. Use this to pick a ticker source (PATCH /v1/slideshows/{id} with ticker.sourceIds) or a data feed for a dynamic slide (PATCH /v1/slides/{id} with sourceId).

POST /v1/sourcesidempotentcontent manager or above

Creates a content source that InfoSlides fetches (RSS feed, weather, flights, calendar, custom text): {"adapterType": "RssFeed", "name": "News", "config": {"feedUrl": "https://example.com/rss"}, "fetchIntervalSeconds": 900}. GET /v1/adapters lists each adapter's configFields; config is validated against them. fetchIntervalSeconds defaults to 3600; lower values than 300 are raised to 300. Fetching sources need a paid plan (EntitlementRequired with upgradeUrl otherwise); a Push source is made by adding a push template's slide with POST /v1/slideshows/{id}/slides/dynamic.

GET /v1/adapterscontent manager or above

Lists available content adapters for the workspace with their user-friendly metadata, license tier requirement, and configuration fields schema.

PATCH /v1/sources/{id}content manager or above

Updates a content source's display name, enabled state, polling interval, or configuration. Omitted fields are left unchanged. fetchIntervalSeconds is at least 300; lower values are raised to 300.

DELETE /v1/sources/{id}content manager or above

Deletes a content source. Fails if the source is currently in use by any dynamic slide or ticker.

POST /v1/sources/{id}/dataadmin key, or bound isk_dp_ key

Pushes a JSON payload to a Push source, validated against the data schema of every template it feeds, and returns receivedAt. Every slide on the source updates; slides stay hidden until the first push and hide again after the source's hideAfterMinutes without data. Supports ?dryRun=true. Callable by a push-only isk_dp_ key bound to the source. Data reaches a video-stream screen at most about once a minute: every push is stored at once, and the next render shows the newest payload, so pushing faster than that gains nothing. On HTML playback a live slide picks up each push without a render.

POST /v1/sources/{id}/keyscontent manager or above

Creates a push-only isk_dp_ key bound to the Push source, to hand to the system that sends its data. It may only call POST /v1/sources/{id}/data for this source. The key is returned once.

GET /v1/sources/{id}content manager or above

Returns a Push source's state: lastReceivedAt (null before the first push), isShowingData (false while its slides are hidden for lack of data), hideAfterMinutes and slideIds, the slides it feeds. Use it to confirm a push landed. Every response also has adapterType and isEnabled; for a fetched source (RSS, weather, etc.) the push fields are empty, lastFetchedAt says when it last fetched, and health, lastError, consecutiveFailures and lastSuccessAt say whether it is working.

POST /v1/sources/{id}/fetchcontent manager or above

Queues an immediate fetch of a source the workspace owns or has been assigned, like Fetch Now on the Sources page. The fetch runs in the background: read GET /v1/sources/{id} after a few seconds for the new health. Limited to 10 calls a minute per workspace (429 with Retry-After).

GET /v1/sources/{id}/itemscontent manager or above

Returns the most recent items a source has fetched, newest first (title, body, imageUrl, url, fetchedAt, expiresAt). ?limit= is 1 to 100, default 20. Use it to see what a slide on this source would show.

GET /v1/sources/{id}/fieldscontent manager or above

Returns the field names a template can be mapped to for a source, taken from its latest item: each has key, label and a sample value. Empty until the source has fetched an item (POST /v1/sources/{id}/fetch asks for one). Use it before mapping a source to a dynamic slide; NotFound for a source that is not the workspace's.

POST /v1/sources/test-connectioncontent manager or above

Checks an adapter configuration against the live service without saving anything: {"adapterType": "RssFeed", "config": {"feedUrl": "https://example.com/rss"}}. Returns success and a plain-language message (a failed check is still a 200). Use it before POST /v1/sources. Limited to 10 calls a minute per workspace.

Media

The workspace's media library: upload, browse, rename, move and delete files, and organise them in folders.

POST /v1/mediaidempotentcontent manager or above

Uploads a file (multipart/form-data, a single file part, up to 100 MB) into the media library and returns its id, type, and pixel dimensions. Use the id as mediaAssetId in POST /v1/slideshows/{id}/slides (show it as it is) or POST /v1/slideshows/{id}/slides/ai (a photo, background or document for AI Studio). A video is transcoded after upload; GET /v1/media/{id} shows its progress. Counts against the workspace storage quota: check GET /v1/media/limits before a large upload. A file over the size limit answers 413, a full library 400; both texts point at that call.

GET /v1/media/limitscontent manager or above

What an upload can be, before trying one: storageUsedBytes, storageLimitBytes, storageFreeBytes (never below 0, even when a plan downgrade left the workspace over its limit), maxFileBytes (the largest upload request, form framing included, so a file must be a little smaller) and acceptedFileTypes (the file extensions, for example .jpg, .mp4, .pdf). The storage figures are the ones GET /v1/media carries in storage.

GET /v1/media/{id}content manager or above

Returns a media asset's processingStatus (Ready, Queued, Processing or Failed), and while a video transcode runs, progressPercent (0-100, from FFmpeg's own encoding progress) and, if it failed, error. Images and documents are always Ready. Poll at most every 5 seconds.

GET /v1/mediacontent manager or above

Lists the media library, newest first: each item has id, name, fileType (image, video, document), sizeBytes, width, height, folderId (null at the root), shared (true for a file another workspace shares, which cannot be changed or deleted here), sharedWithOthers (true for the workspace's own file that it shares) and createdAt. Filter with folderId, type and name (contains, any case); page and pageSize (1 to 100, default 50) page through it. The answer also carries total, totalPages and storage (usedBytes, limitBytes), so check the room left before uploading. Pictures AI Studio made for slides are not listed.

PATCH /v1/media/{id}content manager or above

Renames (name, up to 200 characters), moves (folderId, a folder from GET /v1/media/folders, or null for the root) and/or shares (shared: true lets other workspaces use the file, false stops) a file the workspace owns; sharedWithOthers in the answer shows the sharing. Leave a member out to keep it as it is; send at least one. Returns the item. NotFound for another workspace's file, Forbidden for one it only shares.

DELETE /v1/media/{id}content manager or above

Deletes a file the workspace owns and frees its storage. Refused with 409 while a slide uses it: details.slides lists each one (slideId, slideshowId, slideshowTitle), so remove or change those slides first. This covers media slides and designed slides, the ways a file is held by a slideshow; a file used only as a background or picture inside a designed slide's settings is not checked. NotFound for another workspace's file, Forbidden for one it only shares.

GET /v1/media/folderscontent manager or above

Lists every media folder as one flat list: id, name and parentFolderId (null at the root). Find a folder by name here, then pass its id as folderId to GET /v1/media or PATCH /v1/media/{id}.

POST /v1/media/foldersidempotentcontent manager or above

Creates a media folder: {"name": "Menus", "parentFolderId": null}. name is 1 to 200 characters; parentFolderId is optional (root when left out). Returns the folder (201).

PATCH /v1/media/folders/{id}content manager or above

Renames a media folder: {"name": "Specials"}. Returns the folder.

DELETE /v1/media/folders/{id}content manager or above

Deletes an empty media folder. Refused with 409 while it still holds files or sub-folders; move or delete those first.

Templates

Reusable slide templates, authored as HTML/CSS or generated from a prompt.

POST /v1/templatesidempotentcontent manager or above

Creates a workspace-owned template. title is required, plus exactly one of code mode (html + css; every {{field}} in the HTML becomes a data field) or AI mode (prompt describing the design + sampleJson, an example of the data it shows). "dataMode": "push" makes a template whose data an outside system pushes (see POST /v1/sources/{id}/data). Example: {"title": "Queue", "html": "<h1>Now serving {{number}}</h1>", "css": "h1{font-size:12vh}", "dataMode": "push"}. Try a design with POST /v1/templates/preview first. Supports ?dryRun=true. Needs a plan that includes AI Studio, except one code-mode push template on any plan (the free live data slide); otherwise EntitlementRequired with upgradeUrl.

GET /v1/templatescontent manager or above

Lists templates visible to the workspace: the built-in ones plus its own, each with id, title, sampleJson (the data fields it expects, with example values), html and css. Pick one here for POST /v1/slideshows/{id}/slides/dynamic (templateId) before designing a new one.

GET /v1/templates/categoriescontent manager or above

Lists the template category names the workspace may use, such as News or Generic: the same set that decides which built-in templates GET /v1/templates shows.

GET /v1/templates/{id}content manager or above

Returns one template visible to the workspace: id, title, sampleJson (its data fields with example values, which is what a push to its source must send), html and css. NotFound for another workspace's template.

POST /v1/templates/previewcontent manager or above

Renders a PNG of the given layoutHtml/layoutCss/sampleData and saves nothing — try a design before creating it with POST /v1/templates. Same validation as creating a template. Optional aspectRatio (16:9, 9:16, 1:1) sizes the render; defaults to 16:9. Raw image/png bytes, not an envelope. Rate limited per workspace.

PATCH /v1/templates/{id}content manager or above

Changes a template the workspace owns: any of title, html, css and schema (the data fields, {"fields": [{"name": "sales", "type": "number"}]}); omitted ones stay as they are. Same checks as creating one, and a new html without a schema takes its fields from the {{field}} tokens, keeping the type of fields that remain. Slides already using the template pick up the change. The thumbnail is redrawn. A built-in template is Forbidden: copy it first with POST /v1/templates/{id}/clone. NotFound for another workspace's template.

DELETE /v1/templates/{id}content manager or above

Deletes a template the workspace owns. Refused with 409 while any slide uses it: details.slides lists each one (slideId, slideshowId, slideshowTitle), so change or remove those slides first. A built-in template is Forbidden; NotFound for another workspace's template.

POST /v1/templates/{id}/cloneidempotentcontent manager or above

Copies a template, usually a built-in one from GET /v1/templates, into the workspace as <title> (Copy). No body. Returns the new template (201), which can then be changed with PATCH /v1/templates/{id}. Needs a plan with dynamic slides (EntitlementRequired with upgradeUrl otherwise).

GET /v1/templates/{id}/usagecontent manager or above

Shows where a template is used in this workspace: slideCount, slideshowCount and slides (slideId, slideshowId, slideshowTitle). Check it before deleting or reworking a template.

POST /v1/templates/{id}/thumbnailcontent manager or above

Redraws the gallery thumbnail of a template the workspace owns and returns its thumbnailPath. No body. A built-in template is Forbidden. Limited per workspace, like the preview.

Gallery

Curated starter decks that can be cloned into a workspace.

GET /v1/galleryany credential

Lists the curated starter slideshows, each with id, title, description, previewUrl (a picture to show the person) and resolution. A quick start for a person with no content yet: show a few, then copy the chosen one with POST /v1/gallery/{id}/clone.

POST /v1/gallery/{id}/cloneidempotentcontent manager or above

Copies a starter slideshow from GET /v1/gallery into the workspace, keeping its title. No body. Returns the new slideshow (201), ready to edit like any other and to put on a screen with POST /v1/devices/{id}/play. Counts as one of the plan's slideshows; at the limit it returns ValidationFailed with a message to read out.

Brand

The workspace's brand guide: colours, fonts, logos, voice, picture style and layout rules. Read it before making slides so they match the brand. A person confirms it on the Brand page; an agent can only propose changes as a draft.

GET /v1/brandcontent manager or above

Returns the brand guide: inUse (what slides are made from: the confirmed version merged over the workspace logo, accent colour and tone), draft (the working copy), status (Draft or Confirmed), version, usingWorkspaceDefaults, waitingForApproval and brandPageUrl. Each guide has palette (hex colours), type (font keys), logos (media ids), voice, imagery and layout. When waitingForApproval is true, tell the person a draft needs their approval and give them brandPageUrl.

PUT /v1/brandworkspace admin

Replaces the draft guide with the one sent (send the whole guide: {"palette": {"primary": "#0a5c36", "accent": "#e11d48", "background": "#ffffff", "text": "#111827"}, "type": {"headingFontKey": "montserrat"}, "voice": {"tone": "Friendly", "preferredPhrases": ["Welcome"]}, "layout": {"logoPosition": "top-right"}}). Colours are #rrggbb; low text contrast comes back as a warning. The draft is never used for slides until a person confirms it on the Brand page (brandPageUrl); an agent cannot confirm. Needs workspace-admin rights. Nothing is read from files: values are typed, never inferred.

POST /v1/brand/logoidempotentworkspace admin

Uploads a logo image (multipart/form-data, a single file part up to 5 MB) and puts it in the draft as the logo for slot=light (light backgrounds), dark (dark backgrounds) or icon (small square mark). Returns the brand guide. Needs workspace-admin rights; a person still confirms the draft.

POST /v1/brand/checkcontent manager or above

Checks a slideshow against the confirmed brand guide ({"slideshowId": "..."}) and lists, slide by slide, colours outside the palette, text that is hard to read, a missing logo, banned phrases and fonts that are not the guide's (rule: palette, contrast, logo, bannedPhrases, fonts; message is a sentence to read out). Advisory only: nothing is blocked. Looks at designed and dynamic slides, not slides inside an uploaded presentation. followsGuide is true when nothing drifts; hasConfirmedGuide is false when there is no guide to check against. A summary count also appears in GET /v1/workspace/health.

GET /v1/brand/previewcontent manager or above

Lists the sample slides GET /v1/brand/preview.png can draw: title, text, data, image, logo-light, logo-dark, ticker and clock.

GET /v1/brand/preview.pngcontent manager or above

Draws one sample slide from the guide as a PNG (sample, default title; source=draft by default or confirmed), at the workspace's slideshow size. Raw image/png bytes, not an envelope. Show it to the person before asking them to confirm. Rate limited per workspace.

Devices and streams

Register a screen, tell it what to play, and read back its stream links.

POST /v1/devicesidempotentdevice manager or above

Registers a screen (TV, display, menu board) in the workspace: {"name": "Lobby Screen", "resolution": {"width": 1920, "height": 1080}} (resolution optional, snapped to the nearest supported preset; 1080 x 1920 for a portrait screen). Returns the device (201). It shows nothing until a TV is paired to it (POST /v1/pairings with its deviceId) and it has something to play (POST /v1/devices/{id}/play). When a TV is already showing its pairing screen, POST /v1/pairings with qr or nickname and a slideshowId does all of this in one call. Fails with EmailNotVerified until the owner email is verified, and DeviceLimitReached at the plan's screen limit.

GET /v1/devicesdevice manager or above

Lists the workspace's screens (broadcast channels included), what each is playing right now (nowPlayingSlideshowId, nowPlayingTitle), and whether each supports HTML playback mode (supportsHtml). Start here to find "the lobby screen" by name. ?q= filters by name, case- and accent-insensitively. With ?near=lat,lon, screens with a saved location are returned nearest-first with distanceMeters set; screens without one come last, unsorted.

PATCH /v1/devices/{id}device manager or above

Renames a screen (name), sets its resolution (resolution), and/or saves its physical location (latitude, longitude together, optional locationName) so GET /v1/devices?near= can find it by distance. Every field is optional; at least one is required.

DELETE /v1/devices/{id}device manager or above

Removes a screen from the workspace, as the Devices page does: it stops playing, no longer counts against the plan's screen limit, and disappears from GET /v1/devices. Its schedule and pairing are kept. Returns an undo request that brings it back; if the plan has since filled up, the undo fails with DeviceLimitReached. NotFound for another workspace's screen.

POST /v1/devices/broadcastidempotentdevice manager or above

Creates a broadcast channel: one stream that plays a slideshow on any number of screens or in a browser, with no pairing. {"name": "Lobby channel", "slideshowId": "..."} (slideshowId optional; set what it plays later with POST /v1/devices/{id}/play). Returns the device (201); its watch link is GET /v1/devices/{id}/stream. Needs a plan with broadcast channels (EntitlementRequired with upgradeUrl otherwise) and counts against the screen limit (DeviceLimitReached). NotFound for another workspace's slideshow.

GET /v1/devices/{id}/statusdevice manager or above

Returns one screen's online (true when it checked in within the last few minutes), lastSeenAt, and nowPlaying.slideshowId (what its schedule and takeovers resolve to now; nowPlaying is null when nothing is assigned). A quick check after pairing or a change. For all screens at once use GET /v1/workspace/health; for why a screen looks wrong, GET /v1/devices/{id}/diagnosis.

POST /v1/devices/identifydevice manager or above

"Which screen is this?" — shows each targeted screen's own name in large text on its ticker bar for about 90 seconds ("Screen {number}" for a screen with no name), so a person standing in front of an unlabeled TV can read it out. Optional deviceIds targets specific screens; omitted or empty targets every online, non-broadcast screen. Each result includes showsWithinSeconds (up to 60s — the TV app's poll interval) and until (when it reverts). Try GET /v1/devices?near= first when the caller's location is known; identify is the fallback for telling several screens in one room apart.

POST /v1/devices/{id}/playidempotentdevice manager or above

"Play my Q1 slides on the lobby screen" as one call: checks the slideshow's render state, whether the screen is online, and whether it is already scheduled to something else, then either does it (a takeover when until is set, otherwise the default schedule entry) or returns NeedsClarification with ready-made choices. Supports dryRun=true.

POST /v1/playidempotentdevice manager or above

Same as POST /v1/devices/{id}/play, but the target screen is in the body (deviceId) and may be omitted when the workspace has one screen, or the slideshow is already playing on exactly one — otherwise NeedsClarification lists the candidate screens.

POST /v1/devices/{id}/scheduledevice manager or above

Sets the slideshow a screen plays whenever no timed window or takeover applies (its default entry), replacing the previous one: {"slideshowIds": ["..."]} with exactly one id (a list for historical reasons; playlists are not supported). Timed windows are kept. An orientation mismatch attaches an AspectMismatch warning rather than failing, and an HTML slideshow on a screen whose app cannot play HTML attaches HtmlPlaybackUnsupported. POST /v1/devices/{id}/play does the same after checking for conflicts first.

GET /v1/devices/{id}/scheduledevice manager or above

Returns all schedule entries for a device along with the workspace's effective timeZone. Each entry has id (to delete it), slideshowId, startTime and endTime (HH:mm, workspace time; null on the default entry), isDefault and priority. Temporary takeovers are not schedule entries: see GET /v1/takeovers?deviceId=.

POST /v1/devices/{id}/schedule/entriesidempotentdevice manager or above

Adds a daily time window to a device (dayparting): {"slideshowId": "...", "startTime": "09:00", "endTime": "17:00", "priority": 1}. Times are HH:mm in the workspace time zone; priority is optional (default 0). Outside every window the screen plays its default entry. A window that overlaps an existing one is rejected with ValidationFailed; an orientation mismatch attaches an AspectMismatch warning.

PATCH /v1/devices/{id}/schedule/entries/{entryId}device manager or above

Changes a daily time window in place: any of slideshowId, startTime, endTime (HH:mm, workspace time) and priority; leave a member out to keep it, send at least one. The result is checked like a new window, so one that overlaps another is rejected with ValidationFailed. Returns the entry as POST /v1/devices/{id}/schedule/entries does, with the same AspectMismatch and HtmlPlaybackUnsupported warnings, and an undo. The default entry has no times: change what it plays with POST /v1/devices/{id}/schedule.

DELETE /v1/devices/{id}/schedule/entries/{entryId}device manager or above

Deletes a schedule entry (time window) from a device; entryId is an id from GET /v1/devices/{id}/schedule. The screen falls back to its default entry during that time.

GET /v1/devices/{id}/streamdevice manager or above

Returns the device's stream links: playerUrl (plays either playback mode) is always populated; hlsUrl is populated only when the resolved playbackMode is VideoStream, and is null for Html mode. Flags StreamNotReady when nothing is playable yet, and HtmlPlaybackMode whenever the mode is Html. A screen with nothing scheduled has no mode to resolve: playbackMode reads VideoStream, hlsUrl is set and StreamNotReady says why.

GET /v1/devices/{id}/playscontent or device manager

How much a screen has played: estimated minutes per day per slideshow shown on it over ?from=&to= (both yyyy-MM-dd, required) and a workspace total. Minutes are estimated from device heartbeats, not exact; per-slide counts are not recorded. The range is clamped to the log retention window.

GET /v1/devices/{id}/now.pngdevice manager or above

The slide a device's currently-resolved slideshow should be showing right now, as a PNG, derived from the schedule, takeovers, slide rules and slide durations. This is what should be on the screen, not a camera on it — an offline TV may not actually be showing this. The slideshow and slide ids are in the X-Slideshow-Id/X-Slide-Id response headers.

GET /v1/devices/{id}/diagnosisdevice manager or above

Every cause that could explain why a screen looks wrong, most likely first: offline since a time, an empty or failed slideshow, nothing scheduled, every slide hidden by its rules, live slides waiting for their first data (WaitingForData: push data to the source), plan expired or over the device limit, or its stream token rotated (paired elsewhere). Each cause carries a sentence and, where there is a concrete next step, a ready-made fix request. A healthy screen returns no causes.

POST /v1/devices/{id}/showidempotentdevice manager or above

"Show this on the lobby screen" in one call: send a photo or video (multipart/form-data, a file part, or a mediaUrl field — exactly one) plus optional until (default 30 minutes) and caption. Stores the file (a mediaUrl download is streamed to disk, up to 1 GB), creates a one-slide slideshow named from caption or today's date, and starts a temporary takeover (APX-12) until until. A portrait photo/video on a landscape screen (or vice versa) gets a blurred, darkened fill rather than plain black bars. Returns at once with the ids and a status link; the screen switches once processing finishes — poll GET /v1/devices/{id}/show/{mediaAssetId}?slideshowId= for the combined percent.

GET /v1/devices/{id}/show/{mediaAssetId}device manager or above

The status link from POST /v1/devices/{id}/show: one combined percent covering the video transcode (when the media is a video still processing) then the slideshow render, plus status (Processing, Ready or Failed) and, if failed, error. Requires ?slideshowId= from the show response.

POST /v1/pairingsidempotentdevice manager or above

Pairs a screen. Send exactly one of qr (the scanned pairing URL or bare session id), nickname, or deviceId alone. qr/nickname with slideshowId creates a new device playing it; with deviceId instead, binds the TV to that existing device (rotating its stream token and evicting any previously paired TV). deviceId alone generates a fresh code to type on that device's TV. qr/nickname with neither returns NeedsClarification listing candidate slideshows and screens. Requires device-manager rights or above — a content-manager session cannot pair a screen.

POST /v1/takeoversidempotentdevice manager or above

Temporarily overrides one or more screens with a slideshow from startsAt (default now) until endsAt or for durationMinutes, or indefinitely until ended: {"deviceIds": ["..."], "slideshowId": "...", "startsAt": "2026-12-31T23:50:00", "endsAt": "2027-01-01T00:30:00"} (or deviceId for one screen). Times with Z or an offset are exact instants; times without a zone are workspace local time. Crossing midnight is fine; endsAt must be after startsAt. Returns the takeover (a list, one per screen, when deviceIds names several), each with its id for DELETE /v1/takeovers/{id}. A slideshow that has never finished rendering takes over only once its first render completes; until then the screens keep their content.

GET /v1/takeoversdevice manager or above

Lists the workspace's temporary screen takeovers, latest start first, each with id, deviceId and deviceName, slideshowId and slideshowTitle, startsAt, endsAt (null when open-ended), cancelledAt and isActive (running right now). ?deviceId= narrows it to one screen; ?activeOnly=true returns only those running right now (not ended, cancelled or still to start). Use it to answer "is anything overriding the lobby screen?" or to find the id to end one early.

GET /v1/takeovers/{id}device manager or above

Returns one temporary screen takeover by id, in the same shape as GET /v1/takeovers: whether it isActive, when it starts and ends, and the screen and slideshow it covers.

DELETE /v1/takeovers/{id}device manager or above

Ends an active or scheduled temporary screen takeover early, restoring regular schedules.

Team

Add the actual person to the workspace as a real team member (APX-37). Admin key only.

POST /v1/team/invitationsworkspace admin

Invites a person to the workspace by email. role defaults to TenantAdmin so they can manage the workspace immediately; any other workspace role works too (never a platform role). They receive an email and can accept by signing in with Google, Microsoft, GitHub, or a password, whether or not they already use InfoSlides elsewhere. Fails with EntitlementRequired when the plan's user limit (active members plus pending invitations) is already reached.

GET /v1/teamworkspace admin

Lists the workspace's members and pending invitations. A member whose access is turned off has active: false; use its id to turn it back on.

DELETE /v1/team/invitations/{id}workspace admin

Revokes a pending invitation before it is accepted.

DELETE /v1/team/members/{id}workspace admin

Removes a member from the workspace. Refuses to remove the last active admin.

POST /v1/team/members/{id}/deactivateworkspace admin

Turns off a team member's access without removing them; they can be turned back on. Returns the member with active: false. Refuses to turn off the last active admin.

POST /v1/team/members/{id}/reactivateworkspace admin

Turns a team member's access back on. Returns the member with active: true. Fails with EntitlementRequired when the plan's user limit is already reached.

API keys

Issue and revoke the credentials agents authenticate with.

POST /v1/apikeysidempotentworkspace admin

Creates a workspace API key: {"type": "admin", "name": "Muse"} for a full admin key (isk_admin_), or {"type": "dataProvider", "name": "Till system", "slideIds": ["..."]} for a push-only key (isk_dp_) that may only call POST /v1/slides/{id}/source for those slides. For a Push source, prefer POST /v1/sources/{id}/keys, which binds the key to the source. The plaintext key is returned exactly once as key; store it, it cannot be shown again. Workspace admins only.

GET /v1/apikeysworkspace admin

Lists the workspace's keys, revoked ones included: id, name, type (admin or dataProvider), prefix (the first 12 characters, to tell keys apart), slideIds for a push-only key, createdAt, lastUsedAt and revokedAt. Full keys are never returned. Use it to find a key's id to revoke.

DELETE /v1/apikeys/{id}workspace admin

Revokes an API key. Idempotent — revoking an already-revoked key still succeeds.

Billing

Which plan the workspace is on, and the upgrade path out of the free plan. Plan changes, pause and cancel stay in the web app.

GET /v1/billingworkspace admin

Reports the workspace's plan and what it costs, its status (free, active, cancelling, paused, past_due, cancelled or suspended), renewsAt (empty when it does not renew or could not be read), endsAt for a cancellation already scheduled, and what is used against each limit (slideshows, devices, users, storageMb, each { used, limit }). invoicesUrl points a person at the invoices page. Read-only and without card data.

POST /v1/billing/checkoutworkspace admin

Returns a link to upgrade the plan. Optional plan (default: the cheapest plan lifting current limits) and billingPeriod (monthly or annual). Ask the person which period they want: with it, the link opens a ready-to-pay Paddle checkout; without it, the link opens the plans page with the plan highlighted and they choose there. A workspace that already pays gets its manage-billing page, where plan changes are prorated.

Audit and errors

What changed and what went wrong in the workspace (APX-65). Workspace admins only; always this workspace.

GET /v1/auditworkspace admin

Reads the audit trail, newest first: time, action (e.g. SlideshowDeleted), entityType, entityId, userId (who; empty for the system) and details (JSON, redacted; left out for events that carry credentials, such as API keys, pairing and invitations). Filter with entityType and entityId (what a thing's history is), userId, action, category (User, Tenant, Slideshow, Device, Schedule, Sources, Templates, Billing, Settings, Support, TvDevice, WebClient), from and to (UTC). Paged with page and pageSize (default 25, at most 100): items, total, page, pageSize, totalPages.

GET /v1/errorsworkspace admin

Reads the workspace's error log, newest first, as a workspace admin sees it in the web app: time, level (Warning, Error, Critical), component, message (redacted, at most 1000 characters), entityType, entityId and acknowledged. No stack traces. Filter with level, component, acknowledged, from and to; paged as GET /v1/audit.

Undo

Take back a change an agent made (APX-25). Every response to a changing call carries `undo`, a ready-made request for this endpoint.

POST /v1/undocontent or device manager

Puts back what an earlier call changed: {"token": "..."}, the undo.body of that call's response. Works for 24 hours. When the same slide, slideshow, schedule or screen was changed again since, answers NeedsClarification with a choice that overwrites that later change (force: true); ask the person before sending it. Returns { "restored": n }. Not covered: pushed live data, slideshow deletion (restore it with POST /v1/slideshows/{id}/undelete), screen settings, and which TV was bound before a re-pairing.

Feedback

Tell the InfoSlides team what went wrong, or give your verdict on InfoSlides for the website (APX-47). Nothing else changes.

POST /v1/feedback/issuesany credential

Reports something you could not do for the person: no endpoint for it, an error you could not get past, or docs that led you wrong. problem (required, at most 2000 characters) says what went wrong; send intent (what the person asked for, in their words, at most 500), request (the call that failed, e.g. PATCH /v1/slides/{id}, at most 200) and agent (your product name, at most 40) when you have them. Leave out personal data. Returns { "id" }. At most 50 reports and comments per workspace in 24 hours.

POST /v1/feedback/testimonialsany credential

Leaves a testimonial: your own verdict on InfoSlides after working with it (e.g. "Had the lunch menu on the lobby TV in two calls."), which the team may show on the website or a partner page as a quote attributed to agent, your product name (at most 40 characters), labelled as an AI agent. quote is required, at most 1000 characters. Send context too (what you did that the verdict is about, e.g. "set up a live menu board", at most 200 characters) so the quote can be shown with it. Write it as you would want it shown, in your own words; never quote the person, and leave out personal data and anything they told you in confidence. Returns { "id" }. Shares the 50-per-24-hours limit with POST /v1/feedback/issues.

Error codes

Every error envelope carries exactly one of these codes, so a client can branch on error.code rather than parsing messages.

CodeHTTPMeaning
Unauthorized 401 Missing credential, or a credential type that is not an API key.
InvalidApiKey 401 The presented API key does not match any known key.
ApiKeyRevoked 401 The presented API key exists but has been revoked.
Forbidden 403 Valid credential, insufficient rights.
EmailNotVerified 403 Workspace owner's email is unverified. Remediate with POST /v1/auth/resend-verification.
EntitlementRequired 403 The feature requires a paid plan. Details carry an upgrade URL.
DeviceLimitReached 403 The workspace's active-device quota is exhausted.
KeyScopeViolation 403 A push-only data-provider key was used outside its allowed endpoint or slides.
ValidationFailed 400 Schema or field validation error. details.fields lists the offending fields. A field the endpoint does not know (a misspelt or guessed name) is rejected this way too, never silently ignored.
NotFound 404 The resource does not exist within this workspace, or no endpoint matches the method and path (a mistyped route or an id that is not a GUID).
MethodNotAllowed 405 The path exists but not for this HTTP method. The Allow header and details.allowedMethods list the methods it takes.
NeedsClarification 409 The request is ambiguous or collides with existing state. details.question is a plain-English question and details.choices lists ready-made ways out, each a complete request (id, label, recommended, apply). Send the recommended choice's apply request to resolve it without asking, or read the question out and send whichever choice the person picks.
RateLimited 429 Too many requests. The Retry-After header is set.
InternalError 500 Unexpected server error.

Zero to a live screen

Four calls take a brand-new workspace to a playable HLS stream. The first is anonymous; the rest use the key it returns.

# 1. Provision a workspace (anonymous), returns an admin API key, once.
curl -X POST https://infoslides.app/v1/tenants \
  -H 'Content-Type: application/json' \
  -d '{"tenantName":"Acme Cafe","ownerEmail":"<owner-email>","source":"api"}'

# 2. Clone a starter deck from the gallery.
curl -X POST https://infoslides.app/v1/gallery/$GALLERY_ID/clone \
  -H "Authorization: Bearer $ISK"

# 3. Register the screen.
curl -X POST https://infoslides.app/v1/devices \
  -H "Authorization: Bearer $ISK" -H 'Content-Type: application/json' \
  -d '{"name":"Lobby screen"}'

# 4. Assign the deck, then read the stream link.
curl -X POST https://infoslides.app/v1/devices/$DEVICE_ID/schedule \
  -H "Authorization: Bearer $ISK" -H 'Content-Type: application/json' \
  -d '{"slideshowId":"'$SLIDESHOW_ID'"}'
curl https://infoslides.app/v1/devices/$DEVICE_ID/stream \
  -H "Authorization: Bearer $ISK"

The same workflow is available as a CLI and as an MCP server for AI agents, one dependency-free binary. See github.com/arnibj/InfoSlides.MCP or the agent guide.

© 2026 InfoSlides. All rights reserved.
An unhandled error has occurred. Reload ✕