API Documentation

Sign in to access the API integration guide.

Don't have credentials? Request access

Confidential — for integration partners only

← Back to overview
Docs Integration Guide
v1.0
Integration Guide v2.0

API Integration Guide

Integrate DS Templates into your platform. Choose between a native deep integration with your CMS or a standalone white-label deployment.

OAuth 2.0 REST API PostMessage Iframe White-label
Confidential - Integration partners only

This document is confidential and intended for DS Templates integration partners only. The endpoints, data flows, and authentication methods described here can be adapted to your platform's needs. Contact us to discuss the optimal integration approach for your architecture.

Choose Your Integration Option

DS Templates supports two integration approaches. Select the one that best fits your platform.

Option 1: Native Integration

Deep CMS integration with your own media library, branding bridge, and full postMessage protocol. Templates feel completely native within your CMS.

Deep CMS Media bridge Full protocol

Option 2: Standalone Integration

White-label deployment using DS Templates' built-in media library and editor. Simpler integration without CMS media bridging.

White-label Built-in library Simple setup

Feature Comparison

FeatureOption 1: NativeOption 2: Standalone
Media libraryYour CMS media library via postMessage bridgeDS Templates built-in library
Branding / logoInjected from CMS via APIConfigured in DS Templates
Template editorEmbedded iframe with full controlEmbedded iframe, standard
PostMessage eventsFull protocol (editor + media + playback)Basic protocol (editor + playback)
Module configurationIframe-based, token authIframe-based, token auth
White-label domainSupportedSupported
Integration effortHigher - requires media resolutionLower - plug and play
Best forCMS platforms wanting native feelPartners wanting quick deployment

Authentication SHARED

All /api/v1/* endpoints are protected by OAuth 2.0 using the client_credentials grant. You exchange a client_id and client_secret for an access token, then attach that token to subsequent requests as a Bearer token.

Tokens are short-lived (one hour). Refresh tokens are not issued for the client_credentials grant - when an access token expires, request a new one with the same credentials.

Token Endpoint

POST/api/v1/token
Request
Token Request
http
POST /api/v1/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&
client_id=<your_client_id>&
client_secret=<your_client_secret>&
scope=<space_separated_scopes>
ParameterRequiredDescription
grant_typeYesMust be client_credentials
client_idYesThe client identifier issued during provisioning
client_secretYesThe client secret issued during provisioning
scopeNoSpace-separated list of requested scopes. Defaults to templates:read if omitted.
Successful response
200 OK
json
{
    "token_type": "Bearer",
    "expires_in": 3600,
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
    "scope": "templates:read templates:write"
}
Error response
400 Bad Request
json
{
    "error": "invalid_client",
    "error_description": "Client authentication failed",
    "message": "Client authentication failed"
}
error valueCause
invalid_clientWrong client_id or client_secret, or the client is inactive
invalid_grantgrant_type is not supported for this client
invalid_scopeA requested scope is not registered against your client
invalid_requestA required parameter is missing or malformed
Token caching

Tokens are valid for one hour. Implement server-side caching so you are not requesting a new token on every API call. Refresh before expiry (60-second margin). On a 401 response, drop your cached token and request a fresh one before retrying. Never expose client_secret to a browser - the token exchange must happen server-to-server.

Available Scopes

ScopeGranted toPermits
users:createintegrator_adminPOST /api/v1/users/provision
templates:readintegrator_admin, subuserGET /api/v1/templates, GET /api/v1/template-categories, GET /api/v1/modules (integrator_admin); plus GET /api/v1/my-templates (subuser only)
subusers:manageintegrator_userAll /api/v1/subusers/* routes
templates:writesubuserPOST /api/v1/library/external-image, POST /api/v1/library/external-video, POST /api/v1/company-branding/set, and all editor-iframe save routes
Scope strategy

The integrator_admin carries both users:create and templates:read. Catalogue endpoints (/templates, /template-categories, /modules) return the same data for every subuser, so cache the catalogue once at the partner-platform level using your integrator_admin token. /api/v1/my-templates returns per-subuser data and must be called with a subuser token.

Using a token

Bearer token usage
http
GET /api/v1/my-templates HTTP/1.1
Host: templates.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

For embedded routes under /embed/*, you may also pass the token as a ?token=... query parameter on the iframe URL.

Example: full token flow

curl example
bash
# 1. Exchange credentials for an access token
ACCESS_TOKEN=$(curl -s -X POST https://templates.example.com/api/v1/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials' \
  -d 'client_id=01HRX...' \
  -d 'client_secret=s3cr3t...' \
  -d 'scope=templates:read' \
  | jq -r .access_token)

# 2. Call an API endpoint with the token
curl -s https://templates.example.com/api/v1/my-templates \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  | jq .

Subuser Management SHARED

A subuser is the account that ultimately edits templates and owns saved work. Your integrator_user provisions one subuser per end-user in your CMS. All routes require a Bearer token with subusers:manage scope.

Create subuser

POST/api/v1/subusers

Creates a subuser under the calling integrator_user's account. Idempotent on email - re-posting the same email returns the existing subuser's credentials.

Request body
Create Subuser
json
{
    "email": "alice@partner-cms.example",
    "firstName": "Alice",
    "lastName": "Andersson",
    "permissions": {
        "templateEditor": {
            "elementPositioning": true,
            "elementAnimation": true,
            "elementColor": true,
            "elementColorPicker": true,
            "fontSize": true,
            "fontType": true,
            "fontColor": false,
            "fontStyling": false
        }
    }
}
FieldTypeRequiredDescription
emailstringYesUnique identifier for this subuser
firstNamestringNoDefaults to email if omitted
lastNamestringNoDefaults to integrator_admin's client name
permissions.templateEditorobjectNoPer-field toggles. Omitting grants full access. Set fields to false to restrict.
Response - newly created (201)
201 Created
json
{
    "userId": "01HRX...",
    "email": "alice@partner-cms.example",
    "firstName": "Alice",
    "lastName": "Andersson",
    "clientId": "abc123...",
    "clientSecret": "xyz789...",
    "permissions": { /* ... */ },
    "created": true
}

