Skip to main content

Wallboard API - Getting Started (2.0)

API Support: [email protected]

Welcome to the Wallboard API documentation. This guide covers the fundamentals you need to get started with the API.

Overview

The Wallboard API is a RESTful API using OAuth 2.0 for authentication. All data is exchanged in JSON format.

All API endpoints use the /api/ prefix and require OAuth 2.0 Bearer token authentication.

AI-Optimized Documentation

This API documentation is optimized for AI coding assistants:

  • Static docs: Pass docs.wallboard.info/llms.txt to your AI agent for context about the API.
  • MCP Server: Connect the Wallboard MCP server (https://{server}/mcp) for interactive API exploration — search endpoints, inspect schemas, read/write data, and search platform docs directly from your AI assistant.

Quick Start

  1. Get an access token - Use OAuth 2.0 (see Authentication)
  2. Make API calls - Include Authorization: Bearer <token> header
  3. Handle responses - All responses are JSON, null values are omitted

API Versions

API mixes v1 (/api/{resource}) and v2 (/api/v2/{resource}) — always check endpoint in api_howto.

Multi-tenancy

Network Owner IS a customer — same table, subreseller object present indicates Network Owner status. Enables cross-reference WBQL queries between customers and subresellers.

Role Without customerId With customerId
ADMIN ⚠️ GLOBAL (all tenants) Scoped to that tenant
Network Owner All member tenants Scoped to that tenant (if owned)
Regular Own tenant only Own tenant only

⚠️ ADMIN without customerId = data corruption risk:

  • Queries return mixed cross-tenant results
  • Using those IDs can link resources across tenants
  • Always: Ask user which customer → search by name → use customerId in requests

Network Owner: Without customerId operates on all member tenants. Provide customerId to scope to specific tenant.

Batch operations (e.g., /api/v2/device/{command}?search=...): Require customerId, cannot run cross-tenant.

Explicit global: customerId=-1 when cross-tenant query is intended.

No batch create endpoint — create one resource per API call. (MCP tools orchestrate multiple calls.)

Response Behavior

  • Null values omitted — don't expect all schema fields
  • PUT = partial update — only provided fields change
  • Exception: data field — full replacement (use manage_playlist/manage_datasource)
  • Counting: size=1&select=idtotalElements in response

IDs

Format: UUID (32-char, no dashes) or integer. Server-generated on create.

Search vs Select

  • search (WBQL): ANY field including internal (e.g., teamAssignments)
  • select: Only DTO fields in OpenAPI schema

Flat API

No nested URLs. Query by relationship in search param.

Folders

Three types: deviceGroup, fileFolder, contentGroup. Root folders: auto-created per customer, no name field — query first to get ID.

Summary view (v2, folders+items paginated):

/api/v2/file/view/summary?folderSearch=parentId={folderId}
/api/v2/device/view/summary?deviceGroupSearch=parentId={folderId}
/api/v2/deviceContent/view/summary?contentGroupSearch=parentId={folderId}

Params: {entity}Search, {folder}Search for WBQL; select, {folder}Select for projection.

Teams

Control resource visibility.

Create with team: POST /api/{resource}?teamIds={id}:{readOnly},{id2}:{readOnly2},...

Modify (OWNER role required): POST /api/{resource}/updateTeamAssignments?{idParam}={id} Body: {"assignToTeams": [{"teamId": "x", "readOnly": false}], "removeFromTeamIds": ["y"]}

Tags

Resources: Device, DeviceGroup, Campaign, Message, DeviceContent, File.

Bulk ops: POST /api/v2/{resource}/tags/{add|set|remove}?search=... body: ["tag1"]

Note: Devices use bulk endpoint only. Other entities support {"tags":[...]} in PUT.

Protected Operations

User CRUD, password change, customer delete, config changes may require CAPTCHA (g-recaptcha-response) and/or 2FA (x-totp) headers — depends on server and user settings.

Scheduling (affectedDateRanges)

Used in playlists, datasources, campaigns/messages.

{"intervals": [{
  "affectedDays": {"monday":true,...,"sunday":false},  // 7 booleans, omit=all true
  "affectedHours": {
    "allDay": false,        // true=24h, omit=true
    "from": "09:00",        // daily start (HH:mm), omit=00:00
    "end": "17:00"          // daily end (HH:mm), omit=23:59
  },
  "from": "2024-01-01",     // start date (yyyy-MM-dd), omit=no limit
  "fromTime": "10:00",      // time on start date (HH:mm), omit=start of day
  "to": "2024-12-31",       // end date, omit=no limit
  "toTime": "18:00",        // time on end date, omit=end of day
  "isExcluded": false       // false=active during, true=blackout (required)
}]}

Multiple intervals: Evaluated with OR logic. Exclude (isExcluded: true) takes precedence over include.

Overnight schedules: When from > end (e.g., 22:00→06:00), the interval spans midnight. Additional flags (allowPartialStartingSegment, allowPartialEndingSegment, allowDayOfWeekOverflow) control edge cases — rarely needed.


Parameters for GET requests. MCP handles URL encoding automatically.

Pagination

Param Default Description
page 0 Page index (zero-based)
size 20 Items per page (max: 1000)

Response includes totalElements, totalPages for pagination info.

Sorting

sort=field,asc|desc — Multiple: sort=name,asc&sort=date,desc — Nested: sort=content.name,asc

Select

Syntax Example
* All primitive fields (not computed/relations)
field,field select=id,name,enabled
relation(fields) select=*,customer(id,name)

id always included. Max 2 levels nesting. Computed and select-only fields must be requested explicitly.

Additional Filters

Parameter Description
quickFilterId Saved WBQL filter, ANDed with search
selectTeamIds Filter by team ID(s), comma-separated
includeResourcesWithoutTeam Include items without team (default: false)
includeReadOnlyInfo Add readOnly field to response

Filtering syntax for search parameter. Case-sensitive.

Value Operators

Symbol Meaning Example
: Contains (strings) / equals (others) name:lobby
= Exact match deviceStatus=ONLINE
Not equal type≠SCREEN
Not contains (strings) name∉test
^ Starts with name^Device
> < Numeric/date comparison brightnessLevel>50
In set (comma = OR) type∈SCREEN,TABLET

Note: tags:value checks array contains (special case).

Logical Operators

Symbol Meaning Example
, AND status=ONLINE,type=SCREEN
| OR name=A|name=B
() Grouping (status=ONLINE|status=OFFLINE),platform=ANDROID

⚠️ OR binds tighter than ANDa:1|b:2,c:3 = (a:1 OR b:2) AND c:3

Special Values

Value Meaning Example
=NULL Is null / collection empty parentId=NULL, teamAssignments=NULL
=!NULL Not null / collection not empty content=!NULL
=true / =false Boolean enabled=true

NULL works on JPA collections — useful for checking empty relations (e.g., teamAssignments=NULL).

Advanced Syntax

Nested fields: content.name:welcome, teamAssignments.team.id=abc

Dates: Unix ms — createdAt>1704067200000

URL Encoding

MCP clients: Send raw values — server encodes automatically.

Exception: If VALUE contains operators (: = , | > < ( )), pre-encode them (e.g., name:Hello%2CWorld).

Raw HTTP: Double-encode operators in values (%2C%252C).

Folder Filtering

Entity Parent field Path field (recursive)
Device deviceGroupId deviceGroupPath
Content/Playlist contentGroupId contentGroupPath
File fileFolderId fileFolderPath
DeviceGroup parentId deviceGroupPath
ContentGroup parentId contentGroupPath
FileFolder parentId fileFolderPath

Patterns: Direct: {field}={id} — Recursive: {path}:{id} — Root: parentId=NULL

Team Filtering

All entities use teamAssignments field:

teamAssignments.team.id={teamId}
teamAssignments.team.id=A|teamAssignments.team.id=B
teamAssignments=NULL
teamAssignments=!NULL

Examples

Goal Query
By ID id=abc123
Name contains name:test
Has content content=!NULL
In set type∈SCREEN,TABLET
Has tag tags:promo
Tags AND tags:a,tags:b
Folder recursive deviceGroupPath:uuid

Entity Mapping

UI Term API Entity Notes
Channel campaign level=WIDGET
Schedule campaign level=TOP
Sub-channel message
Sub-channel Group messageGroup
Screen / Player / Display device
Device Folder deviceGroup
Media Folder fileFolder
Content Folder contentGroup
Playlist / Loop simpleLoop JSON slides = UI "pages"
Slide content structureType=SLIDE
Content (Interactive) content structureType=COMPLEX
Template template
Data Source / Feed datasource
Action webhookEventAction Integrations menu
Alert / Notification alertRule
Quick Filter quickFilter
Tag tag
User / Member user
Team team
Network Owner subreseller
Client / Tenant customer
Advertiser advertiser /api/adv
Licenses / Customer Licenses licenseOrder UI: "Licenses" (customer), "Customer Licenses" (admin)
License Package / License Template licensePackage Admin-only templates
UI Profile userInterfaceProfile
Logs / Audit Log log
Device Metrics / Device Stats deviceStat

Content Type Hierarchy

SystemContent (base)
├── Template                         → /api/template
└── DeviceContent (abstract)/api/v2/deviceContent (combined query)
    ├── Content (slides, multi-page)/api/content
    └── SimpleLoop (playlists)/api/simpleLoop

/api/v2/deviceContent: Queries Content + SimpleLoop together. Use deviceContentType to distinguish.

deviceContentType structureType UI Term
simpleLoop - Playlist
content SLIDE Slide
content COMPLEX Content (Interactive)

Embedding Rules

Container Can Embed
Playlist Media files, folders, Slides, nested playlists (NOT multi-page Content)
Slide Slides, playlists, channels (via widgets)
Content (COMPLEX) Slides, playlists, channels, content, media (full)

Enum Values

User Roles (hierarchical - higher includes lower)

API Value UI Display
ADMIN Super Administrator
OWNER Administrator
TECHNICIAN Technical manager
APPROVER Content manager
EDITOR Content editor
VIEWER View only
DEVICE_USER Device user

Note: MESSENGER exists in backend enum but is unused/not supported.

License Types

API Value UI Name
BASIC Lite
PROFESSIONAL Professional
DBA Broadcaster (legacy, not used for new orders)
ENTERPRISE Premium
VIDEO_WALL Video Wall

Platforms

Code Description
ANDROID Android media players
WINDOWS Windows PCs and players
BRIGHTSIGN BrightSign players
SAMSUNG Samsung Tizen displays
LG LG webOS displays
CHROME_OS Chrome OS devices
UNKNOWN Unknown platform

Note: TIZEN, WEBOS, JSCORE are UI-level aliases, not backend enum values. PWA and BROWSER exist in enum but are deprecated/unused.

Data Source Types

sourceType type/systemDatasourceType Description
INTERNAL - Manual/API data
EXTERNAL JSON, XML, RSS, ICAL, GOOGLESHEET_API, CALENDAR, JDBC, SHAREPOINT_LISTS, MICROSOFT_WORKBOOK(_V2), WEATHER, TOAST, CAP, SCREENSHOT, CUSTOM_INTEGRATION, WEBSITE_CONTENT, USER_ACTIVE_DIRECTORY, USER_ACTIVE_DIRECTORY_V2, FACEBOOK_USER_FEED, FACEBOOK/INSTAGRAM_PAGE_FEED, SHAREPOINT_NEWS, FILE_FROM_URL, POWER_BI_EXPORT External sources
SYSTEM FILES, DEVICES, SCREENSHOT, AI_DATASOURCE, CALENDAR_MERGE System data

structureType: TABLE, KEY_VALUE, LIST, FEED, WAY_FINDING, CUSTOM


Content

Entity attributes:

Field Type Notes
name string Display name
comment string Description
structureType enum SLIDE (single page) or COMPLEX (multi-page)
simpleLoopType enum Playlist: NORMAL (default) or EINK (rare)
version string Playlist: required "2.0". Content/Slide: "1.0" or empty
locked boolean If true, cannot be edited
lastSaved datetime Last save time
lastSavedBy object .email, .name
lastActivity datetime Last play time (direct assignment only, not channel/embedded)
contentGroup object .id, .name — parent folder
contentGroupPath string Full path, useful for recursive search
assignedDeviceCount int Devices showing this (computed)
previewPath string Preview image URL (computed)
orientation string From dimensions (computed)
data JSON ⚠️ HUGE — avoid in SELECT
campaignUsageDetails array Channels using this (SELECT-ONLY)
messageUsageDetails array Sub-channels using this (SELECT-ONLY)

Unified read endpoint: /api/v2/deviceContent (all types) Type-specific write endpoints: /api/content, /api/simpleLoop

deviceContentType UI Term Type-specific endpoint
content Slide or Content /api/content
simpleLoop Playlist /api/simpleLoop

Read

GET /api/v2/deviceContent?select=id,name,deviceContentType,structureType,contentGroup(id,name)
GET /api/v2/deviceContent?includeLoops=false              # Only slides/content
GET /api/v2/deviceContent?includeContents=false           # Only playlists
GET /api/v2/deviceContent?search=contentGroupId%3D{id}    # In folder
Param Default Notes
includeContents true Include slides/content
includeLoops true Include playlists

Usage info:

GET /api/{content|simpleLoop}/usageDetails?search=id%3D{id}                 # Embedded in (other content/playlists only)
GET /api/{content|simpleLoop}/getDeviceOnlineOfflineRatio?search=id%3D{id}  # Device stats
GET /api/v2/device?search=contentId%3D{id}|emergencyContentId%3D{id}        # Devices with this assigned

Templates:

GET /api/template?select=id,name,structureType,tags,comment,previewPath&size=20
GET /api/template?search=(tags:healthcare|tags:retail),tags:minimalist  # (industry OR) AND style

Use template_tags tool to discover available tags.

Quick-Editable Templates (for playlist pages):

GET /api/contentGroup/?search=isTemplateGroup%3Dtrue&select=id,name
GET /api/v2/deviceContent?search=contentGroupId%3D{folderId},structureType%3DSLIDE&select=id,name,previewPath
GET /api/v2/deviceContent?search=id%3D{slideId}&select=contentMetaData

Check contentMetaData for widgets with quickEdit: true — customizable via widgetOverrides in playlist.

Create

Content/Slide:

POST /api/content/?contentGroupId={folderId}
Body: {
  "name": "Welcome",
  "structureType": "SLIDE",
  "data": {
    "Default": {
      "tags": {},
      "pageStyle": {},
      "pageData": {"screenSize": {"width": 1920, "height": 1080}}
    }
  }
}

Playlist:

POST /api/simpleLoop/?contentGroupId={folderId}
Body: {
  "name": "My Playlist",
  "simpleLoopType": "NORMAL",
  "version": "2.0",
  "data": {
    "loopData": {
      "orientation": "landscape",
      "resolution": {"width": 1920, "height": 1080},
      "defaultImageFit": "fill",
      "defaultVideoFit": "fill"
    },
    "slides": []
  }
}

From template:

POST /api/content/fromTemplate/{templateId}?contentGroupId={folderId}
Body: {"name": "From Template"}
Param Where Notes
contentGroupId query Required
name, structureType, data body Content: all required
name, simpleLoopType, version, data body Playlist: all required

Update

PUT /api/{content|simpleLoop}/{id}?autoSave=false
Body: {"name": "Updated", "comment": "Description"}

POST /api/{content|simpleLoop}/lock/{id}/true   # Lock (Approver role)
POST /api/{content|simpleLoop}/lock/{id}/false  # Unlock

POST /api/{content|simpleLoop}/duplicate/{id}   # Duplicate
Body: {"name": "Copy"}

Note: Content duplicate returns the created entity, SimpleLoop duplicate returns void.

⚠️ NEVER read or edit the data field via api_read/api_write. For playlists use manage_playlist tool.

autoSave only matters when updating data field. locked content cannot be updated.

Move: See Folders section → POST /api/contentGroup/move

Tags:

POST /api/deviceContent/{id}/setTags  Body: ["tag1", "tag2"]

Bulk tags (via v2 API):

POST /api/v2/deviceContent/tags/set?search={wbql}     Body: ["tag1", "tag2"]
POST /api/v2/deviceContent/tags/add?search={wbql}     Body: ["tag1"]
POST /api/v2/deviceContent/tags/remove?search={wbql}  Body: ["tag1"]

Delete

DELETE /api/{content|simpleLoop}/{id}?autoSave=false

Customer

Customer = tenant in multi-tenant system.

Access levels:

  • ADMIN: Global access to all customers
  • Network Owner (subreseller): Manages owned customers — a Network Owner IS itself a customer with subreseller object present
  • Regular user: Scoped to their own customer

Cross-reference queries: Because customers and subresellers share the same table, WBQL can cross-reference them:

search=subreseller=!NULL                           # All Network Owners
search=ownerSubresellerId={id}                     # Customers under a Network Owner
search=subreseller.freeLicenses>0                  # Network Owners with free licenses

Entity attributes:

Field Type Notes
name string Required, globally unique
comment string Description
country string Country code
location string Physical location
type string Custom classification
vertical enum Industry vertical (see below)
createdDate datetime Creation timestamp
restricted boolean Account disabled — blocks all access
expirationDate datetime Account expiration — triggers device lockout
force2FA boolean Mandatory 2FA for all users
needsToBeInvoiced boolean Invoice tracking (ADMIN only)

Vertical values (21): BANKING_AND_FINANCE, EMPLOYEE_CORPORATE_COMMUNICATION, CORPORATE_MEETING_ROOMS, DIGITAL_SIGNAGE, EDUCATION_COLLAGE_AND_UNIVERSITIES, CONFERENCE_AND_CONVENTION_CENTERS, GOVERNMENT, HEALTHCARE, HOSPITALITY, QUICK_SERVICE_RESTAURANT, RETAIL, SPORTS, PETROL_STATIONS, TRANSPORTATION, ENTERTAINMENT, WAYFINDING_AND_DIRECTORIES, OTHER, FACTORY_PRODUCEMENT, BUSINESS_PARTNER, RESELLER_PARTNER, TECHNOLOGY_PARTNER

License fields:

Field Type Notes
licenseTier enum Package tier: STARTER, BUSINESS, ENTERPRISE
freeLicenses long Demo licenses from server's global pool — can only assign if pool has availability (ADMIN only)
browserSessionLicenses long Browser content licenses from global pool — legacy, PWA devices replaced this (ADMIN only)
deviceSessionLimit long Max concurrent device sessions (0=unlimited). Special pricing case: pay for 1000 licenses but limit to 20 concurrent (ADMIN only)
activeLicenses long COUNT of Device records assigned to customer — actual deployed devices (computed)
totalLicenses long SUM of approved LicenseOrder amounts — purchased license capacity (computed)

Storage fields:

Field Type Notes
storageSize long Allocated quota in bytes, null=unlimited (ADMIN only)
currentTotalCustomerStorageSize long Total used storage (computed)
currentDatasourceStorageSize long Datasource storage (computed)
currentCustomerFilesStorageSize long Uploaded files storage (computed)

Team access settings (requires separate /settings/teams endpoint):

Field Default Notes
userFullAccessIfNotInTeam true Non-team users: full access vs root only
accessResourcesWithoutTeam true Allow access to team-less resources
isDeviceAndGroupCreationEnabledInRootForTeamUsers true Team users can create devices in root
isContentAndGroupCreationEnabledInRootForTeamUsers true Team users can create content in root
isFileAndFolderCreationEnabledInRootForTeamUsers true Team users can create files in root

Network Owner fields:

Field Type Notes
ownerSubresellerId long Parent Network Owner ID
ownerSubresellerName string Parent name (computed)
subreseller object Present if this customer IS a Network Owner

Other fields: hiddenUIElementRules (JSON), brandingGuideline (JSON), customerMetadata (JSON) — all three read-only for AI (structure undocumented). vistarNetworkId, vistarApiKey

Read

GET /api/customer?search=name:Acme&select=id,name,licenseTier,restricted&size=10
GET /api/customer/{customerId}/license/dashboard    # License dashboard (customer-specific)
GET /api/statistics/network/dashboard?customerId={id}&includeOwnerData=true   # Network Owner: includes owned customers
GET /api/statistics/system/dashboard                # System dashboard (ADMIN only)

ADMIN workflow: Search customer by name → use id as customerId in other requests.

Network Owner: Sees only customers where ownerSubresellerId matches their customer ID.

License dashboard: Customer-specific license metrics — assignedLicenseCount, unAssignedLicenseCount, deviceStatusCounts, licenseTypeCounts, browserSessionLimit, activeBrowserSessionCount, freeCount, usedFreeCount

System dashboard: Comprehensive platform metrics — device counts by status (assigned/unassigned), license counts, customer counts by vertical/tier, user counts, content/playlist/schedule counts, datasource counts by type, credential counts, server stats, license/support expiration dates. Network dashboard (?includeOwnerData=true) includes owned customers.

Create

POST /api/customer
Body: {"name": "Acme Corp", "licenseTier": "ENTERPRISE", "vertical": "RETAIL"}
Field Required Notes
name Globally unique
licenseTier - Default: ENTERPRISE
vertical - Industry vertical
createAsSubreseller - true to promote immediately
ownerSubresellerId - Parent Network Owner (ADMIN only)

Auto-creates: Root deviceGroup, root fileFolder, "Templates" contentGroup.

Network Owner creating: Automatically sets ownerSubresellerId to caller's customer. Cannot set freeLicenses, browserSessionLicenses.

Update

PUT /api/customer/{id}
Body: {"name": "New Name", "restricted": false, "expirationDate": 1735689600000}

Team settings (separate endpoint — fields not on main DTO):

PUT /api/customer/settings/teams
Body: {"userFullAccessIfNotInTeam": false, "accessResourcesWithoutTeam": false}

Emergency content (separate endpoint — field not on main DTO):

PUT /api/customer/settings/setDefaultContents?defaultEmergencyContentId={contentId}
PUT /api/customer/settings/setDefaultContents?defaultEmergencyContentId=null   # Remove

Network Owner operations (ADMIN only):

POST /api/customer/{id}/promoteAsSubreseller          # Promote to Network Owner
POST /api/customer/{id}/removeSubresellerPrivilege   # Remove privilege (must have no owned customers)
PUT /api/customer/{id}/moveToSubreseller?subresellerId={id}   # Move under Network Owner
PUT /api/customer/{id}/moveToSubreseller              # Remove from Network Owner (no param)

Delete

DELETE /api/customer/{id}

Protected operation — may require TOTP + CAPTCHA. Cascades: Deletes all devices, content, datasources, campaigns.

Device

Entity attributes:

Field Type Notes
name string Display name
comment string Description
deviceGroupId string Folder reference
deviceStatus enum ONLINE, OFFLINE
lastDeviceStatusChange datetime Status change timestamp
emergencyStatus boolean Emergency mode active
tags array Device tags
type enum TABLET, PHONE, SCREEN, DESKTOP, EINK, PWA
platform enum ANDROID, WINDOWS, BRIGHTSIGN, SAMSUNG, LG, CHROME_OS, BROWSER, UNKNOWN
serial string Serial number
version string Player app version
firmwareVersion string Device firmware
nativeResolutionWidth int Screen width
nativeResolutionHeight int Screen height
localIpAddress string Device IP
nativeOrientation string Physical orientation
webViewOrientation string Content orientation
previewPath string Screenshot URL (computed)
lastActivity datetime Last device contact
licenseStatus enum UN_LICENSED, FREE, LICENSED, TRIAL
licenseOrderId long License order reference
licenseType enum BASIC, PROFESSIONAL, ENTERPRISE, VIDEO_WALL
timeZone string IANA format
deviceInfo JSON ⚠️ HUGE — avoid in SELECT, request only for single device
deviceGroup object .id, .name — parent folder
content object .id, .name — assigned content
emergencyContent object .id, .name — emergency content
alerts array Active alerts

Read

GET /api/v2/device?search=deviceGroupId%3D{folderId}&select=id,name,deviceStatus,previewPath
GET /api/v2/device?search=deviceGroupPath:{folderId}       # Recursive
GET /api/v2/device?search=tags:lobby                       # By tag
GET /api/v2/device/{id}/previewStore                       # Preview history

Preview URL: /api/storage/customers/{customerId}/devices/{deviceId}/preview.jpg

Admin only — unassigned devices (not yet linked to customer):

GET /api/v2/device?search=customerId%3Dnull&select=id,activationCode,serial,platform

Create

Two methods:

  1. Activation Code — device shows 4-digit code on screen after first boot, user reads it

    • First query unassigned devices: GET /api/v2/device?search=customerId%3Dnull (ADMIN only)
    • Then assign using the code from the device
  2. Pre-registration (zero-touch) — create before physical device exists, auto-links on first boot

    • Use serial + createDeviceIfSerialNotFound: true
    • Device auto-assigns when it boots with matching serial
POST /api/v2/device/assignToCustomer
Body: {"activationCode": "1234", "deviceName": "Lobby", "licenseStatus": "FREE", "licenseType": "BASIC", "timeZone": "Europe/Budapest"}
Field Required Notes
activationCode * 4-digit code shown on device screen — query unassigned devices first
serial * Serial number — use with createDeviceIfSerialNotFound for pre-registration
deviceName
licenseStatus FREE (+ licenseType), LICENSED (+ licenseOrderId), UN_LICENSED
licenseType Required if FREE: BASIC, PROFESSIONAL, ENTERPRISE
timeZone IANA format (e.g., Europe/Budapest)
createDeviceIfSerialNotFound true for pre-registration

Update

PUT /api/device/{id}  {"name": "New Name", "comment": "Updated"}
POST /api/v2/device/move  {"deviceIds": [...], "targetGroupId": "target"}

Batch operations — Pattern: {POST|DELETE} /api/v2/device/{command}?search={wbql}&applyOn={DEVICE|DEVICEGROUP|ALL}[&params]

applyOn specifies what search targets: DEVICE (query matches devices), DEVICEGROUP (query matches folders → affects all devices in them), ALL (both).

Commands marked with ⊖ also support DELETE (same pattern, no body) to remove/reset.

Content:

Command Description Params
assignContent Assign content to device contentId (string), asAssigned (bool)=true, asEmergency (bool)=false
detachContent Remove content from device detachNext (bool)=true, detachAssigned (bool)=true, detachEmergency (bool)=false
refreshContent Reload current content
cacheContent Pre-download content contentId (string), cacheAt (datetime)?
clearCache Clear device cache limit (int)?

Display:

Command Description Params
display Turn screen on/off enabled (bool)
volume Set volume level level (int) 0-100
brightness Set brightness level level (int) 0-100
rotation Rotate display angle (int), type (enum): WEB_VIEW, DEVICE

Power:

Command Description Params
restart Restart device
rebootTime Set daily reboot time time (string) HH:mm
workingHours Set operating hours body: {"mode": "SCREEN|DEVICE", "days": {"MON": {"from": "09:00", "to": "18:00"}, ...}}

Config:

Command Description Params
time Set timezone timeZone (string) IANA format
location Set GPS coordinates body: {"latitude": (double), "longitude": (double)}
setWeatherLocation Set weather location weatherLocation (string)
sensorConfig Configure sensors body: JSON — ask user for format
resetSensor Reset sensor config
advancedConfiguration Set advanced config updateMethod (enum): SET, ADD_OR_UPDATE, REMOVE — body: JSON, ask user for format
updateRule Set update rules body: JSON — ask user for format
setUpdateVersionUpperLimit Limit app version version (string)
datasource Bind datasource datasourceId (string)
dataRowId Set data row binding dataRowId (string)

Debug:

Command Description Params
showName Show device name overlay enabled (bool)
requestLog Request device logs systemLog (bool)=false, systemReport (bool)=false

Other:

Command Description Params
update Update player app version (string)?
emergency Enable/disable emergency emergencyStatus (bool)
tags/{set|add|remove} Manage tags body: ["tag1", "tag2"]
turnOnRapidPreviewMode Fast screenshot mode duration (long) ms, minimumDelayBetweenPreviews (long) ms
takeHighResPreview Request HD screenshot
updateLicense Change license licenseOrderId (long)?, licenseType (enum)?, licenseStatus (enum)?

Delete

DELETE /api/device/{id}

Channel (Campaign)

Terminology: API uses campaign (level=WIDGET for channels, level=TOP for schedules), messageGroup, message. UI shows "Channel", "Schedule", "Sub-channel Group", "Sub-channel".

How channels work: Channels play through Channel Widgets embedded in Content/Slides. Multiple channels in a widget play sequentially by order (weight). Saturation balances play time. Channel tags filter which channels the widget includes. Some playback behaviors (balancing mode, units) are controlled by the Channel Widget, not Campaign settings.

Channel dominance:

Channel Widget (in Slide) → filters channels by tags
└── Main Channel → playback slot DOMINATES all below
    └── MessageGroup → orchestrates, respects sub-channel limits
        └── Message → defines its contribution limit

"All" at any level = cycle through all, but each contributes per its own setting.

Key constraints:

  • Schedules (level=TOP) can only use type=CONTENT with one content item
  • Channels can only contain ONE content type (no mixing media with content)
  • type and deviceSelectionMode immutable (update silently ignored)

Common Fields

Field Type Required Notes
name string CREATE
level enum CREATE WIDGET (channel) or TOP (schedule)
type enum CREATE See type table
version string CREATE Must be "2.0"
enabled boolean - Default: true
weight int - Order (lower first)
saturation float - Balance weight between channels (higher = more play time)
tags array - Tag array for channel widget filtering
locked boolean - Prevent modifications

Device Targeting

Mode How to Target
STATIC deviceAssignment in body (individual devices only)
DYNAMIC (default) deviceGroupAssignment + deviceTagCondition + teamAccessList
QUICK_FILTER deviceQuickFilterId field

Campaign Types

Type Level Content Selection
MESSAGE_GROUP WIDGET MessageGroup → Message hierarchy
CONTENT WIDGET or TOP contentAssignment in body
CONTENT_DYNAMIC WIDGET contentTagCondition + contentGroupAssignment + playedAssetTeamAccessList
ASSETS_STATIC WIDGET fileAssignment in body
ASSETS_DYNAMIC WIDGET fileTagCondition + fileFolderAssignment
CONTENT_QUICK_FILTER WIDGET contentQuickFilterId
ASSET_QUICK_FILTER WIDGET fileQuickFilterId
DSP_VISTAR_MEDIA WIDGET Programmatic advertising (Vistar Media integration)

Assignments

No separate endpoints. Assignments are embedded in campaign/message create/update request body.

Behavior: Assignments are ADDITIVE - adding new items preserves existing ones. Use removeIds to remove.

Pattern:

{
  "xxxAssignment": {
    "assignments": [{"xxxId": "id", "weight": 1}],
    "removeIds": ["id-to-remove"]
  }
}
Campaign Type Assignment Field Item Structure
CONTENT contentAssignment {contentId, weight}
CONTENT_DYNAMIC contentGroupAssignment {contentGroupId}
ASSETS_STATIC fileAssignment {fileId, weight}
ASSETS_DYNAMIC fileFolderAssignment {fileFolderId}
MESSAGE_GROUP messageGroupAssignment {messageGroupId} (Long)

Device assignments:

Mode Field Item
STATIC deviceAssignment {deviceId}
DYNAMIC deviceGroupAssignment {deviceGroupId}

Tag conditions: {"operator": "AND|OR", "tags": ["tag1", "tag2"]}

Fields: deviceTagCondition, contentTagCondition, fileTagCondition

Tip: To target ALL devices/content/files in DYNAMIC mode, assign the root folder ID.

Team filtering (DYNAMIC modes):

  • teamAccessList - filter devices by team (where to play)
  • playedAssetTeamAccessList - filter content/files by team (what to play)

Format: {"teams": [{"id": "team-uuid"}]}

Required for non-OWNER team users (at least one team must be selected).

Advertiser: advertiserId for ad tracking.

Playback Slot (Campaign)

Slot mode mapping:

UI playMultipleItemsInSinglePlaybackSlot playAllItemsInSinglePlaybackSlot
One item/page/media false -
Specific N true false
All true true

UI labels by type:

  • CONTENT types: One page / Specific pages / All contents
  • ASSETS types: One media / Specific media / All media
  • MESSAGE_GROUP: One item / Specific items / All groups

When "Specific N":

Field Notes
playbackSlotDuration Max slot duration (seconds). undefined = no limit
playbackSlotNumberOfElementsToPlay Items per slot

contentShuffleMode (item cycling):

Type BALANCED SERIAL
CONTENT One page per content All pages per content
MESSAGE_GROUP One item per sub-channel All items per sub-channel

Type-specific fields:

Field Applies to Notes
defaultDuration ASSETS types Duration for images (seconds). Default: 10, min: 3
duration All Max item duration override (seconds)
orderingMode DYNAMIC types RANDOM, ALPHABET, DEFAULT
skipDefaultPage CONTENT types Legacy, rarely used

MessageGroup Fields

Field Type Required Notes
name string CREATE
weight int - Order (lower first)

Note: saturation/shuffleMode fields exist but are not used by player.

Message Fields

Important: A sub-channel can only play on a device if the parent channel is also allowed on that device.

Field Type Required Notes
name string CREATE
messageGroupId long CREATE Parent group
type enum CREATE CONTENT, CONTENT_DYNAMIC, ASSETS_STATIC, ASSETS_DYNAMIC, CONTENT_QUICK_FILTER, ASSET_QUICK_FILTER
version string CREATE Must be "2.0"
contentId string type=CONTENT
weight int - Order (lower first)
enabled boolean -
affectedDateRanges JSON - Scheduling

Message playback (via saturation - different from Campaign!):

UI "Play" saturation
One item/page 1
Specific N N
All items 0

Message has NO playbackSlotDuration field.

Additional fields:

Field Applies to Notes
isPlayedElementsCappedByItemCount All types (when "Specific N") If true and saturation=5 but only 3 items exist, plays 3 (not 1,2,3,1,2). Default: true
contentShuffleMode CONTENT types BALANCED (one page per content) or SERIAL (all pages)
orderingMode MEDIA, or CONTENT+DYNAMIC DEFAULT, ALPHABET, RANDOM
skipDefaultPage CONTENT types
defaultDuration ASSETS types For images (seconds)

Message device targeting: Same as Campaign (deviceSelectionMode, deviceAssignment, deviceGroupAssignment, deviceTagCondition, deviceQuickFilterId).

Message content assignments: Same pattern as Campaign (contentAssignment, contentGroupAssignment, fileAssignment, fileFolderAssignment, contentTagCondition, fileTagCondition, playedAssetTeamAccessList).

Scheduling

affectedDateRanges: See API Conventions → Scheduling for format.

validFrom/validTo: Frontend-provided for backend search optimization.

Read

Select: plural form (contentAssignments, deviceGroupAssignments, messageGroupAssignments)

Search: entity prefix + plural (campaignContentAssignments.contentId, messageDeviceGroupAssignments.deviceGroupId)

GET /api/v2/campaign?search=level%3DWIDGET&select=id,name,type,messageGroupAssignments(id,name)
GET /api/v2/messageGroup?select=id,name,weight
GET /api/v2/message?search=messageGroupId%3D{id}&select=id,name,type,saturation,content(id,name)
GET /api/v2/campaign?search=campaignContentAssignments.contentId%3D{id}  # content usage
GET /api/v2/campaign?search=campaignMessageGroupAssignments.messageGroupId%3D{id}  # group usage

Create

Base campaign pattern:

POST /api/campaign
{"name":"...", "level":"WIDGET|TOP", "type":"...", "version":"2.0", "deviceSelectionMode":"DYNAMIC", ...type-specific}

Type-specific fields to add:

Type Required Fields
CONTENT contentAssignment, contentShuffleMode
CONTENT_DYNAMIC contentGroupAssignment, contentTagCondition?, orderingMode, contentShuffleMode
ASSETS_STATIC fileAssignment, defaultDuration, orderingMode
ASSETS_DYNAMIC fileFolderAssignment, fileTagCondition?, defaultDuration, orderingMode
MESSAGE_GROUP messageGroupAssignment, contentShuffleMode

Schedule (level=TOP): type=CONTENT + contentAssignment + contentShuffleMode:"BALANCED" (required but ignored) + affectedDateRanges. Scheduling format: see API Conventions.

MESSAGE_GROUP flow:

POST /api/messageGroup {"name":"Group","customerId":123}
POST /api/campaign {..., "type":"MESSAGE_GROUP", "messageGroupAssignment":{"assignments":[{"messageGroupId":456}]}}
POST /api/message {"name":"Sub", "messageGroupId":456, "type":"CONTENT_DYNAMIC", "version":"2.0", "saturation":1, "contentGroupAssignment":{...}, "deviceSelectionMode":"DYNAMIC", ...}

Playback slot (Specific N): Add playMultipleItemsInSinglePlaybackSlot:true, playAllItemsInSinglePlaybackSlot:false, playbackSlotNumberOfElementsToPlay:N

Update

PUT /api/campaign/{id} {"enabled":true, "weight":2, "contentAssignment":{"assignments":[...], "removeIds":[...]}}
PUT /api/messageGroup/{id} {"weight":2}
PUT /api/message/{id} {"enabled":true, "saturation":2}

Delete

DELETE /api/message/{id}
DELETE /api/messageGroup/{id}  # cascades: deletes all messages in group
DELETE /api/campaign/{id}

Constraints: MessageGroup cannot be deleted while assigned to a campaign. Campaign delete does NOT cascade.

Files

Entity attributes:

Field Type Source Notes
name string upload Filename
contentType string frontend MIME type (browser detection, backend validates)
size long backend File size in bytes
width int frontend Image/video width (backend recalculates if missing)
height int frontend Image/video height (backend recalculates if missing)
tags array upload File tags
validFrom datetime upload Validity start — skipped before this in lists
validTo datetime upload Validity end — skipped after this in lists
muted boolean upload Audio muted flag
metaData JSON frontend Media metadata (extracted via mediainfo.js)
crcCheckSum string backend CRC32 checksum (for duplicate detection)
createDate datetime backend Upload timestamp
creator object backend .email, .name
fileFolderId string upload Parent folder ID
fileFolderPath string computed Full folder path
orientation string computed landscape, portrait, square
location string computed Download URL
thumbnail string computed Thumbnail URL

Thumbnails: Frontend generates for images, videos, PDFs and uploads separately.

Validity: Skipped outside validFrom/validTo in playlists, channels, folder-connected galleries. Direct widget refs unaffected.

Usage fields (SELECT-ONLY):

Field Contains
usageDetails Content references — which contents use this file
duplicateDetails Files with same CRC checksum
campaignUsageDetails Widget-level campaigns using this file
topLevelCampaignUsageDetails Top-level campaigns (schedules) using this file
messageUsageDetails Sub-channels (messages) using this file

Read

GET /api/v2/file?search=fileFolderId%3D{folderId}&select=id,name,contentType,size,thumbnail
GET /api/v2/file?search=fileFolderPath:{folderId}&select=id,name              # Recursive
GET /api/v2/file?search=tags:promo&select=id,name,tags                        # By tag
GET /api/v2/file?search=id%3D{id}&select=id,usageDetails                      # Content usage

URL fields (computed, include in select): location, thumbnail

URL pattern (construct if you have id and customerId):

Download:  /api/storage/customer/{customerId}/files/{fileId}
Thumbnail: /api/storage/customer/{customerId}/files/{fileId}.tmb

Create

Chunked upload (large files, up to 10GB):

Step 1: POST /api/v2/file/upload/init
Body: {"name": "video.mp4", "size": 524288000, "totalChunks": 50, "contentType": "video/mp4", "parentId": "folderId"}
Response: {"uploadId": "...", "uploadUrls": {"1": "https://s3..."}, "maxChunkSize": 10485760, "name": "video.mp4", "size": 524288000, "totalChunks": 50}

Step 2: PUT /api/v2/file/upload/{uploadId}/part/{partNumber} with chunk binary

Step 3: POST /api/v2/file/upload/{uploadId}/complete

Step 4: PUT /api/v2/file/upload/{fileId}/thumbnail with image binary

Recovery: GET /api/v2/file/upload/{uploadId}/recover → {"uploadedChunks": [1,2,3], "missingChunks": [...]}
Cancel: DELETE /api/v2/file/upload/{uploadId}

Simple upload (small files):

POST /api/file?parentId={folderId}
Content-Type: multipart/form-data
Body: files=@video.mp4, previews=@thumbnail.png (preview filename MUST match file!)

Optional params: tags, validFrom, validTo, muted, width, height, inheritParentTeams, fileIdToReplace, removeOldFile

MCP upload (AI assistants cannot send binary):

  • file_upload — UI file picker (preferred)
  • api_upload_from_url — from public URL
  • api_proxy_url + curl — scripted upload

Update

PUT /api/file/{id}
Body: {"name": "renamed.mp4", "tags": ["promo"], "validFrom": 1704067200000, "validTo": 1735689600000, "muted": true}

POST /api/file/rename?fileId={id}&newName=newname.mp4

Tags (bulk):

POST /api/v2/file/tags/add?search={wbql}    Body: ["tag1", "tag2"]
POST /api/v2/file/tags/set?search={wbql}    Body: ["tag1"]
POST /api/v2/file/tags/remove?search={wbql} Body: ["tag1"]

Validity (bulk):

POST /api/v2/file/validity/set?search={wbql}&validFrom={timestamp}&validTo={timestamp}

Replace references (swap file in all content/channels):

PUT /api/file/replaceFileReference?fileIdToReplace={oldId}&sourceFileId={newId}&removeOldFile=true

Files are immutable — use replace to swap everywhere. Requires APPROVER role.

Move: See Folders section → POST /api/fileFolder/moveBatchToFolder

Delete

DELETE /api/file/{id}

Checks usage first — fails if file is referenced in content/channels.

Data Sources

Three categories: INTERNAL (user-managed data), EXTERNAL (fetched from URLs/APIs), SYSTEM (platform-generated from Wallboard's own resources).

Entity attributes:

Field Type Notes
name string Display name (unique per customer)
sourceType enum INTERNAL, EXTERNAL, SYSTEM
structureType enum TABLE, CUSTOM, KEY_VALUE, LIST, FEED, WAY_FINDING
type enum EXTERNAL only: data format (see table below)
systemDatasourceType enum SYSTEM only: FILES, DEVICES, SCREENSHOT, AI_DATASOURCE, CALENDAR_MERGE
comment string Description
global boolean Available to all customers (ADMIN only)
deactivated boolean Manually disabled
editableByDisplay boolean Allow devices to edit (INTERNAL only)

External source type values:

Type Description Key Fields
JSON JSON API remoteUrl, requestSettings
XML XML API (auto-converts to JSON) remoteUrl, requestSettings
RSS RSS feed remoteUrl, keepLastXDays, keepLastXItem
ICAL iCalendar format remoteUrl, dateFormat, timeFormat, timeZone
GOOGLESHEET_API Google Sheets credentialId, spreadSheetId, sheetId, firstRowIsHeader
CALENDAR Google/Microsoft calendar credentialId, calendarId (Google) or microsoftCalendarDatasourceType (O365)
SHAREPOINT_LISTS SharePoint lists credentialId, siteId, listId
MICROSOFT_WORKBOOK_V2 Excel Online credentialId, microsoftWorkbookV2Parameters
JDBC Database query remoteUrl, jdbcUserName, jdbcPassword, jdbcQuery
WEATHER Weather API weatherParameters
SCREENSHOT URL screenshot screenshotParameters
WEBSITE_CONTENT Web scraping with AI webScraperParameters
CAP Emergency alerts capParameters
CUSTOM_INTEGRATION Custom integrations credentialId, dynamicParameters

Credential types: GOOGLE, O365, CUSTOM — set via credentialId + credentialType fields.

Refresh fields (EXTERNAL):

Field Type Notes
refreshFrequency int Refresh interval (seconds)
cronExpressionParameters object Alternative: {cronExpression, timeZone}
nextRefreshTime datetime Calculated next refresh (read-only)
lastUpdated datetime Last successful refresh
errorCounter int Consecutive errors (auto-deactivates at 25)

System datasource configuration:

systemDatasourceType Description Key Fields
FILES File library data quickFilterId, maxElementCount
DEVICES Device inventory quickFilterId, maxElementCount
SCREENSHOT Content page screenshots screenshotParameters
AI_DATASOURCE AI-generated from other datasources aiParameters
CALENDAR_MERGE Merged calendars calendarMergeParameters

Read

GET /api/v2/datasource?search=sourceType%3DINTERNAL&select=id,name,structureType
GET /api/v2/datasource?search=type%3DGOOGLESHEET_API&select=id,name,credentialId
GET /api/datasource/{id}/data?parseData=true   # Get data as JSON
GET /api/datasource/usageDetails?search=id%3D{id}   # Usage in content

Create

INTERNAL:

POST /api/datasource
Body: {"name": "KPI Dashboard", "sourceType": "INTERNAL", "structureType": "CUSTOM"}

EXTERNAL (URL-based):

POST /api/datasource
Body: {"name": "News", "sourceType": "EXTERNAL", "type": "RSS", "structureType": "FEED", "remoteUrl": "https://example.com/feed.xml", "refreshFrequency": 3600}

EXTERNAL (Google Sheets):

POST /api/datasource
Body: {"name": "Sales", "sourceType": "EXTERNAL", "type": "GOOGLESHEET_API", "structureType": "TABLE", "credentialId": "...", "credentialType": "GOOGLE", "spreadSheetId": "...", "sheetId": "...", "firstRowIsHeader": true, "refreshFrequency": 3600}

SYSTEM (Files):

POST /api/datasource
Body: {"name": "Media Files", "sourceType": "SYSTEM", "systemDatasourceType": "FILES", "structureType": "TABLE", "quickFilterId": "...", "maxElementCount": 100}

Update

Metadata:

PUT /api/datasource/{id}
Body: {"name": "Renamed", "refreshFrequency": 1800, "deactivated": false}

Data (INTERNAL only):

PUT /api/datasource/{id}/data
Body: {"visitors": 150, "sales": 42}

Note: Use manage_datasource MCP tool for TABLE structure (row/column operations).

Partial update (display API, public):

POST /api/display/datasource/{id}/set?key=visitors&value=175
POST /api/display/datasource/{id}/merge   Body: {"newField": "value"}

Refresh:

POST /api/datasource/refresh/{id}?sync=true   # Force immediate refresh
DELETE /api/datasource/{id}/clearCache        # Clear cached resources

Delete

DELETE /api/datasource/{id}

Checks usage first — fails if referenced in content.

Folders

Three types with identical hierarchy: deviceGroup, fileFolder, contentGroup.

Entity attributes (shared):

Field Type Notes
name string Display name (absent on root folders)
parentId string Parent folder ID (null for root)
deviceGroupPath|fileFolderPath|contentGroupPath string Full path (computed, for recursive queries)

Type-specific attributes:

deviceGroup
alertCount int Active alerts (computed)
campaignUsageDetails array Channels targeting this folder (SELECT-ONLY)
topLevelCampaignUsageDetails array Schedules targeting this folder (SELECT-ONLY)
messageUsageDetails array Sub-channels targeting this folder (SELECT-ONLY)
contentGroup
isTemplateGroup boolean Template folder flag
contentCount int Content items (computed)
simpleLoopCount int Playlists (computed)
campaignUsageDetails array Channels using content from this folder (SELECT-ONLY)
messageUsageDetails array Sub-channels using content from this folder (SELECT-ONLY)
fileFolder
fileFolderType int 1=STORED (regular/synced), 2=FILTERED (virtual)
quickFilterId string QuickFilter ID (FILTERED type only)
fileCount int Files in folder (computed)
autoSync boolean Cloud sync enabled
googleCredentialId string Google credential (synced folders)
microsoftTenantId string Microsoft tenant (synced folders)
googleDriveFolderDetails object Google Drive sync config
oneDriveFolderDetails object OneDrive sync config
campaignUsageDetails array Channels linked to this folder — pulls all files dynamically (SELECT-ONLY)
topLevelCampaignUsageDetails array Schedules linked to this folder (SELECT-ONLY)
messageUsageDetails array Sub-channels linked to this folder (SELECT-ONLY)
usageDetails array Content items linked to this folder (SELECT-ONLY)

Root folders: Auto-created per customer per type. Have parentId=null, no name. Cannot be deleted.

⚠️ To create in root: First GET root folder ID (search=parentId%3Dnull), then use that ID as parent. Cannot pass null directly.

Read

GET /api/v2/{deviceGroup|fileFolder}?search=parentId%3Dnull&customerId={id}&select=id   # Root (ADMIN only)
GET /api/contentGroup/?search=parentId%3Dnull&customerId={id}&select=id                 # Root (ADMIN only)
GET /api/v2/{deviceGroup|fileFolder}?search=parentId%3D{id}&select=id,name              # Children
GET /api/contentGroup/?search=parentId%3D{id}&select=id,name                            # Children
GET /api/v2/deviceGroup?search=deviceGroupPath:{id}&select=id,name                      # Recursive
GET /api/v2/fileFolder?search=googleCredentialId!%3Dnull&select=id,name,autoSync        # Synced

deviceGroup stats (recursive — each group includes its subfolders):

GET /api/deviceGroup/getDeviceOnlineOfflineRatio?search=parentId%3D{id}   # All children at once

Returns {groupId: {onlineCount, offlineCount}, ...} for each matching group.

Folder hierarchy (flat list, build tree from parentId):

GET /api/v2/{deviceGroup|fileFolder}?select=id,name,parentId
GET /api/contentGroup/?select=id,name,parentId

Create

deviceGroup:

POST /api/deviceGroup/{parentId}
Body: {"name": "Floor 1"}

contentGroup: (deviceContentType: always use "content")

POST /api/contentGroup/?parentGroupId={parentId}
Body: {"name": "Templates", "deviceContentType": "content"}

fileFolder — type determines endpoint:

Type Endpoint Notes
Regular POST /api/fileFolder/?parentId={parentId}&name={name} No body, query params only
Filtered POST /api/fileFolder/addFilteredFileFolder?parentId={parentId}&name={name}&quickFilterId={qfId} Virtual view using QuickFilter
Google Drive POST /api/fileFolder/addSyncedGoogleDriveFolder?parentId={parentId}&name={name} Requires pre-authenticated GoogleCredential
OneDrive POST /api/fileFolder/addSyncedOneDriveFolder?parentId={parentId}&name={name} Requires pre-authenticated MicrosoftTenant

Synced folder body (Google Drive example):

{
  "googleCredentialId": "cred-uuid",
  "googleFolderId": "google-folder-id",
  "fileTypes": ["IMAGE", "VIDEO"]
}

⚠️ MCP limitation: Creating synced folders requires OAuth-authenticated credentials. Use UI for credential setup, then API for folder creation.

Update

PUT /api/deviceGroup/{id}   {"name": "Renamed"}
POST /api/fileFolder/rename?folderId={id}&newName=NewName
PUT /api/contentGroup/{id}  {"name": "Renamed"}
PUT /api/fileFolder/setAutoSync?folderId={id}&autoSync=true  # Toggle cloud sync
PUT /api/fileFolder/synchronize?folderId={id}                # Trigger manual sync

Move folders/items:

POST /api/deviceGroup/moveToParent/{folderId}/{targetFolderId}   # Move folder (one at a time)
POST /api/v2/device/move  {"deviceIds": [...], "deviceGroupId": "target"}  # Move devices (batch)

POST /api/contentGroup/move                                       # Move folders + items (batch)
Body: {"contentGroupIds": [...], "contentIds": [...], "targetGroupId": "..."}

POST /api/fileFolder/moveBatchToFolder                            # Move folders + files (batch)
Body: {"folderIds": [...], "fileIds": [...], "targetFolder": {"id": "..."}}

Delete

DELETE /api/deviceGroup/{id}?targetGroupId={targetId}  # Devices move to target (default: parent)
DELETE /api/fileFolder/{id}                            # ⚠️ Deletes ALL files recursively!
DELETE /api/contentGroup/{id}?removeContents=true      # removeContents=false moves to root

Teams

Entity attributes:

Field Type Notes
name string Display name (required for CREATE)
comment string Description

Response extras (optional via query params):

Field Param Notes
teamUserNumber includeTeamUserNumber=true Count of users
team{Resource}Number includeTeamResourceNumber=true Counts per resource type

Teams control resource visibility. Resources without team → customer setting determines if visible to all or none.

Resource assignment pattern — applies to Read (query) and Update (modify):

{resourceType} values: user, device, deviceGroup, deviceContent, contentGroup, file, fileFolder, datasource, campaign, message, messageGroup, microsoftTenant, googleCredential, advertiser, customCredential, alertRule, notificationChannel, webhookEventAction, webhookApiKey, deviceInstallRule, quickFilter

Read

GET /api/team/?search=name:Marketing                   # Paged list with search
GET /api/team/{resourceType}Assignments?teamId={id}    # Resources assigned (IDs + readOnly)
GET /api/team/{resourceType}/{resourceId}/assignments  # Teams assigned (IDs + readOnly)

Full entity data via WBQL (when you need more than IDs):
GET /api/v2/device?search=teamAssignments.team.id%3D{teamId}              # Devices in team
GET /api/team?search=teamDeviceAssignments.device.id%3D{deviceId}         # Teams for device

Create

POST /api/team/
Body: {"name": "Marketing Team", "comment": "Content editors"}

Update

PUT /api/team/?teamId={id}
Body: {"name": "Updated Name"}

Assign/unassign resources (from team side):
PUT /api/team/update{ResourceType}Assignments?teamId={id}
Body: {"resourcesToAdd": [{"id": "xxx", "readOnly": false}], "resourceIdsToRemove": ["yyy"]}

Assign/unassign teams (from resource side) — all v1:
POST /api/{resource}/updateTeamAssignments?{resource}Id={id}
Body: {"assignToTeams": [{"teamId": "xxx", "readOnly": false}], "removeFromTeamIds": ["yyy"]}

Exception: user uses email param instead of userId.

Delete

DELETE /api/team/{id}

Tags

Tags are per-customer, organized by type — not standalone entities.

Tag types: DEVICE, CONTENT, CAMPAIGN, MESSAGE, FILE

Format rules: Cannot start with - or whitespace. Allowed: letters, numbers, -_#.". Max 2048 chars total.

Read

GET /api/tag                                           # All tags by type
GET /api/tag?select=deviceTags,campaignTags            # Only specific types
GET /api/tag/suggest?keyword={prefix}&entity=DEVICE    # Autocomplete (startsWith, case-insensitive)

Selectable: deviceTags, deviceContentTags, campaignTags, messageTags, fileTags Response: {"deviceTags": {"lobby": {"count": 5, "comment": "..."}}, ...}

Create

POST /api/tag?tagType=DEVICE
Body: {"tag": "lobby", "comment": "Lobby displays"}

Update

PUT /api/tag?tag=lobby&tagType=DEVICE
Body: {"comment": "Updated description"}

Update tags on entities:

Entity Method Body
Device POST /api/v2/device/tags/{add|set|remove}?search={wbql} ["tag1"]
DeviceContent POST /api/v2/deviceContent/tags/{add|set|remove}?search={wbql} ["tag1"]
File POST /api/v2/file/tags/{add|set|remove}?search={wbql} ["tag1"]
DeviceGroup PUT /api/deviceGroup/{id} {"tags": ["tag1"]}
Campaign PUT /api/campaign/{id} {"tags": ["tag1"]}
Message PUT /api/message/{id} {"tags": ["tag1"]}

Note: /api/v2/campaign and /api/v2/message are read-only endpoints.

Bulk operations: add (append), set (replace all), remove.

Delete

DELETE /api/tag?tag=lobby&tagType=DEVICE

Cannot delete tags still in use.

User

Entity attributes:

Field Type Notes
email string Primary key, unique identifier
name string Display name
role enum ADMIN, OWNER, TECHNICIAN, APPROVER, EDITOR, VIEWER, DEVICE_USER
password string Write-only, min 8 chars. Optional if SSO-only
authProvider enum LOCAL, LDAP, KEYCLOAK, SAML — immutable after creation
editorLevel enum BASIC, ADVANCED, PROFESSIONAL
language string UI language code (e.g., en, de)
comment string Description
address string Physical address
readOnly boolean Read-only flag
restricted boolean If true, sees only team-assigned resources
use2FA boolean Two-factor authentication enabled
forceToSet2FA boolean Must set up 2FA on next login
ssoLoginEnabled boolean SSO login allowed
usernamePasswordLoginEnabled boolean Password login allowed
magicCodeLoginEnabled boolean Passwordless login allowed
oauthPasswordFlowEnabled boolean OAuth password grant enabled
trustedAdmin boolean Elevated admin trust (ADMIN only)
lastLogin datetime Last login time
lastActivity datetime Last activity
createdDate datetime Account creation
hasProfilePicture boolean Profile picture exists (computed)
profilePictureApiPath string Avatar URL (computed)
active boolean Account active (computed)
microsoftSsoEnabled boolean Microsoft SSO connected (computed)
pinCodePresent boolean PIN code set (computed)
customer object .id, .name, .expirationDate, .force2FA — embedded
userInterfaceProfile object .id, .name — UI customization profile

Endpoints: /api/v2/user (read), /api/user (write)

⚠️ Write protection: DELETE, password, 2FA always require sensitive_action. POST only when role is OWNER or ADMIN — other roles use api_write directly. PUT with sensitive fields (password, use2FA, readOnly, restricted, trustedAdmin, role→OWNER/ADMIN) requires sensitive_action.

Read

GET /api/v2/user?select=email,name,role,lastLogin,customer(id,name)
GET /api/v2/user?search=role%3DEDITOR&select=email,name
GET /api/v2/user?search=teamAssignments.team.id%3D{teamId}         # Users in team
GET /api/v2/user?search=restricted%3Dtrue                          # Restricted only
GET /api/v2/user?search=use2FA%3Dtrue                              # 2FA enabled
GET /api/v2/user/me                                                # Current user
GET /api/user/simple?customerId={id}                               # Dropdown list (email+name only)

Create

POST /api/user/?customerId={id}&teamIds={id1},{id2}
Body: {"email": "[email protected]", "name": "John Smith", "role": "EDITOR", "password": "SecurePass1!", "language": "en"}
Field Required Notes
email Unique, valid email format
name Display name
role See role enum above
password * Required unless SSO-only. Min 8 chars
customerId query Required for non-ADMIN
teamIds query Comma-separated team UUIDs

Auto-sends password setup email to new user.

Update

PUT /api/user/?email={email}
Body: {"name": "New Name", "role": "APPROVER", "readOnly": false}

Team assignments:

POST /api/user/updateTeamAssignments?customerId={id}&email={email}
Body: {"assignToTeams": [{"teamId": "xxx", "readOnly": false}], "removeFromTeamIds": ["yyy"]}

Password: POST /api/user/password?oldPassword={old}&newPassword={new}

2FA: POST /api/user/2fa?totp={code}&enable=true

Profile picture: POST /api/v2/user/profilePicture (multipart), DELETE /api/v2/user/profilePicture

Delete

DELETE /api/user/?email={email}
DELETE /api/user/myAccount              # Self-delete

Quick Filters

QuickFilters are saved WBQL queries for dynamic content/device/file selection. Used by channels, system datasources, and filtered folders.

Entity attributes:

Field Type Notes
name string Unique per customer
filteredEntityType enum FILE, CONTENT, DEVICE — immutable after creation
criteria object Filtering logic (see below)
listed boolean Visible in filter dropdowns
custom boolean Custom filter flag
readOnly boolean True if used by campaigns/messages (computed)
campaignUsageDetails object Channels using this filter (SELECT-ONLY)
messageUsageDetails object Sub-channels using this filter (SELECT-ONLY)

Criteria structure (polymorphic by filteredEntityType):

{
  "type": "FILE|CONTENT|DEVICE",
  "search": "WBQL query string",
  "folderId|groupId|deviceGroupId": "folder-uuid",
  "searchRecursively": true,
  "name": "name filter",
  "tags": ["tag1", "tag2"]
}

FILE criteria adds: validFileOnly, isUndefinedValidityValid

Read

GET /api/v2/quickFilter?select=id,name,filteredEntityType,listed
GET /api/v2/quickFilter?search=filteredEntityType%3DDEVICE
GET /api/quickFilter/{id}

Create

POST /api/quickFilter/?customerId={id}&teamIds={id1},{id2}
Body: {
  "name": "Active Promo Content",
  "filteredEntityType": "CONTENT",
  "criteria": {"type": "CONTENT", "search": "tags:promotion", "searchRecursively": true},
  "listed": true
}
Field Required Notes
name Unique per customer
filteredEntityType Cannot change after creation
criteria Must match filteredEntityType

Update

PUT /api/quickFilter/?quickFilterId={id}
Body: {"name": "Updated", "criteria": {...}, "listed": false}

Delete

DELETE /api/quickFilter/?quickFilterId={id}

Cannot delete if used by campaigns, messages, or datasources.

Actions

Actions (WebhookEventActions) define automated responses triggered by external webhook calls. External systems POST to a webhook URL with an eventId, and Wallboard executes the configured action.

Entity attributes:

Field Type Notes
name string Display name
eventId string Webhook trigger ID, unique per customer
enabled boolean Whether action is active
action enum Action type (see categories below)
actionParams map Action-specific key-value parameters
targetData object {"targetIds": ["id1", "id2"]}
targetId string Convenience: single target ID
targetName string Target entity name (computed)

Action type categories:

Category Actions Target
Emergency ENABLE_EMERGENCY_ON_DEVICE{_TAG|_GROUP|_ALL}, DISABLE_... Device(s)
Content control PAUSE_CONTENT_ON_DEVICE{...}, RESUME_..., REFRESH_... Device(s)
Content assign ASSIGN_CONTENT_ON_DEVICE{...}, PREVIEW_... Device(s)
Device control RESTART_DEVICE{...}, WAKE_UP_DEVICE{...}, SNOOZE_DEVICE{...} Device(s)
Display SHOW_TOAST_MESSAGE_DEVICE{...}, CHANGE_VOLUME_DEVICE{...} Device(s)
URL loading LOAD_URL_ON_DEVICE{...} Device(s)
Sensor events SEND_SENSOR_EVENT_TO_DEVICE{...} Device(s)
Datasource REFRESH_DATASOURCE, REFRESH_DATASOURCE_ALL, SET_INTERNAL_DATASOURCE, MERGE_INTERNAL_DATASOURCE, INCREASE_VALUE_IN_DATASOURCE, DECREASE_VALUE_IN_DATASOURCE, INSERT_CAP_DATASOURCE, DELETE_BY_KEY_INTERNAL_DATASOURCE, INSERT_TO_ARRAY_INTERNAL_DATASOURCE, REMOVE_FROM_ARRAY_INTERNAL_DATASOURCE, EMPTY_ARRAY_INTERNAL_DATASOURCE, ROTATE_ARRAY_INTERNAL_DATASOURCE, REPLACE_OR_MERGE_ELEMENT_IN_ARRAY_INTERNAL_DATASOURCE Datasource
Campaign ENABLE/DISABLE_CAMPAIGN{_BY_TAGS}, CHANGE_SATURATION/PRIORITY_CAMPAIGN{_BY_TAGS} Campaign
File/Folder UPLOAD_FILE_TO_FOLDER, FORCE_SYNC_SHARED_FOLDER Folder

Device targeting suffixes: no suffix = specific device, _TAG = by tag, _GROUP = by folder, _ALL = all devices.

Read

GET /api/webhookEvent/actions?customerId={id}
GET /api/webhookEvent/actions/{actionId}
GET /api/webhookEvent/simple?customerId={id}                       # Dropdown (id+name)

Create

POST /api/webhookEvent/actions?customerId={id}&teamIds={id1}
Body: {
  "name": "Emergency Lobby",
  "eventId": "emergency.lobby.on",
  "enabled": true,
  "action": "ENABLE_EMERGENCY_ON_DEVICE_TAG",
  "actionParams": {"tag": "lobby"},
  "targetData": {"targetIds": ["device-group-id"]}
}
Field Required Notes
name Display name
eventId Unique per customer
action Valid WebhookActionType enum

Update

PUT /api/webhookEvent/actions/{actionId}
Body: {"name": "Updated", "enabled": false, "actionParams": {"url": "https://new.com"}}

Delete

DELETE /api/webhookEvent/actions/{actionId}

Alert Rules

Alert rules monitor system conditions and send notifications via configured channels.

Entity attributes:

Field Type Notes
name string Unique per customer
comment string Description
enabled boolean Whether rule is active
@type string Rule type discriminator (see below)
condition object Rule-specific condition (see below)
notificationChannels array Where to send alerts
delayEvaluateAfterViolationMinutes int Delay before re-evaluation
workingDays object Days when rule is active
workingTime object Time window when rule is active

Rule types (@type):

Type Description Key Condition Fields
DeviceOffline Device offline detection inactiveMinutes, deviceGroupId, recursive, excludeDeviceIds, tagFilter
DatasourceError Datasource refresh errors threshold, comparsionOperator
DeviceMetricChanged Device metric changes metric-specific conditions
DeviceStatusChanged Device status changes status-specific conditions

Condition @type values:

Rule Type Condition @type
DeviceOffline DeviceOfflineAndNoActivityForMinutes
DatasourceError DatasourceRefreshErrorCondition
DeviceMetricChanged DeviceMetricChangedCondition
DeviceStatusChanged DeviceStatusChangedCondition

DeviceOffline condition example:

{
  "@type": "DeviceOfflineAndNoActivityForMinutes",
  "inactiveMinutes": 30,
  "deviceGroupId": "root-group-id",
  "recursive": true,
  "tagFilter": {"tags": ["critical"], "logicalOperator": "OR", "tagFilterType": "INCLUDE"}
}

NotificationChannel (separate entity, embedded by reference):

Field Type Notes
name string Unique per customer
channelType enum EMAIL, SMS, PUSH, ALL
notify array Recipients (emails, phone numbers)

Read

GET /api/alertRule?customerId={id}
GET /api/alertRule/{alertRuleId}
GET /api/alertRule/simple?customerId={id}                          # Dropdown (id+name)

Create

POST /api/alertRule?customerId={id}&teamIds={id1}
Body: {
  "name": "Lobby Offline Alert",
  "@type": "DeviceOffline",
  "enabled": true,
  "condition": {
    "@type": "DeviceOfflineAndNoActivityForMinutes",
    "inactiveMinutes": 30,
    "deviceGroupId": "group-id",
    "recursive": true
  },
  "notificationChannels": [{"name": "IT Team", "channelType": "EMAIL", "notify": ["[email protected]"]}],
  "delayEvaluateAfterViolationMinutes": 5
}
Field Required Notes
name Unique per customer
@type Rule type
enabled Active flag
condition Must match @type
notificationChannels At least one channel

Update

PUT /api/alertRule/{alertRuleId}
Body: {"enabled": false, "delayEvaluateAfterViolationMinutes": 10}

Delete

DELETE /api/alertRule/{alertRuleId}

Proof of Play

READ-ONLY reporting entity. Playback statistics collected by devices and aggregated by the Display Stat microservice. No create/update/delete operations.

Base endpoint: /api/v2/proof-of-display

Required params for all queries:

Param Type Notes
customerId number Tenant ID
from number Start timestamp (Unix ms)
timeZone string IANA timezone (e.g., Europe/Budapest)

Optional params:

Param Type Notes
to number End timestamp (defaults to now)
resolution enum HOURLY, DAILY, WEEKLY, MONTHLY, TOTAL
groupBy enum ADVERTISER, ASSET, CAMPAIGN, CONTENT, CONTENT_PAGE, DEVICE, DEVICE_GROUP, MEDIA_FILE, MESSAGE, WIDGET
search string Text filter
sort string Sort field
limit number Max results

Constraints: Max query period 3 months. Data retention 1 year.

Response fields:

Field Type Notes
duration number Display duration (ms)
impressionStartCount number Play start count
impressionEndCount number Completed plays
clickCount number User interactions
viewerCount number Viewer impressions
viewerAttention number Total view time (ms)
viewerAverageAttention number Average attention (ms)
readableDateTime string Human-readable timestamp

Read

By entity type:

GET /api/v2/proof-of-display/device/{deviceId}?customerId={id}&from={ts}&timeZone=Europe/Budapest&resolution=DAILY
GET /api/v2/proof-of-display/content/{contentId}?customerId={id}&from={ts}&timeZone=Europe/Budapest
GET /api/v2/proof-of-display/campaign/{campaignId}?customerId={id}&from={ts}&timeZone=Europe/Budapest
GET /api/v2/proof-of-display/file/{fileId}?customerId={id}&from={ts}&timeZone=Europe/Budapest
GET /api/v2/proof-of-display/message/{messageId}?customerId={id}&from={ts}&timeZone=Europe/Budapest

Aggregated (all entities of type):

GET /api/v2/proof-of-display/device/all?customerId={id}&from={ts}&timeZone=Europe/Budapest
GET /api/v2/proof-of-display/content/all?customerId={id}&from={ts}&timeZone=Europe/Budapest
GET /api/v2/proof-of-display/campaign/all?customerId={id}&from={ts}&timeZone=Europe/Budapest
GET /api/v2/proof-of-display/file/all?customerId={id}&from={ts}&timeZone=Europe/Budapest

Dashboard (top played):

GET /api/v2/proof-of-display/content/dashboard/top/played?customerId={id}&from={ts}&timeZone=Europe/Budapest&limit=10
GET /api/v2/proof-of-display/campaign/dashboard/top/played?customerId={id}&from={ts}&timeZone=Europe/Budapest&limit=10
GET /api/v2/proof-of-display/file/dashboard/top/played?customerId={id}&from={ts}&timeZone=Europe/Budapest&limit=10

CSV export: Append /csv to any endpoint above.

Logs

READ-ONLY audit trail. Runs in a separate microservice (wb-log-service) behind the security gateway. OWNER+ required.

Response fields:

Field Type Notes
time datetime Timestamp
logLevel enum DEBUG, INFO, WARN, ERROR
message string Log message (max 500 chars, trimmed)
userEmail string Acting user
deviceId string Associated device
deviceName string Device name (enriched)
contentId string Associated content
contentName string Content name (enriched)
datasourceId string Associated datasource
datasourceName string Datasource name (enriched)
customerId number Tenant ID
customerName string Tenant name (enriched)

Read

GET /api/v2/log?customerId={id}&page=0&size=20&order=DESC
GET /api/v2/log?customerId={id}&search=error+device&from={ts}&to={ts}
GET /api/v2/log/csv?customerId={id}&search=error                     # CSV export
Param Type Default Notes
customerId number - Required for ADMIN
page int 0 Page number
size int 20 Page size
search string - Full-text search
order enum DESC ASC or DESC by time
from number - Start timestamp (Unix ms)
to number - End timestamp (Unix ms)

Device Metrics

READ-ONLY device monitoring data. Runs in a separate microservice (wb-device-metric-service) behind the security gateway. VIEWER+ required.

Endpoints: /api/deviceStat/{deviceId}/*

Metric type categories:

Category Types
CPU CPU, CPU_LOAD_1, CPU_LOAD_5, CPU_TEMP
GPU GPU, GPU_TEMP, GPU_MEM, FPS
Memory USED_SYS_MEM, USED_VM_MEM, USED_APP_MEM
Storage USED_STORAGE, FILE_COUNT, U_FILE_COUNT
Network NET_APP_RX, NET_APP_TX, NET_SYS_RX, NET_SYS_TX, NET_APP_SERVER_RX, NET_APP_SERVER_TX, REQ_COUNT, SERVER_AVG_REQ, SERVER_PING
System WIFI_SIGNAL_DBM, WIFI_SIGNAL_LEVEL, NET_INTERNET_CONNECTION, BATTERY
Threads NUMBER_OF_THREADS, NUMBER_OF_PROCESSES, GC_COUNT, GC_RUN_AVG
Logs LOG_INFO, LOG_WARN, LOG_ERROR
App events STARTUP_COUNT, CRASH_COUNT, WEB_VIEW_CRASH_COUNT, ANR_COUNT, SYSTEM_ERROR_COUNT, FRONTEND_PROCESS_CRASH_COUNT
Sensors SENSOR_EVENT_COUNT, USER_INTERACTION_COUNT
Status STATUS, RECONNECT

Graph response structure:

{
  "graphs": {
    "CPU": {
      "values": {"1706745600000": 45.2, "1706746200000": 52.1},
      "avg": 48.6, "min": 12.0, "max": 95.3, "sum": 4860.0, "count": 100,
      "noDataIntervals": [{"from": 1706750000000, "to": 1706753600000}]
    }
  }
}

Read

GET /api/deviceStat/{deviceId}/supportedMetrics
POST /api/deviceStat/{deviceId}/graphs?from={ts}&to={ts}&dataPoints=50
Body: {"metricTypes": ["CPU", "USED_SYS_MEM", "FPS"]}
Param Type Default Notes
from number 30 days ago Start timestamp (Unix ms)
to number now End timestamp (Unix ms)
dataPoints number 50 Number of aggregated points (max 1000)

User Interface Profiles

Controls which UI elements are visible for specific users. OWNER+ required.

Entity attributes:

Field Type Notes
id number Auto-generated
name string Profile name
comment string Description
hiddenUIElementRules JSON Rules defining which UI elements to hide
customerId number Tenant ID

Endpoints: /api/v2/userInterfaceProfile

Read

GET /api/v2/userInterfaceProfile?customerId={id}&select=id,name,comment
GET /api/v2/userInterfaceProfile?search=name%3DDefault

Create

POST /api/v2/userInterfaceProfile?customerId={id}
Body: {"name": "Limited Editor", "comment": "Hides admin features", "hiddenUIElementRules": {...}}
Field Required Notes
name Profile name
hiddenUIElementRules JSON rules

Update

PUT /api/v2/userInterfaceProfile/{id}
Body: {"name": "Updated", "hiddenUIElementRules": {...}}

Assign/remove users:

POST /api/v2/userInterfaceProfile/{id}/assign
Body: {"assignUserIds": ["[email protected]", "[email protected]"], "removeUserIds": ["[email protected]"]}

Delete

DELETE /api/v2/userInterfaceProfile/{id}

License Orders

Manages device license purchases. UI calls these "Licenses" (customer view) or "Customer Licenses" (admin view). Backend entity: licenseOrder. Related: licensePackage (admin-only templates defining tiers/pricing).

Entity attributes:

Field Type Notes
id number Auto-generated
orderDate datetime Order date (required)
createDate datetime Creation timestamp (auto-set)
deviceLicenseCount number Number of device licenses
pricePerDevice float Price per device (admin-set)
currency string Currency code
comment string Order notes
approved boolean Approval status (default false)
approveDate datetime When approved (auto-set)
invoiceStartDate datetime Billing start
poNumber string Purchase order number
licenseType enum BASIC, PROFESSIONAL, DBA (legacy), ENTERPRISE, VIDEO_WALL
licensePayType enum SUBSCRIPTION, ONETIME
recurrence object {recurrenceType, frequency} — recurrenceType: DAILY/WEEKLY/MONTHLY/YEARLY
managedInInvoiceSystem boolean External invoicing
invoiceSystemId string External invoice ID
assignedDeviceCount number Devices using this order (computed)
customer object Embedded customer (id, name)
licensePackage object Embedded package (id, name)
approvedByUser object Embedded user who approved
soldByUser object Embedded user who sold

Endpoints: /api/licenseOrder

Approval workflow: Customer creates order → admin reviews → sets approved=true (auto-sets approveDate and approvedByUser).

Read

GET /api/licenseOrder?customerId={id}                              # OWNER+ — full entity
GET /api/licenseOrder/{id}                                         # OWNER+ — full entity
GET /api/licenseOrder/getByCustomer/{customerId}                   # TECHNICIAN+ — customer response
GET /api/licenseOrder/getAllApprovedForCustomer/{customerId}        # TECHNICIAN+ — approved only
GET /api/licenseOrder/csv?customerId={id}                          # ADMIN — CSV export
GET /api/licenseOrder/getUnapprovedLicenseOrderCount                # Unapproved count

getByCustomer/getAllApproved return limited fields: id, licenseType, licensePayType, recurrence, orderDate, comment, deviceLicenseCount, assignedDeviceCount, approved, poNumber.

Create

As customer (limited fields, requires approval):

POST /api/licenseOrder/createAsCustomer?customerId={id}
Body: {"orderDate": "2024-03-15", "licensePackageId": 5, "deviceLicenseCount": 10, "comment": "Q1 expansion", "poNumber": "PO-2024-001"}
Field Required Notes
orderDate Order date
deviceLicenseCount Number of licenses
licensePackageId - Package template
comment - Order notes
poNumber - PO reference

As admin (full fields, can pre-approve):

POST /api/licenseOrder?customerId={id}
Body: {"orderDate": "2024-03-15", "deviceLicenseCount": 10, "pricePerDevice": 5.0, "currency": "EUR", "approved": true, "licenseType": "PROFESSIONAL", "licensePayType": "SUBSCRIPTION", "recurrence": {"recurrenceType": "MONTHLY", "frequency": 1}, "licensePackageId": 5}

Without licensePackageId: licenseType and licensePayType are required. If licensePayType=SUBSCRIPTION, recurrence is also required.

Update

PUT /api/licenseOrder/{id}
Body: {"approved": true, "pricePerDevice": 4.5, "comment": "Approved with discount"}

Delete

DELETE /api/licenseOrder/{id}

Cannot delete if devices are assigned to this order.

Advertisers

Manages advertisers for proof-of-play attribution and reporting. TECHNICIAN+ for write, VIEWER for read. Team-enabled.

Entity attributes:

Field Type Notes
id string (UUID) Auto-generated
name string Max 25 chars, unique per customer
comment string Description
enabled boolean Active status
validity object Schedule: {fromDate, toDate, cron, timeZone}
readOnly boolean True if user lacks write access (computed)

Endpoints: /api/adv

Read

GET /api/adv?customerId={id}
GET /api/adv/{advertiserId}
GET /api/adv/simple?customerId={id}                                # Dropdown (id+name)
GET /api/adv/simplePaged?customerId={id}                           # Dropdown paginated

Create

POST /api/adv?customerId={id}&teamIds={id1},{id2}
Body: {"name": "Coca-Cola", "comment": "Q1 campaign", "enabled": true}
Field Required Notes
name Max 25 chars, unique per customer
enabled - Defaults to false

Update

PUT /api/adv/{advertiserId}
Body: {"name": "Updated", "enabled": false, "validity": {"fromDate": "2024-01-01", "toDate": "2024-12-31"}}

Team assignments:

POST /api/adv/updateTeamAssignments?advertiserId={id}
Body: {"assignToTeams": [{"teamId": "xxx", "readOnly": false}], "removeFromTeamIds": ["yyy"]}

Proof-of-play access:

PUT /api/adv/enableProofOfPlayLinks?advId={id}                     # Generate access token
PUT /api/adv/disableProofOfPlayLinks?advId={id}                    # Revoke access
GET /api/adv/getProofOfPlayLinks?advId={id}                        # Get export URL

Export URL: /public-api/adv/proof-of-play/history/export/csv?token={base64token}

Delete

DELETE /api/adv/{advertiserId}

Workflows

Multi-step guides for complex tasks. Use api_workflows tool to retrieve these.

Note: Workflows describe WHAT to do, not HOW to call the API. Use api_howto for concrete endpoints.

customer-onboarding

Full tenant setup from scratch.

Steps

  1. Get root folders — Query each folder type with parentId not set

    • deviceGroup, fileFolder, contentGroup — each tenant has one root per type
  2. Create folder hierarchy

    • Device folders: locations, departments, etc.
    • Media folders: organize by type, campaign, etc.
    • Content folders: templates, active, archive, etc.
  3. Create teams (if team-based access needed)

    • Teams control resource visibility
    • Assign folders/resources to teams
  4. Pre-register devices (optional, zero-touch deployment)

    • Create device with serialNumber — auto-links on first boot
  5. Create initial content structure

    • Default playlists in content folders
    • Template slides for quick editing

Caveats

  • Root folders have no parentId and no name field
  • ADMIN users must specify customerId for all operations

channel-setup

Create a channel (Campaign) with sub-channels (Messages).

Steps

  1. Create campaignlevel=WIDGET for channels, level=TOP for schedules
  2. Create message group — mandatory wrapper
  3. Create messages — link to content via contentId
  4. Configure device selection — deviceIds, deviceGroupIds, or deviceTagCondition

Caveats

  • Campaign → MessageGroup → Message hierarchy mandatory
  • Content must exist before creating messages

device-troubleshooting

Diagnose and fix device issues.

Steps

  1. Check status — deviceStatus, lastActivity, previewUrl
  2. View screenshotapi_read_image with previewUrl
  3. Check content — Priority: Emergency > Schedule > Base
  4. Send commands — restart, refresh, request logs

Common Issues

  • OFFLINE: Network or app not running
  • Old preview: Try refresh command
  • Wrong content: Check priority

content-publishing

Create and deploy content to devices.

Steps

  1. Create playlistapi_write POST to /api/simpleLoop/

    • Set version: "2.0", simpleLoopType: "NORMAL"
  2. Add pagesmanage_playlist tool

    • add_slide with type: image, video, content, folder, loop
  3. Deploy — Choose method:

    • Direct: api_write PUT device contentId
    • Channel: Create campaign with level=WIDGET, add messages linking to content

Caveats

  • For Slides/Content (interactive), use UI editor — API is read-only for complex layouts
  • Playlists can embed: media, folders, slides, nested playlists

datasource-binding

Connect live data to content widgets.

Steps

  1. Create datasourceapi_write POST to /api/datasource/

    • sourceType: "INTERNAL" for API-managed data
    • structureType: "TABLE" (rows/columns) or "CUSTOM" (free JSON)
  2. Set up datamanage_datasource tool

    • TABLE: create_tableadd_rows
    • CUSTOM: direct JSON structure
  3. Bind widgets — In UI content editor, connect widget properties to datasource fields

  4. Update data — Use manage_datasource (update_rows, add_rows, etc.)

    • Changes propagate to all devices in real-time

Caveats

  • External datasources (RSS, JSON URL, etc.) are read-only — create via UI
  • TABLE supports: string, number, boolean, date, time, dropdown, filePicker, scheduling
  • Use schema action to discover existing table structure