Channels & schedules

Create a FAST channel, generate a schedule, edit it, pass the publish gate and publish.

A channel is a 24/7 linear FAST channel with its own clock, dayparts, content scope and rules. Ingenee builds its schedule in two steps: AI proposes the dayparts and blocks, then a deterministic packer fits titles to the clock, respecting each title's runtime, the repeat window, maturity and rights. A person-editable draft is then published through a blocking validation gate, and only the published version goes to your EPG and playout feeds.

Rules the server enforces

  • The AI never writes timestamps. It plans dayparts; the packer places titles. A channel's AI guidance (schedulingPrompt) shapes dayparts only. It cannot choose which titles air or in what order, and it isn't used at all when any daypart is tied to specific weekdays.
  • Nothing reaches air unreviewed. publish runs the gate first and refuses with 409 BLOCKED while a blocker is open.
  • Hard rules are checked after generation. Maturity, watershed, rights and overlap problems surface as blockers or warnings; the AI can't override them.
  • You schedule a version of a title, with its own runtime and rating.
  • Locked and promoted slots survive regeneration. A locked slot is kept exactly; promotions (FEATURE, STUNT, BOOST, QUEUED) are hard preferences in the next build.

Which clock?

Dates mean different clocks on different calls. generate's startDate, transmission from/to and fill-gaps with a tz use the channel's own timezone. schedule/range from/to, and place and move (day + startMin, minutes after midnight), use UTC. Timestamps in responses are ISO-8601 UTC.

Create a channel

curl -X POST https://dev.app.ingenee.ai/api/v1/channels \
  -H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
  -d '{
    "name": "Ingenee Classic Cinema",
    "archetype": "movie",
    "timezone": "America/New_York",
    "language": "en",
    "dnaPrompt": "Golden-age Hollywood and postwar classics, warm and unhurried",
    "schedulingPrompt": "Lighter fare in the morning, prestige drama in prime time",
    "scheduleHorizon": 14,
    "autoPublish": false,
    "dayparts": [
      { "name": "Morning", "start": "06:00", "end": "12:00", "tone": "light, family-friendly", "rating": "PG" },
      { "name": "Prime", "start": "20:00", "end": "23:00", "tone": "prestige drama" }
    ]
  }'
201 Created
{ "id": "f27a91c3-4b6d-4e08-a5d2-9c1e3b7f0a64" }

name and dnaPrompt (the channel's character, up to 4,000 characters) are required. The rest are optional:

FieldNotes
archetypemovie (default), tv, mv (music video) or mixed. Decides which title types the channel draws on
timezoneAn IANA name such as Europe/Berlin. Without it the clock is derived from region (default US)
scheduleHorizonDays to schedule ahead, 1 to 90 (default 14)
maxRepeatWindowHours before a title may repeat, 1 to 720 (default 72)
primeTimeStart, primeTimeEndHours (defaults 20 and 23)
daypartsUp to 12 of { name, start "HH:MM", end, tone, rating, days }. days is weekdays, 0 = Sunday
schedulingPromptThe AI guidance described above
autoPublishDefault false. See Suggest a schedule and publish it
scopeWhich titles the channel may use: { "mode": "all" | "catalogs" | "tier" | "view" | "collections", "catalogIds": […] }
distributorsDistributor keys to check readiness for. See Distribution readiness
adConfig, fillerSourcesSee Slates & ads

A channel gets a slug made from its name. The slug never changes when you rename the channel, so feed URLs stay stable. Your plan can limit the number of live channels (403 FORBIDDEN).

GET /v1/channels returns { channels: [...] } (add ?archived=1 for archived ones). GET /v1/channels/{id} returns { channel, health } with the full configuration in channel and channel.dna.

Change a channel's configuration

PATCH /v1/channels/{id} changes any create field. Settings that live in the channel's DNA go under dna and are merged, so sending dna.dayparts doesn't reset dna.scope. adConfig is merged field by field too.

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 '{
    "scheduleHorizon": 7,
    "dna": {
      "scheduling": { "mode": "fixed", "blockMin": 30 },
      "scope": { "mode": "catalogs", "catalogIds": ["0e578f59-bf28-455f-bc82-f0639a7575fb"] },
      "exclusions": ["horror"],
      "watershed": 21
    }
  }'