If the email already exists under the same integrator_admin, returns 200 OK with "created": false and the existing credentials.

StatuserrorCause
400email is requiredBody missing or email empty
409email unavailableEmail exists under a different integrator_admin

List subusers

GET/api/v1/subusers

Lists every subuser provisioned under the calling integrator_user. clientSecret is not returned on list.

Response
200 OK
json
{
    "subusers": [
        {
            "userId": "01HRX...",
            "email": "alice@partner-cms.example",
            "firstName": "Alice",
            "clientId": "abc123...",
            "permissions": { /* ... */ }
        }
    ]
}

Get single subuser

GET/api/v1/subusers/{id}

Returns a single subuser by ID. Same shape as list, without clientSecret. Returns 404 if the ID is not under the calling integrator_user.

Update subuser

PATCH/api/v1/subusers/{id}

Updates name and/or editor permissions. Omitted fields are left unchanged. Email cannot be changed.

Request body
PATCH Subuser
json
{
    "firstName": "Alice",
    "lastName": "Andersson-Williams",
    "permissions": {
        "templateEditor": {
            "fontColor": true
        }
    }
}

Sparse update - only included keys are written. Returns 403 if you try to modify yourself, 404 if not found.

Delete subuser

DELETE/api/v1/subusers/{id}

Deletes a subuser and revokes its OAuth client. Returns 204 No Content. Saved templates remain but are no longer reachable by API. Re-creating with the same email does not restore access.

Editor Permissions

The permissions.templateEditor object controls which UI affordances are exposed inside the editor. Defaults are permissive - omit the object for full access.

FieldWhen true, the subuser may
elementPositioningMove and resize elements on the canvas
elementAnimationEdit per-element animation settings
elementColorChange element background colours from the brand palette
elementColorPickerInverted toggle. false exposes the freeform colour picker; true restricts to company palette.
fontSizeChange font size
fontTypeChange font family
fontColorChange font colour from the brand palette
fontStylingToggle bold / italic / underline

