Slates & ads
Configure slates and fallbacks, ad breaks and inventory, in-house campaigns, interstitials and rescue playlists.
This guide covers everything that fills the time around programmes on a FAST channel: what plays when something goes wrong (slates and fallbacks), where ad breaks fall, which ads fill them (campaigns), and the promos, bumpers and filler clips that fill junctions (interstitials).
What Ingenee does and doesn't do
- Ingenee plans, your playout plays. It stores slate and fallback references, plans the breaks and describes the cue points. It emits no SCTE-35 packets and never calls or validates a VAST tag; your playout or SSAI layer does the inserting.
- Nothing reaches air unreviewed. A required slate with no playout asset ID blocks the channel's publish gate, and campaign spots are placed into a copy of the published schedule that goes through the gate again.
- Viewers read, programmers and admins write. A
viewerkey gets403 FORBIDDENon slate, ad-inventory, ad-source and campaign changes.
Slates and fallbacks
A channel has four scenarios. For each you choose a slate (a Library title, a playout asset ID, or both) and a fallback rule.
scenario | Meaning |
|---|---|
unfilled | The part of an ad break no campaign fills |
missing | A scheduled programme or ad file is missing or unplayable |
failure | Playback of an asset fails while on air |
gap | Time with no scheduled event |
fallbackRule | Meaning |
|---|---|
fill_break | Fill the rest of the break |
until_next_programme | Play until the next programme |
switch_to_slate | Switch to the slate |
loop_until_next_event | Loop until the next event |
skip | Skip and carry on |
curl https://dev.app.ingenee.ai/api/v1/channels/f27a91c3-4b6d-4e08-a5d2-9c1e3b7f0a64/slates \
-H "Authorization: Bearer $INGENEE_API_KEY"GET always returns all four scenarios, saved or not:
{
"rows": [
{ "scenario": "unfilled", "slateAssetId": null, "playoutAssetRef": "SLATE-BE-RIGHT-BACK", "fallbackRule": "fill_break",
"fallbackAfterSec": null, "required": false, "slateTitle": null, "ref": "SLATE-BE-RIGHT-BACK", "refSource": "playout", "state": "mapped" },
{ "scenario": "gap", "slateAssetId": null, "playoutAssetRef": null, "fallbackRule": "switch_to_slate",
"fallbackAfterSec": 5, "required": true, "slateTitle": null, "ref": null, "refSource": null, "state": "blocker" }
],
"chip": { "kind": "blocks", "count": 1, "label": "1 blocks export" }
}PUT saves the scenarios you send (1 to 4, one row per scenario) and returns the same view. Unknown keys are rejected.
curl -X PUT https://dev.app.ingenee.ai/api/v1/channels/f27a91c3-4b6d-4e08-a5d2-9c1e3b7f0a64/slates \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{ "rows": [
{ "scenario": "unfilled", "slateAssetId": null, "playoutAssetRef": "SLATE-BE-RIGHT-BACK", "fallbackRule": "fill_break", "fallbackAfterSec": null, "required": false },
{ "scenario": "gap", "slateAssetId": "5e7f9a1b-2c3d-4e5f-8a6b-7c8d9e0f1a2b", "playoutAssetRef": "SLATE-OFF-AIR", "fallbackRule": "switch_to_slate", "fallbackAfterSec": 5, "required": true }
] }'slateAssetIdis a title in your Library (ornull); another organization's or a trashed title returns422.playoutAssetRefis the ID your playout system uses for the slate (up to 200 characters). It is what exports carry.fallbackAfterSecis 0 to 3600, ornull.stateismapped(a playout ID resolves),blocker(required but no ID),unmapped(optional, a Library slate chosen but no ID) orunset. Ablockerrow blocks publishing with aslate_unmapped_requiredmessage.
Saved slates appear in the channel's XMLTV as ingenee-slate elements and in schedule.json as slates.
Ads on a channel
A channel's ad rules live in dna.adConfig, set with PATCH /v1/channels/{id}. You can send only the keys you want to change;
they are merged over the stored config.
curl -X PATCH https://dev.app.ingenee.ai/api/v1/channels/f27a91c3-4b6d-4e08-a5d2-9c1e3b7f0a64 \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{ "dna": { "adConfig": {
"enabled": true,
"accounting": "included",
"source": "campaigns",
"avgViewersPerBreak": 1200,
"spotSec": 30,
"cueMethod": "scte35",
"preRoll": { "on": true, "pods": 1 },
"midRoll": { "on": true, "pods": 2, "everyMin": 15, "minTailMin": 5, "pattern": "cadence" },
"scte": { "spliceCommand": "time_signal", "segmentationType": "provider_po", "cuePreRollMs": 4000, "autoReturn": true }
} } }'| Key | Values and meaning |
|---|---|
enabled | false means no breaks: the slot is the content |
accounting | included reserves the ad time inside the slot. playout leaves ad time to your playout system, so breaks are exported as zero-length cue points and no time is reserved |
source | Who fills a break. none: exported as an avail. campaigns: your own campaigns' videos. vast: an outside ad server fills it; the break goes out as slate |
vastTagUrl | For vast: an https:// URL up to 2,048 characters. Stored and passed on, never called |
avgViewersPerBreak | Whole number, 0 to 100,000,000. The audience campaign forecasts multiply by. null means no views figure is shown |
inventoryMode | schedule (default) uses the breaks of the schedule Ingenee builds. estimate uses the manual grid below |
fillMode | fixed (default): each ad plays at most the spot length. full: each plays its whole measured length and breaks are packed by seconds |
spotSec | Spot length pods are sized from, 5 to 120 |
preRoll, midRoll, postRoll | { on, pods }, pods being spots per pod (0 to 10). midRoll also takes everyMin, minTailMin, firstBreakAfterMin, maxBreaks, pattern (cadence, fixed, natural) and fixedPositionsMin |
podPattern | Explicit spot lengths in every pod, such as [30, 30, 15] |
cueMethod | scte35, scte104, manifest or none |
scte | spliceCommand (splice_insert, time_signal), segmentationType, cuePreRollMs, autoReturn |
interProgramSec, minProgramMin | Seconds between programmes; programmes shorter than this get no pods |
daypartCaps | Ad minutes per clock hour, for overnight and daytime: { "on": true, "capMinPerHr": 12 } |
rescue | What playout does when a break can't be filled: onFail is house_ads (the channel's own house ads, optionally houseAdIds), extend or black, with triggerSec and repeatProtectHrs |
Refusals come back as 422 VALIDATION with a plain sentence, for example "Ad time on this channel is managed by playout, so it
has no break time for campaigns. Set Ad time to "Included in the schedule" first." Changing source, the VAST tag or the
fill settings needs a programmer or admin key, and is recorded in the audit history. For auto-publish, a change to the break rules
(accounting, pods, cues, caps) counts as a settings change; source, vastTagUrl, avgViewersPerBreak, inventoryMode and the
fill settings don't.
Ad breaks
GET /v1/channels/{id}/ad-breaks reads the breaks of the published schedule, the same ones the exports carry. Ask for
one channel-local day, or a window of hours from now:
curl "https://dev.app.ingenee.ai/api/v1/channels/f27a91c3-4b6d-4e08-a5d2-9c1e3b7f0a64/ad-breaks?day=2026-11-02" \
-H "Authorization: Bearer $INGENEE_API_KEY"day is YYYY-MM-DD on the channel's clock; hours is 1 to 168 (default 24). Sending both returns 422. The response has
window, days, tzLabel, counts, breaks and warnings. Each break lists its start and the ads placed in it. Nothing
here builds or reorders anything.
Ad inventory (for channels scheduled elsewhere)
When a channel's schedule is built outside Ingenee, give campaigns something to forecast against: a weekly grid of ad slots
per hour. It is read only while the channel's inventoryMode is estimate.
curl -X PUT https://dev.app.ingenee.ai/api/v1/channels/f27a91c3-4b6d-4e08-a5d2-9c1e3b7f0a64/ad-inventory \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{ "windows": [ { "weekday": 5, "startMin": 1200, "endMin": 1380, "slotsPerHour": 4 } ] }'{ "mode": "estimate", "windows": [ { "weekday": 5, "startMin": 1200, "endMin": 1380, "slotsPerHour": 4 } ], "weeklySlots": 12, "spotSec": 30 }weekday is 0 (Sunday) to 6. startMin and endMin are minutes of the day, and a window can't cross midnight (split it in two).
slotsPerHour has at most two decimals and must fit in an hour at your spotSec. A PUT replaces the whole grid
(up to 200 windows); overlaps are refused with 422. GET returns the same view.
Campaigns
A campaign is a named group of your own Library videos, in priority order, that plays inside a channel's ad breaks instead of
the slate. It needs a channel with ads enabled, accounting of included and source of campaigns; otherwise saving
returns 422 naming the channel and the fix.
curl -X POST https://dev.app.ingenee.ai/api/v1/campaigns \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{
"name": "Autumn house promo",
"advertiser": "Ingenee",
"startDate": "2026-11-01",
"endDate": "2026-11-30",
"openEnded": false,
"channelIds": ["f27a91c3-4b6d-4e08-a5d2-9c1e3b7f0a64"],
"maxPerBreak": 1,
"items": [
{ "assetId": "5e7f9a1b-2c3d-4e5f-8a6b-7c8d9e0f1a2b", "targetCount": 4, "targetPer": "day", "cpm": 6.5 }
]
}'{ "id": "0f1e2d3c-4b5a-4968-8776-655443322110" }The response also has an applied field reporting what happened when Ingenee applied the change to the channel schedules.
- Dates. A start date is required. Pick an
endDate, or setopenEnded: true(an end date ofnullwithout it is refused). - Items. Up to 50 videos, each from your Library with a measured length.
targetCount(1 to 999) plays pertargetPer:day,week,airing(inside each airing of the programme you name inprogrammeAssetId) ortotal(needs an end date). Optional per item:daypart,playMode(channel,full,cutwithcutSec),positionInBreak(any,first,last),adName,guaranteed. - Money.
moneyModeliscpm(the default; every item needs acpmfrom 0 to 10,000) orcontract(then sendcontractValue).currencydefaults toUSD. Results are reported as estimated value, not revenue. - Daypart.
daypartlimits weekdays and local-time windows. - Status is derived from the dates:
running,scheduledorended. - A change replaces the whole campaign:
PATCH /v1/campaigns/{id}takes the same body as create (name, dates, channels, items).
GET /v1/campaigns returns { campaigns, channels, counts } (filter with ?status=running|scheduled|ended).
DELETE /v1/campaigns/{id} removes one. Saving or deleting a campaign applies the change to the channels' schedules.
Approve, apply and check
POST /v1/campaigns/{id}/approvelocks the campaign's spots that are already in the schedule on air and still to come. Re-adding to the schedule or changing the allocation rule will not move them. Changing its videos, spot counts, days and times, dates or channels clears the approval. Approving publishes nothing. It returns{ id, approvedAt }.POST /v1/channels/{id}/apply-campaignsputs the campaigns' videos into the breaks of the schedule already on air, without changing any programme. Ingenee copies the published version exactly, places the spots in the copy, and publishes it through the gate. It returns409with a reason if the channel has no published schedule, no breaks, ads off, or is in estimate mode.GET /v1/campaigns/{id}/airs?channel={channelId}&day=2026-11-02lists the published breaks on a day that carry the campaign's ads (channelis required;daymust fall in the next seven days).GET /v1/campaigns/forecastforecasts delivery for all campaigns. Add?campaign={id}to focus on one, and?rule=priority|value|shareto see the result under another allocation rule without changing it.
Interstitials
Interstitials are the promos, channel IDs, bumpers, trailers, filler clips, loops and house ads that fill junctions. By default a channel uses only its own interstitials, never another channel's.
curl -X POST https://dev.app.ingenee.ai/api/v1/interstitials \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{ "title": "Ingenee Classic Cinema ID", "category": "channel_id", "duration": "00:00:10", "rating": "G",
"channelId": "f27a91c3-4b6d-4e08-a5d2-9c1e3b7f0a64", "houseNumber": "ICC-ID-01", "mediaPath": "ids/icc-id-01.mxf" }'200 with { "id": "…" }. title and a duration are required.
| Field | Notes |
|---|---|
duration or durationSec | Whole seconds (30) or a timecode (00:00:30 or 00:00:30:00). 01:30 is refused as ambiguous |
category | promo, channel_id, bumper, trailer, filler, loop or house_ad. Blank means unclassified; an unknown label is refused, never guessed |
rating | G, PG, PG-13, R, NC-17, TV-Y, TV-Y7, TV-G, TV-PG, TV-14, TV-MA or 18+. Compact forms like pg13 are accepted |
houseNumber | Your reference. Unique within your organization (409 CONFLICT if taken) |
channelId | Assign to a channel; leave out to keep it organization-wide |
mediaPath, som | The media file reference, and the start of media as HH:MM:SS:FF. Media can arrive after the record; the list marks it mediaPending |
validFrom, validTo | YYYY-MM-DD availability window |
spotType | "", ZS or BLK |
description, notes | Free text |
GET /v1/interstitials returns { interstitials: [...] }, each with mediaPending, usedCount and its channel rule.
Filter with channel (a channel id, or none), category, rating, q, validOn and media (pending or ready).
PATCH /v1/interstitials/{id} edits any field, and "archived": true or false archives or restores one. A malformed field
returns 400 VALIDATION_FAILED.
Rescue playlists
A rescue playlist is a named list of Library titles with a total runtime. What a channel does when an ad break can't be
filled is set separately, in adConfig.rescue above, and uses the channel's house ads.
curl -X POST https://dev.app.ingenee.ai/api/v1/rescue-playlists \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{ "name": "Evergreen shorts", "description": "Safe 5-minute fillers", "assetIds": ["5e7f9a1b-2c3d-4e5f-8a6b-7c8d9e0f1a2b"] }'201 with { "id": "…" }. GET /v1/rescue-playlists returns { playlists: [{ id, name, description, itemCount, runtimeMin, createdAt }] }.
Not yet available
- Changing the organization-wide campaign allocation rule, and reordering campaigns, through the API.
- Un-approving a campaign, and editing or deleting a rescue playlist.
- Creating interstitials in bulk or from CSV (create them one at a time), and setting a channel's per-interstitial usage rules.
Endpoints
| Operation | Method + path | Scope |
|---|---|---|
| Get slates and fallbacks | GET /v1/channels/{id}/slates | channels:read |
| Select slates and fallbacks | PUT /v1/channels/{id}/slates | channels:write |
| Update a channel (ad configuration) | PATCH /v1/channels/{id} | channels:write |
| Ad breaks on a channel | GET /v1/channels/{id}/ad-breaks | ads:read |
| Ad inventory | GET /v1/channels/{id}/ad-inventory | ads:read |
| Set ad inventory | PUT /v1/channels/{id}/ad-inventory | ads:write |
| Place campaign spots into breaks | POST /v1/channels/{id}/apply-campaigns | ads:write |
| List campaigns | GET /v1/campaigns | ads:read |
| Create a campaign | POST /v1/campaigns | ads:write |
| Get a campaign | GET /v1/campaigns/{id} | ads:read |
| Update a campaign | PATCH /v1/campaigns/{id} | ads:write |
| Delete a campaign | DELETE /v1/campaigns/{id} | ads:write |
| Approve a campaign's spots | POST /v1/campaigns/{id}/approve | ads:write |
| Where a campaign aired | GET /v1/campaigns/{id}/airs | ads:read |
| Forecast campaign delivery | GET /v1/campaigns/forecast | ads:read |
| List interstitials | GET /v1/interstitials | ads:read |
| Add an interstitial | POST /v1/interstitials | ads:write |
| Update an interstitial | PATCH /v1/interstitials/{id} | ads:write |
| List rescue playlists | GET /v1/rescue-playlists | channels:read |
| Create a rescue playlist | POST /v1/rescue-playlists | channels:write |