Wallboard API - Getting Started (2.0)
Welcome to the Wallboard API documentation. This guide covers the fundamentals you need to get started with the API.
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.
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.
- Get an access token - Use OAuth 2.0 (see Authentication)
- Make API calls - Include
Authorization: Bearer <token>header - Handle responses - All responses are JSON, null values are omitted
API mixes v1 (/api/{resource}) and v2 (/api/v2/{resource}) — always check endpoint in api_howto.
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
customerIdin 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.)
- Null values omitted — don't expect all schema fields
- PUT = partial update — only provided fields change
- Exception:
datafield — full replacement (usemanage_playlist/manage_datasource) - Counting:
size=1&select=id→totalElementsin response
search(WBQL): ANY field including internal (e.g.,teamAssignments)select: Only DTO fields in OpenAPI schema
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.
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"]}
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.
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.
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.
| Param | Default | Description |
|---|---|---|
page |
0 | Page index (zero-based) |
size |
20 | Items per page (max: 1000) |
Response includes totalElements, totalPages for pagination info.
| 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.
| 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.
| 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).
| Symbol | Meaning | Example |
|---|---|---|
, |
AND | status=ONLINE,type=SCREEN |
| |
OR | name=A|name=B |
() |
Grouping | (status=ONLINE|status=OFFLINE),platform=ANDROID |
⚠️ OR binds tighter than AND — a:1|b:2,c:3 = (a:1 OR b:2) AND c:3
| 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).
Nested fields: content.name:welcome, teamAssignments.team.id=abc
Dates: Unix ms — createdAt>1704067200000
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).
| 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
All entities use teamAssignments field:
teamAssignments.team.id={teamId}
teamAssignments.team.id=A|teamAssignments.team.id=B
teamAssignments=NULL
teamAssignments=!NULL
| 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 |
| 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 |
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) |
| 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) |
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
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 = 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
subresellerobject 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.
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:
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
- First query unassigned devices:
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
- Use
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}[¶ms]
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}
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)
typeanddeviceSelectionModeimmutable (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.
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 URLapi_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.
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.
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
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 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.
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
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 (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 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}
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.
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) |
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) |
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}
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.
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}
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.
Full tenant setup from scratch.
Steps
Get root folders — Query each folder type with
parentIdnot set- deviceGroup, fileFolder, contentGroup — each tenant has one root per type
Create folder hierarchy
- Device folders: locations, departments, etc.
- Media folders: organize by type, campaign, etc.
- Content folders: templates, active, archive, etc.
Create teams (if team-based access needed)
- Teams control resource visibility
- Assign folders/resources to teams
Pre-register devices (optional, zero-touch deployment)
- Create device with
serialNumber— auto-links on first boot
- Create device with
Create initial content structure
- Default playlists in content folders
- Template slides for quick editing
Caveats
- Root folders have no
parentIdand nonamefield - ADMIN users must specify
customerIdfor all operations
Create a channel (Campaign) with sub-channels (Messages).
Steps
- Create campaign —
level=WIDGETfor channels,level=TOPfor schedules - Create message group — mandatory wrapper
- Create messages — link to content via contentId
- Configure device selection — deviceIds, deviceGroupIds, or deviceTagCondition
Caveats
- Campaign → MessageGroup → Message hierarchy mandatory
- Content must exist before creating messages
Diagnose and fix device issues.
Steps
- Check status — deviceStatus, lastActivity, previewUrl
- View screenshot —
api_read_imagewith previewUrl - Check content — Priority: Emergency > Schedule > Base
- Send commands — restart, refresh, request logs
Common Issues
OFFLINE: Network or app not running- Old preview: Try refresh command
- Wrong content: Check priority
Create and deploy content to devices.
Steps
Create playlist —
api_writePOST to/api/simpleLoop/- Set
version: "2.0",simpleLoopType: "NORMAL"
- Set
Add pages —
manage_playlisttooladd_slidewith type: image, video, content, folder, loop
Deploy — Choose method:
- Direct:
api_writePUT devicecontentId - Channel: Create campaign with
level=WIDGET, add messages linking to content
- Direct:
Caveats
- For Slides/Content (interactive), use UI editor — API is read-only for complex layouts
- Playlists can embed: media, folders, slides, nested playlists
Connect live data to content widgets.
Steps
Create datasource —
api_writePOST to/api/datasource/sourceType: "INTERNAL"for API-managed datastructureType: "TABLE"(rows/columns) or"CUSTOM"(free JSON)
Set up data —
manage_datasourcetool- TABLE:
create_table→add_rows - CUSTOM: direct JSON structure
- TABLE:
Bind widgets — In UI content editor, connect widget properties to datasource fields
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
schemaaction to discover existing table structure