200 OK
{ "id": "f27a91c3-4b6d-4e08-a5d2-9c1e3b7f0a64", "updated": ["scheduleHorizon", "dna"] }

Useful dna keys:

KeyMeaning
scheduling{ "mode": "flexible" | "fixed", "blockMin": 15 | 30 | 60, "exceptions": […] }. Fixed starts programmes on block boundaries; flexible starts each when the last ends
scopeAs at create
dayparts, tiersUp to 12 each
exclusions, includeGenresGenre blacklist and whitelist
familiarityTarget, freshnessTarget0 to 100
contentRatioMix by type, such as { "movie": 70, "series": 30 }. Every value at 0 is refused
watershedHour (0 to 24) before which mature content may not air
territories, territoryModeCoverage, with territoryMode of all, include or exclude
distributorsDistributor keys

Also on the body: name (a clash with another channel is 409 CONFLICT), timezone (or null), language, dnaPrompt, schedulingPrompt, streamUrl, status (idle, building, live, paused) and autoPublish. POST /v1/channels/{id}/archive and /restore take a channel off and back into the live list.

Changing settings that shape a schedule (dayparts, ads, watershed, scope, horizon, scheduling mode) matters for auto-publish. See below.

Suggest a schedule and publish it

Generate

curl -X POST https://dev.app.ingenee.ai/api/v1/channels/f27a91c3-4b6d-4e08-a5d2-9c1e3b7f0a64/schedule/generate \
  -H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
  -d '{ "startDate": "2026-11-02" }'