Template Library SHARED

Three endpoints power the template-picker and "My Templates" views. All run on a subuser token with templates:read scope.

GET/api/v1/templates

Returns the catalogue of templates grouped by category. The returned editorUrl is the iframe src you load when the user chooses a template.

Query parameters
ParameterTypeRequiredDescription
categorystringNoFilter to a single category by name. URL-encode the value.
Response
200 OK
json
[
    {
        "categoryName": "Information",
        "translations": {
            "nl_NL": "Informatie",
            "en_GB": "Information",
            "de_DE": "Informationen"
        },
        "items": [
            {
                "name": "Office welcome screen",
                "slug": "info_welcome_screen",
                "isRealtime": false,
                "imagePath": "https://common.digitalsignage-templates.com/template_previews/nl/info_welcome_screen.jpg",
                "editorUrl": "https://templates.example.com/embed/template-editor/new/info_welcome_screen",
                "previewUrl": "https://templates.example.com/template/info-welcome-screen/preview"
            }
        ]
    }
]
FieldTypeDescription
categoryNamestringSource label (English). Pass to category filter.
translationsobjectLocalised category labels by BCP-style locale
items[].namestringDisplay name of the template
items[].slugstringStable identifier for the template
items[].isRealtimebooleantrue if auto-refreshes on live screen
items[].imagePathstringThumbnail image URL
items[].editorUrlstringIframe URL for the editor
items[].previewUrlstringStandalone preview URL

Template Categories

GET/api/v1/template-categories

Returns the flat list of category names with translations. Useful for rendering a category picker independently of loading templates.

Response
200 OK
json
[
    {
        "name": "Information",
        "translations": {
            "nl_NL": "Informatie",
            "en_GB": "Information"
        }
    }
]

Saved Templates (My Templates)

GET/api/v1/my-templates

Returns every saved template belonging to the calling subuser. Powers the "My Templates" view.

Query parameters (all optional, AND semantics)
ParameterTypeDescription
searchstringCase-insensitive substring match against name
createdAfterinteger (unix epoch)Templates created after this timestamp
createdBeforeinteger (unix epoch)Templates created before this timestamp
modifiedAfterinteger (unix epoch)Templates modified after this timestamp
modifiedBeforeinteger (unix epoch)Templates modified before this timestamp
Response
200 OK
json
[
    {
        "id": "01HRX...",
        "name": "Reception screen - Q2",
        "aspectRatio": "16:9",
        "url": "https://templates.example.com/template/01HRX...",
        "createdAt": "2026-04-12T08:30:00+00:00",
        "modifiedAt": "2026-05-19T14:02:11+00:00",
        "thumbnailUrl": "https://...",
        "duration": 15,
        "latestVersionId": "01HSY...",
        "versions": [
            { "id": "01HRX...", "modifiedAt": "2026-04-12T08:30:00+00:00" },
            { "id": "01HSY...", "modifiedAt": "2026-05-19T14:02:11+00:00" }
        ]
    }
]

Template Versioning

DS Templates retains every saved version. The id at top level is the root (original); each save creates a new child in versions[]. latestVersionId is always the most recent.

  • For rendering on screens: pass whichever version ID you want to display. Different screens can show different versions.
  • For editing: always pass latestVersionId. The editor clones the version on save, keeping history linear.

Editor Iframe SHARED

The template editor is loaded in an iframe. The editorUrl from the template library response is the iframe source, with the token appended.

URL format
Editor URL
url
https://templates.example.com/embed/template-editor/new/{slug}?token={access_token}
https://templates.example.com/embed/template-editor/edit/{id}?token={access_token}
Embedding example
html
<iframe
    id="template-editor"
    src="https://templates.example.com/embed/template-editor/new/info_welcome_screen?token=eyJ..."
    style="width:100%;height:100vh;border:none"
    sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
