Schedule a campaign from your own system
Create program entries for a campaign day by day from your marketing calendar, and remove them again by their label.
- For
- Developers who connect a marketing calendar, a shop system, or a campaign tool
- Prerequisite
- a key
ak_…of the class Playout control, a program that your displays play, and a playlist or scene with the campaign - Where
- command line
By the end, one command puts a campaign into a program for a date range, during the hours you choose, and a second command takes it out again. The dates can come straight from your marketing calendar or shop system.
Why one entry per day
A program entry of the type once runs from startsAt to endsAt without a break. A campaign from 2 to 14 November as a single entry would therefore run through every night. It would also push aside the nightly broadcast pause, because a one-off entry beats every recurring one. The script instead creates one one-off entry per day, from opening to closing, and gives them all the same label so that it can find and remove them later.
| Rule | Effect on the campaign |
|---|---|
| A one-off entry beats a recurring one | The campaign wins over the weekly plan during its hours. |
| The shorter window beats the longer one | A short promotion inside the campaign hours still wins over it. |
| If both are equal, the entry created last wins | Two campaigns with the same hours: the newer one plays. |
| Outside every entry, the base program runs | The displays return to normal after closing time. |
Steps
-
Find the IDs. Open the program and the playlist in the portal: the addresses end with their IDs
sch_…andpll_…. -
Save the script:
TypeScript// campaign.ts// node campaign.ts add <sch_…> <label> <pll_…> <from> <to> <opens> <closes>// node campaign.ts remove <sch_…> <label>const API = process.env.SZENERA_API;const KEY = process.env.SZENERA_API_KEY; // class Playout controlconst TIME_ZONE = 'Europe/Berlin'; async function call(method: string, path: string, body?: unknown, idempotencyKey?: string) { const headers: Record<string, string> = { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json', }; if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey; const res = await fetch(`${API}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), }); if (!res.ok) throw new Error(`${method} ${path}: ${res.status} ${await res.text()}`); return res.json();} // A local time on that day with the offset of the time zone, +01:00 in winter and +02:00 in summerfunction at(day: string, time: string): string { const zone = new Intl.DateTimeFormat('en', { timeZone: TIME_ZONE, timeZoneName: 'longOffset' }) .formatToParts(new Date(`${day}T12:00:00Z`)) .find((part) => part.type === 'timeZoneName')?.value ?? 'GMT'; return `${day}T${time}:00${zone === 'GMT' ? '+00:00' : zone.slice(3)}`;} function nextDay(day: string): string { const date = new Date(`${day}T00:00:00Z`); date.setUTCDate(date.getUTCDate() + 1); return date.toISOString().slice(0, 10);} const [action, scheduleId, label, playlistId, from, to, opens, closes] = process.argv.slice(2); if (action === 'add') { for (let day = from; day <= to; day = nextDay(day)) { await call( 'POST', `/v1/schedules/${scheduleId}/items`, { label, content: { kind: 'playlist', playlistId }, recurrence: { kind: 'once', startsAt: at(day, opens), endsAt: at(day, closes) }, }, `campaign-${encodeURIComponent(label)}-${day}`, ); console.log(`added ${day} ${opens} to ${closes}`); }} if (action === 'remove') { const { schedule } = await call('GET', `/v1/schedules/${scheduleId}`); for (const item of schedule.items.filter((entry) => entry.label === label)) { await call('DELETE', `/v1/schedules/${scheduleId}/items/${item.id}`); console.log(`removed ${item.recurrence.startsAt}`); }} -
Add the campaign:
Terminalnode campaign.ts add sch_01KWVAFRM0ZETPAN0000000001 "Herbstaktion" \ pll_01KWV91ZS0PRGRAMM000000001 2026-11-02 2026-11-14 08:00 20:00added 2026-11-02 08:00 to 20:00added 2026-11-03 08:00 to 20:00…The idempotency key is built from label and day. If the script stops halfway, run it again within 24 hours: days that are already there aren't created twice.
-
Remove it when the campaign is canceled or over:
Terminalnode campaign.ts remove sch_01KWVAFRM0ZETPAN0000000001 "Herbstaktion"
Entries that lie in the past don't play, but they stay in the program until someone removes them. A program holds at most 200 entries, so remove a finished campaign by its label.
Check the result
- The weekly plan of the program in the portal shows one block per day with the label of the campaign, see Read the weekly plan.
- During the hours of the campaign, the displays with this program play the playlist.
- After
remove, no entry with this label is left inGET /v1/schedules/{id}.
If something goes wrong
| Symptom | Meaning | Solution |
|---|---|---|
422 Schedule.InvalidRecurrence |
The times don't fit, for example the end lies before the start. | Give opening and closing time as 08:00 and 20:00. |
422 Schedule.ItemLimitReached |
The program already holds 200 entries. | Remove finished campaigns by their label. |
422 Schedule.ContentNotFound |
The playlist ID doesn't exist. | Check the ID in the portal address. |
422 Validation.IdempotencyKeyReused |
The same label and day were sent with other hours within 24 hours. | Remove the campaign, then add it again under a new label. |
| The campaign doesn't appear on a display | Another program runs there, an override is active, or a shorter entry wins. | See How does a display decide what to play?. |