startDate is optional: a channel-local calendar date, YYYY-MM-DD, not in the past and at most 90 days ahead (otherwise 400 VALIDATION, naming the channel's own dates). The window to rebuild begins at local midnight on that date, and everything the channel already airs outside it is carried into the new version unchanged. Without startDate the build covers the channel's horizon from now, keeping what has already aired. A long horizon can take minutes, so allow a 5-minute client timeout.

201 Created (trimmed)
{
  "versionId": "6b0e1d2c-3a4f-4b5c-8d6e-7f8091a2b3c4",
  "versionNum": 7,
  "itemCount": 412,
  "itemsBuilt": 398,
  "days": 14,
  "distinctTitles": 188,
  "window": { "start": "2026-11-02T05:00:00.000Z", "end": "2026-11-16T05:00:00.000Z" },
  "published": false,
  "changedFromPrevious": true,
  "gapCount": 0,
  "gapMinutes": 0,
  "replayWarnings": [],
  "poolWarnings": [],
  "tierSplit": [],
  "dayparts": [ { "name": "Prime", "startHour": 20, "endHour": 23, "concept": "prestige drama", "allowedRating": null, "days": null } ]
}

The new build is saved as a new version. Read the warnings: replayWarnings (the pool is too small to honor the repeat window), poolWarnings (a starved pool), gaps and gapMinutes (time nothing fit), eventWarnings, and semanticRanking: false (built without AI similarity ranking). If nothing could be scheduled the call returns 422 with the reason, for example an empty scope.

Does it publish itself?

  • If the channel has autoPublish on, your plan includes auto-publish, the build passes the publish gate, and the channel's settings haven't changed since the last published version, the build is published and the response says "published": true.
  • Otherwise the build waits as a draft ("published": false). If auto-publish was blocked because settings changed, the response adds "autoPublishHeld": "settings changed since the published schedule", so a person reviews the effect of the new rules first.

Preview what you built

GET /v1/channels/{id}/schedule/simulate analyses the latest saved version without changing anything and returns totals (airings, distinctTitles, hours, avgSlotMin), titlePlays, genreMix, daypartMix, timeline (per-day airings and hours) and a projection. It takes no parameters. It reads the latest version, so call it after generate. With no schedule yet it returns { "hasData": false }.

Check the gate

curl https://dev.app.ingenee.ai/api/v1/channels/f27a91c3-4b6d-4e08-a5d2-9c1e3b7f0a64/publish-gate \
  -H "Authorization: Bearer $INGENEE_API_KEY"
200 OK (trimmed)
{
  "blockers": [ "2 title(s) scheduled with expired rights — out of licence window: Captain Comet, Harbor Lights." ],
  "warnings": [ "3 airing(s) repeat a title inside the 72h window." ],
  "passing": ["Health OK — Familiarity 62 · Freshness 48"],
  "destinations": [ { "name": "EPG (XMLTV)", "path": "/channels/…/epg.xml", "ready": true } ],
  "counts": { "blockers": 1, "warnings": 1, "passing": 1 },
  "canPublish": false,
  "version": { "versionNum": 7, "itemCount": 412, "coveredDays": 14 }
}

Blockers include: no schedule or an empty one, EPG coverage under 3 days, mature titles before the watershed, airings shorter than their content or overlapping, titles out of licence, off-air gaps, an invalid playout export, and a required slate with no playout asset ID. Warnings include rights expiring within 30 days, repeats inside the window, missing poster art and ad load over the cap.

Publish

curl -X POST https://dev.app.ingenee.ai/api/v1/channels/f27a91c3-4b6d-4e08-a5d2-9c1e3b7f0a64/publish \
  -H "Authorization: Bearer $INGENEE_API_KEY"
200 OK
{ "id": "f27a91c3-4b6d-4e08-a5d2-9c1e3b7f0a64", "status": "live", "versionId": "6b0e1d2c-3a4f-4b5c-8d6e-7f8091a2b3c4" }
409 Conflict
{ "error": { "code": "BLOCKED", "message": "Cannot publish — 1 blocker(s) must be fixed first.", "details": {} } }

Publishing makes the latest version the one your EPG and playout feeds serve, and sends the publish-succeeded webhook. Like the Pages publish, the BLOCKED reply only counts blockers; call publish-gate to read them.

Read the schedule

CallReturns
GET /v1/channels/{id}/scheduleThe latest version (which may be an unpublished draft): version, published (the live version number), adBreaks, and items
GET /v1/channels/{id}/schedule/range?from=2026-11-02&to=2026-11-08The same shape for a date range (YYYY-MM-DD, UTC days, at most 40 days), with days showing which version supplied each day
One entry of items[]
{
  "id": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
  "assetId": "1b478b6a-7721-4a94-aade-55c5658f259c",
  "title": "The Long Night",
  "startsAt": "2026-11-02T01:00:00.000Z",
  "endsAt": "2026-11-02T02:48:00.000Z",
  "slotRuntimeMin": 108,
  "daypart": "Prime",
  "blockLabel": null,
  "isFiller": false,
  "rating": "TV-14",
  "lockId": null,
  "breaks": [ { "kind": "mid", "sequence": 1, "durationSec": 120, "offsetMin": 45, "scteEventType": "SPLICE_INSERT" } ]
}

A bad range returns 400 VALIDATION_FAILED. Days in the past come from the version that was current at the end of that day, which is a reconstruction rather than a transmission record (publishLog: false).

Edit the schedule by hand

Edits apply to the latest version. Publish afterwards for them to reach your feeds. Every edit refuses to overlap or cut a programme: a title is never trimmed to fit, and a clash returns 409 CONFLICT with a message saying by how much.

CallBodyNotes
POST …/schedule/place{ "assetId", "day": "2026-11-02", "startMin": 1200, "episodeId"?, "durationMin"?, "adOverride"? }Adds an airing at a UTC time (snapped to 5 minutes) and locks it. Needs a generated schedule first (409). The airing is at least the title's runtime plus its planned ad time. A repeat of the neighbouring title is allowed and returned as a warning
POST …/schedule/item/move{ "itemId", "day", "startMin", "durationMin"?, "locked"? }Moves an airing; locked: true pins it at the new time and false releases its lock
POST …/schedule/item/remove{ "itemId" }Removes an airing as a new draft version. { removed, undoable, versionNum }
POST …/schedule/lock{ "itemId" }Pins an airing. The next generate keeps it. { lockId }
POST …/schedule/fill-gaps{ "day": "2026-11-02", "tz"?: "America/New_York" }Fills only the empty time on that day, in place, with filler that respects exclusions, scope and rating. { itemsAdded, minutesFilled, gaps }. day is a UTC day unless you send tz
POST …/schedule/clear{ "kind": "range", "from", "to" } or { "kind": "items", "itemIds": […] }Removes airings that start in the range (ISO timestamps) as a new draft. Aired time and locked airings are kept unless includeLocked: true. Send dryRun: true to count first. { removed, keptLocked, keptAired, versionNum, undoable }
POST …/schedule/extend{ "days": 7 }Adds 1 to 90 days after the end of the latest version. A version can span at most 90 days
POST …/schedule/undononeDrops the last hand edit made by remove or clear, restoring the previous version exactly. 409 if the latest version is a full regenerate or the base schedule

All of these are under /v1/channels/{id}/. The place response is { itemId, title, startMin, dur, episode, warning } with status 201.

Versions and rollback

curl https://dev.app.ingenee.ai/api/v1/channels/f27a91c3-4b6d-4e08-a5d2-9c1e3b7f0a64/versions \
  -H "Authorization: Bearer $INGENEE_API_KEY"
200 OK (trimmed)
{
  "channel": { "id": "f27a91c3-…", "name": "Ingenee Classic Cinema" },
  "liveVersionId": "5c9d8e7f-…",
  "publishedAt": "2026-10-29T18:36:29.063Z",
  "versions": [
    { "id": "6b0e1d2c-…", "versionNum": 7, "itemCount": 412, "modelId": "…", "generatedAt": "…", "horizonDays": 14, "isLive": false },
    { "id": "5c9d8e7f-…", "versionNum": 6, "itemCount": 405, "modelId": "…", "generatedAt": "…", "horizonDays": 14, "isLive": true }
  ]
}

The list holds the latest 40 versions of any kind, with the live one marked. POST /v1/channels/{id}/versions/{vid}/restore copies a version's airings into a new draft version; it returns 201 with { versionId, versionNum, restoredFrom, itemCount }. The channel's status becomes building, and the restored version must pass the publish gate again before it is live.

Blocks and promotions

GET /v1/channels/{id}/blocks lists the channel's programming blocks.

A promotion tells the next build to prefer a title, series, collection or genre in a date window:

curl -X POST https://dev.app.ingenee.ai/api/v1/channels/f27a91c3-4b6d-4e08-a5d2-9c1e3b7f0a64/promotions \
  -H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
  -d '{
    "kind": "feature",
    "targetKind": "title",
    "targetRef": "1b478b6a-7721-4a94-aade-55c5658f259c",
    "startsOn": "2026-11-06", "endsOn": "2026-11-06",
    "priority": "high",
    "params": { "feature": { "timeMode": "exact", "start": "21:00" } }
  }'