></iframe>
Token handling

Set token once on the initial iframe src. The platform validates the token on first request, establishes a session, and uses the session for subsequent requests. For editing existing templates, always use latestVersionId.

Module Iframes SHARED

Each module exposes its own configuration UI as an iframe. Unlike the editor, module iframes do not use postMessage - they are server-rendered pages that the user interacts with directly.

URL contract
Module iframe URL
url
https://templates.example.com/embed/modules/{link}?token=<subuser-token>
Example
html
<iframe
    src="https://templates.example.com/embed/modules/birthday-module/?token=eyJ..."
    sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
></iframe>

Module categories

CategoryDescriptionExamples
simple-credentialShort form for API key or credentials. Validates and shows "Connected" state.Bundeling, SolarEdge, Humly, MDM
manual-entryCRUD UI for user-maintained data: list view, edit form, optional CSV import.Birthday, Calamity, Events, News, Meeting Room, Menu Board, Poll
oauth3-legged OAuth via popup window. Platform handles the OAuth handshake.LinkedIn (available), Google/Microsoft/TeamLeader (coming soon)
No "done" event

Module iframes do not emit a save signal. Configuration is persisted on each in-iframe save. A reasonable UX: open the iframe in a modal, provide a "Close" button outside the iframe, and optionally re-fetch state on close.

PostMessage Reference

Every window.postMessage event exchanged between your parent CMS and the platform's iframes.

Option difference

Option 1 (Native): implement the full protocol - editor, playback, and media resolution.
Option 2 (Standalone): implement editor and playback protocols only. Media is handled by DS Templates internally.

Editor Protocol

Events for the editor iframe at /embed/template-editor/...

Editor to Parent

EventPayloadMeaning
DST_EDITOR_READY{ templateSlug, action }Editor booted. action is "new" or "edit".
DST_FORM_CHANGED{ hasUnsavedChanges, field }Form field changed (debounced 500ms)
DST_SAVE_STARTED{}Save POST dispatched
DST_SAVE_COMPLETE{ templateId, message, media, previewKey }Save succeeded
DST_SAVE_ERROR{ error, errors? }Save failed
DST_FORM_DATA_RESPONSE{ formData, previewKey }Reply to DST_GET_FORM_DATA
DST_FORM_VALIDATION_RESULT{ isValid, errors }Reply to DST_VALIDATE_FORM
DST_FORM_DATA_SET_COMPLETE{}Reply to DST_SET_FORM_DATA
DST_EDITOR_MEDIA_SELECTION_REQUEST{ fileType, allowedFileTypes }User clicked "add image/video". Open your CMS media picker.
DST_EDITOR_OPEN_MODULE_SETTINGS{ moduleLink }User clicked module settings button

Parent to Editor

EventPayloadEffect
DST_EDITOR_SAVE{}Trigger save flow
DST_GET_FORM_DATA{}Request current form state
DST_SET_FORM_DATA{ formData }Restore form state
DST_REQUEST_PREVIEW_UPDATE{}Force preview reload
DST_VALIDATE_FORM{}Run validation without saving
DST_EDITOR_MEDIA_SELECTION_RESPONSE{ fileNameWithExtension, displayName? }User picked a file in your CMS picker

Event listener example

PostMessage handler
javascript
const ORIGIN = 'https://templates.example.com';
const editor = document.getElementById('template-editor');

window.addEventListener('message', (event) => {
    if (event.origin !== ORIGIN) return;

    switch (event.data.type || event.data.msg) {
        case 'DST_EDITOR_READY':
            console.log('Editor ready');
            break;

        case 'DST_SAVE_COMPLETE':
            console.log('Saved:', event.data.templateId);
            refreshUserTemplates();
            break;

        case 'DST_SAVE_ERROR':
            console.error('Save failed:', event.data.error);
            break;
    }
});

// Trigger save from your CMS UI
function triggerSave() {
    editor.contentWindow.postMessage(
        { type: 'DST_EDITOR_SAVE' }, ORIGIN
    );
}

Playback Protocol

For templates in playback mode via /template/{slug}/preview?playlist=true. Without ?playlist=true the template auto-plays standalone.

Template to Parent

msgWhenExtra payload
preRenderFinishedTemplate finished pre-rendering, ready to play-
DST_TEMPLATE_LAZY_LOADPre-rendering state enteredtemplateId, templateName
DST_TEMPLATE_PLAYTemplate started playingtemplateId, templateName, startTime
DST_TEMPLATE_PAUSETemplate pausedtemplateId, templateName
DST_TEMPLATE_RESUMETemplate resumedtemplateId, templateName
DST_TEMPLATE_STOPTemplate stoppedtemplateId, templateName, stopTime, playDuration
DST_TEMPLATE_DURATIONDynamic-duration template reports runtimetemplateId, templateName, duration

Parent to Template (raw strings)

event.dataEffect
'playlistSlideRender'Enter pre-rendering
'initIntroAnimation'Start the template (triggers intro animations)
'exitIntroAnimation'Stop the template

Minimum playback flow

Playback sequence
text
parent: load <iframe src="/template/{slug}/preview?playlist=true" />
iframe -> parent: { msg: "DST_TEMPLATE_LAZY_LOAD" }
iframe -> parent: { msg: "preRenderFinished" }
iframe -> parent: { msg: "DST_TEMPLATE_DURATION", duration: 30 }
parent -> iframe: "initIntroAnimation"
iframe -> parent: { msg: "DST_TEMPLATE_PLAY", startTime: 173... }
... template plays ...
parent -> iframe: "exitIntroAnimation"
iframe -> parent: { msg: "DST_TEMPLATE_STOP", playDuration: 30000 }

Media Resolution Protocol OPTION 1 ONLY

Runs alongside editor and playback protocols when the DOM contains references to external media registered via POST /api/v1/library/external-image or external-video.

Iframe to Parent

EventPayloadMeaning
DST_RENDERER_MEDIA_URL_REQUEST{ fileNameWithExtension }Iframe needs a real URL for a registered filename

Parent to Iframe

EventPayloadEffect
DST_RENDERER_MEDIA_URL_RESPONSE{ fileNameWithExtension, mediaUrl?, mediaBlob? }Provide URL or Blob. If both are sent, mediaBlob wins.

Flow

Media resolution flow
text
iframe -> parent: { type: "DST_RENDERER_MEDIA_URL_REQUEST",
                   fileNameWithExtension: "office.jpg" }
parent -> iframe: { type: "DST_RENDERER_MEDIA_URL_RESPONSE",
                   fileNameWithExtension: "office.jpg",
                   mediaUrl: "https://your-cdn.example/uploads/office.jpg" }
iframe: updates all img.src, video.src, and inline-style references

The iframe caches resolved URLs in memory - duplicate requests for the same filename are deduplicated.

Not required for Option 2

In Standalone mode, DS Templates handles all media internally through its built-in library. You do not need to implement the media resolution protocol. If you see DST_RENDERER_MEDIA_URL_REQUEST or DST_EDITOR_MEDIA_SELECTION_REQUEST events, you can safely ignore them.

Error Handling SHARED

Every JSON endpoint uses the same error shape: { "error": "Human-readable description" }. The HTTP status carries the semantics.

StatusMeaningAction
200OKRequest succeeded
201CreatedNew resource created
204No ContentSuccess, no body (e.g. DELETE)
400Bad RequestFix the request. Show error near the offending field.
401UnauthorizedRefresh token, then retry
403ForbiddenValid token but wrong scope or relationship. Do not retry.
404Not FoundResource does not exist under this caller's scope
409ConflictExistence constraint violated (e.g. duplicate email)
500Server ErrorRetry once after short delay. File a ticket with timestamp.
401 vs 403

401 means "we couldn't identify you" - token is missing, expired, or invalid. 403 means "we know who you are, but you can't do this" - wrong scope or relationship.