201 with { "id": "…" }. kind is feature, stunt, boost or queued, and each needs its own params key: feature (timeMode exact or best, start), stunt (start, lengthMin 15 to 1440), boost (tier Heavy or Medium), queued (withinDays 1 to 90). A feature or queued promotion can target a title or series; stunt also a collection; boost also a genre (targetRef is the genre name). Optional: days (0 = Sunday), daypartId (the name of one of the channel's dayparts), priority (high, normal, low), epgLabel (none, new, premiere, season_finale, marathon), notes. The reply to GET …/promotions is { channel, promotions, counts }. A viewer key can read but not add.

Distribution readiness

The distributors you tick on a channel are a list of keys in dna.distributors, set with PATCH. They don't create a per-channel delivery target. What they give you is a readiness check:

curl https://dev.app.ingenee.ai/api/v1/channels/f27a91c3-4b6d-4e08-a5d2-9c1e3b7f0a64/distribution \
  -H "Authorization: Bearer $INGENEE_API_KEY"
200 OK (trimmed)
{
  "channel": { "id": "f27a91c3-…", "name": "Ingenee Classic Cinema", "slug": "ingenee-classic-cinema" },
  "scheduledTitles": 188,
  "partners": [
    { "key": "roku_dpf", "name": "Roku Direct Publisher Feed", "selected": true, "entitled": true, "blockedTitles": 4,
      "requirements": ["IMDb id required", "16:9 hero ≥ 1280×720", "Runtime present"],
      "rejections": [ { "code": "…", "message": "…", "titles": ["…"] } ] }
  ],
  "canonicalFeeds": [ { "label": "EPG (XMLTV 1.3)", "apiPath": "/channels/…/epg.xml" }, { "label": "Schedule (JSON v1)", "apiPath": "/channels/…/schedule.json" } ]
}

Keys that are checked: roku_dpf, samsung_tvplus, jsonld. Other known keys (lg, vizio, pluto, xumo, amazon, plex) are listed but not validated. For each selected, checked distributor, rejections lists what its documented requirements would reject among the titles on this channel's schedule. apiPath values are shown without the /v1 prefix; call them as /v1/channels/{id}/epg.xml and /v1/channels/{id}/schedule.json. See EPG & exports.

Not yet available

  • Per-channel delivery targets. Distributors are a list on the channel, plus the readiness check above.
  • Releasing a single lock on its own, and the "shift following titles" and "replace what's there" placement options. place only drops into a gap, and move with locked: false releases a lock.
  • Webhook events for generation. publish-succeeded is sent on publish.

Endpoints

OperationMethod + pathScope
List channelsGET /v1/channelschannels:read
Create a channelPOST /v1/channelschannels:write
Get a channelGET /v1/channels/{id}channels:read
Update a channelPATCH /v1/channels/{id}channels:write
Archive a channelPOST /v1/channels/{id}/archivechannels:write
Restore a channelPOST /v1/channels/{id}/restorechannels:write
Distribution readinessGET /v1/channels/{id}/distributionchannels:read
List programming blocksGET /v1/channels/{id}/blockschannels:read
List promotionsGET /v1/channels/{id}/promotionschannels:read
Add a promotionPOST /v1/channels/{id}/promotionschannels:write
Suggest a schedulePOST /v1/channels/{id}/schedule/generateschedules:write
Simulate a scheduleGET /v1/channels/{id}/schedule/simulatechannels:read
Get the scheduleGET /v1/channels/{id}/schedulechannels:read
Get the schedule for a rangeGET /v1/channels/{id}/schedule/rangechannels:read
Extend the schedulePOST /v1/channels/{id}/schedule/extendschedules:write
Place a titlePOST /v1/channels/{id}/schedule/placeschedules:write
Move an itemPOST /v1/channels/{id}/schedule/item/moveschedules:write
Remove an itemPOST /v1/channels/{id}/schedule/item/removeschedules:write
Lock an itemPOST /v1/channels/{id}/schedule/lockschedules:write
Fill gapsPOST /v1/channels/{id}/schedule/fill-gapsschedules:write
Clear a rangePOST /v1/channels/{id}/schedule/clearschedules:write
Undo the last editPOST /v1/channels/{id}/schedule/undoschedules:write
Check the publish gateGET /v1/channels/{id}/publish-gatechannels:read
Publish the schedulePOST /v1/channels/{id}/publishschedules:write
List versionsGET /v1/channels/{id}/versionschannels:read
Restore a versionPOST /v1/channels/{id}/versions/{vid}/restoreschedules:write

On this page