Common error messages

These are the canonical error strings the API returns. Treat the HTTP status as the source of truth.

Authentication and authorisation

StatuserrorWhen
401No OAuth client foundToken decoded but no matching OAuth client (revoked, deleted, or never minted)
403OAuth client does not have the required scopesToken valid but the scope this endpoint requires is missing
403Caller is not a provisioned subuserCaller has subusers:manage scope but is not in the integrator_user role chain

Provisioning (POST /users/provision)

StatuserrorWhen
400email is requiredRequest body missing or email field absent

Subuser management

StatuserrorWhen
400email is requiredRequest body missing or email field absent
403Cannot modify yourselfCaller attempted to PATCH the subuser record that issued the current token
403Cannot delete yourselfCaller attempted to DELETE the subuser record that issued the current token
404Subuser not foundThe targeted subuser does not exist under this caller's integrator_user
409email unavailableAnother subuser under the same integrator_admin already uses this email
409Integrator admin has no OAuth clientData-integrity issue - contact support

Library and branding

StatuserrorWhen
400fileName requiredPOST /library/external-image or external-video with no fileName
400Invalid JSON bodyPOST /company-branding/set with a body that is not JSON or not an object

Template editor save

The editor save endpoint wraps errors in a success envelope:

Validation error
json
{
    "success": false,
    "error": "Validation failed",
    "errors": [
        "The first headline must not be empty.",
        "Brand color 1 must be a valid hex code."
    ]
}

Note: the wrapping HTTP status may be 200 - check the success field, not the status. The editor's DST_SAVE_ERROR postMessage passes through the errors array verbatim. A 401 inside the iframe means the token expired and returns server-rendered HTML instead of JSON.

Rendering (GET /template/{id})

StatusBodyWhen
400API templates must be served from a recognized integrator domain.Host does not match a registered integrator host
400trackingId is required for this template.Render URL has no ?trackingId= query parameter

Validation error shapes

Two slightly different shapes depending on context:

JSON-API endpoints (/api/v1/...) - a single error string:

API error
json
{ "error": "email is required" }

Editor-iframe save (/embed/template-editor/...) - JSON wrapped in success envelope (see above).

What to retry vs. surface

OutcomeRetry?Show to end user?
401Yes, after refreshing the token. A bare retry will keep failing.Internal - show "session expired, please reload"
403 (scope)No. Integrator is misconfigured.Internal - file a ticket with DS Templates.
403 (relationship)No. Wrong token used for this surface.Internal - likely a bug in your CMS token routing.
400 (validation)No, fix the request.Yes - show the error string near the offending field.
404No.Yes if user-driven (stale link); silent if internal.
409No, the conflict is durable.Yes - explain in user terms. For email unavailable consider a "use existing" path.
500Yes once, after a short delay. Don't loop.Generic "something went wrong" - file a ticket with timestamp.

Rendering Templates SHARED

Once a subuser has saved a template, embed it on a digital signage screen by loading the rendering URL in an iframe.

Use caseURLAuth
Render saved template for playbackhttps://{integrator-host}/template/{id}None - but must be from recognised host + trackingId
Render catalogue preview by slughttps://templates.example.com/template/{slug}/previewNone (public)

Required: integrator host

Saved templates may only be rendered from a host the platform recognises as one of your integrator domains. A request from any other host returns 400: "API templates must be served from a recognized integrator domain."

Required: trackingId

Every render request must carry a ?trackingId= query parameter - a stable identifier for the device displaying the template (e.g. screen serial number, device UUID). Used for usage counting and deduplication.

  • One trackingId per screen, not per render - reusing is correct and expected
  • Do not generate a fresh ID per render - that inflates counters
  • Do not derive from end-user PII

Recommended: ?playlist=true

Without ?playlist=true, the template auto-plays with no external control. With it, the template enters parent-driven mode: pre-renders, emits preRenderFinished, then waits for 'initIntroAnimation'. See the Playback Protocol section above.