Library

Read, create, edit and lock titles, and manage catalogs, images, versions and rights.

The Library is your titles: films, series, episodes, shorts, songs and web content, each with its metadata, artwork, credits, versions and rights. Every title lives in a catalog you choose. Use these endpoints to mirror titles into your CMS, push corrections back, and keep selected fields under your control.

Rules the server enforces

  • A locked field is immutable to AI, imports and providers. Editing a locked field through the API returns 409 CONFLICT.
  • Age rating, rights and other compliance fields are always decided by a person. AI proposals for them are never applied in bulk. See Enrichment & review.
  • Every title needs a catalog. Creating a title without catalogId or newCatalogName returns 422 CATALOG_REQUIRED. Ingenee never picks one for you.
  • Schedule a version, not a title. A broadcast edit and a theatrical cut are different versions with their own runtime and rating. See Versions.

Sync titles into your system

GET /v1/titles lists titles newest-updated first. Walk it with nextCursor and stop at the first row older than your last run. Pagination & sync has the full loop.

curl "https://dev.app.ingenee.ai/api/v1/titles?limit=100&catalogId=0e578f59-bf28-455f-bc82-f0639a7575fb" \
  -H "Authorization: Bearer $INGENEE_API_KEY"
200 OK (trimmed)
{
  "rows": [
    {
      "id": "1b478b6a-7721-4a94-aade-55c5658f259c",
      "type": "movie",
      "title": "The Long Night",
      "status": "enriched",
      "stateLabel": "Approved",
      "year": 2019,
      "runtimeMinutes": 94,
      "contentRating": "TV-14",
      "language": "en",
      "posterUrl": "https://image.tmdb.org/t/p/w500/…",
      "genres": ["Thriller"],
      "matchConfidence": "hi",
      "catalog": { "id": "0e578f59-bf28-455f-bc82-f0639a7575fb", "name": "Spring acquisitions" },
      "updatedAt": "2026-10-29T18:36:29.063Z",
      "createdAt": "2026-10-29T10:06:44.936Z"
    }
  ],
  "nextCursor": "eyJ1cGRhdGVkQXQiOiIyMDI2LTEwLTE1VDExOjQ1…"
}

Filters: q (title text or any external id), type, status (pending, enriching, enriched, partial, error), catalogId (or none for titles in no catalog), ids (up to 100, comma-separated), year, rating (comma-separated), hasPoster=true, and source (studio, feed). Unknown values for type, status and source are ignored, not rejected.

runtimeMinutes is the length of the file that will air when one has been measured, and the metadata provider's figure otherwise. claimedRuntimeMin is always the provider's figure.

GET /v1/titles/summary returns library counts. GET /v1/titles/duplicates returns groups of likely duplicates (tier is external_id or title_year). Merging duplicates isn't available through the API.

Read one title

curl https://dev.app.ingenee.ai/api/v1/titles/1b478b6a-7721-4a94-aade-55c5658f259c \
  -H "Authorization: Bearer $INGENEE_API_KEY"

The response holds the canonical values at the top level (title, description, tagline, year, runtimeMinutes, contentRating, language, posterUrl, backdropUrl, type, status, catalogId) plus:

KeyContents
locksNames of locked fields
overridesFields a person has set. The AI and providers won't overwrite these
externalIdssource and externalId pairs (TMDB, IMDb …)
genres, keywords, credits, companiesEach genre and credit with its source where known
imagesPosters, backdrops and other art, with isPrimary, language, size
suggestionsPending AI proposals with confidence and policy
pendingPending changes awaiting a decision
aiThemes, moods, tone, audience, summary and content warnings, or null
runtimeWhich source the runtime comes from
usageAirings, shelves and channels using the title
seasonsFor a series, each season with its episodes
catalog{ id, name } or null

Look up before you create

GET /v1/titles/lookup searches metadata providers. Use it to find the TMDB or IMDb id to pass to Create a title.

curl -G https://dev.app.ingenee.ai/api/v1/titles/lookup \
  --data-urlencode "q=The Long Night" --data-urlencode "kind=movie" --data-urlencode "year=2019" \
  -H "Authorization: Bearer $INGENEE_API_KEY"

kind is movie or tv (default movie). year and person (a director or actor) narrow the match. The response is { "source": "tmdb", "results": [...] }; each TMDB result carries a confidence of hi, mid or lo. lowConfidence: true means even the best hit is weak, so create the title manually instead. When TMDB finds nothing, the response comes from the next provider in the chain and adds fallback: true. An explicit source other than tmdb (for example omdb) can return 403 FORBIDDEN if that provider isn't enabled for your platform.

Create a title

Create from a TMDB id, an IMDb id, or manually. The catalog is required: pass an existing catalogId, or a newCatalogName to create one.

curl -X POST https://dev.app.ingenee.ai/api/v1/titles \
  -H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
  -d '{ "source": "tmdb", "kind": "movie", "tmdbId": 603, "catalogId": "0e578f59-bf28-455f-bc82-f0639a7575fb" }'
201 Created
{ "id": "7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11", "title": "The Matrix" }
sourceAlso send
tmdbkind (movie or tv) and tmdbId
imdbimdbId, for example tt0133093
manualtitle (required), and optionally type, year, runtimeMinutes, contentRating, description, language (ISO 639-1)

type is movie, series, episode, short, song or web. A season is created from its series, not here. oembed (a YouTube or Vimeo url) and podcast (an RSS url) also work.

Ingenee enriches the new title straight away, so the call can take a few seconds. A description or contentRating you send on a manual title is recorded as yours and protected from enrichment.

If a title with the same TMDB or IMDb id already exists, the call returns 409 CONFLICT with details.existingId. Add ?force=1 to add it anyway; the response then carries a warning naming the existing title. A near match on title and year does not block: it is added and reported in warning.

Edit fields and lock them

PUT /v1/titles/{id} writes one or more fields. Send only what changes; send null to clear a nullable field.

curl -X PUT https://dev.app.ingenee.ai/api/v1/titles/7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11 \
  -H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
  -d '{ "description": "A hacker learns the world is a simulation.", "contentRating": "R" }'
200 OK
{ "id": "7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11", "updated": ["description", "contentRating"] }

Editable fields: title, description (max 5,000 characters), tagline, year, runtimeMinutes, contentRating, language, posterUrl, backdropUrl, videoUrl and type. An edit is recorded as a person's change, with the old and new value in the title's change history, and enrichment will not overwrite it afterwards. An empty body returns 400 VALIDATION.

To pin a field so that nothing but another explicit edit through an unlock can change it:

curl -X POST https://dev.app.ingenee.ai/api/v1/titles/7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11/lock \
  -H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
  -d '{ "field": "title" }'
200 OK
{ "id": "7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11", "field": "title", "locked": true }

POST …/unlock with the same body releases it. Once title is locked, a PUT that includes title fails:

409 Conflict
{ "error": { "code": "CONFLICT", "message": "Locked field(s) cannot be edited: title", "details": {} } }

Genres, keywords and credits

Each of these replaces the whole list. Each returns 409 CONFLICT while its field (genres, keywords or credits) is locked.

curl -X PUT https://dev.app.ingenee.ai/api/v1/titles/7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11/genres \
  -H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
  -d '{ "genres": ["Science Fiction", "Action"] }'

curl -X PUT https://dev.app.ingenee.ai/api/v1/titles/7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11/credits \
  -H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
  -d '{ "credits": [ { "name": "Lana Wachowski", "role": "director" }, { "name": "Keanu Reeves", "role": "actor", "character": "Neo" } ] }'

Genres take up to 30 names and keywords up to 80. Credits take up to 300 entries, in the order you want them shown. Saving credits also locks credits, so a later provider refresh can't replace your list. Unlock credits before the next edit.

Images

curl -X POST https://dev.app.ingenee.ai/api/v1/titles/7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11/images \
  -H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
  -d '{ "kind": "poster", "url": "https://cdn.example.com/matrix-poster.jpg", "setDefault": true }'

kind is poster, backdrop, logo, title_card, thumb or banner. url is an http(s) URL or an inline image data URL. With setDefault (the default) the image becomes the primary for that kind, and for a poster or backdrop it also becomes the title's posterUrl or backdropUrl, recorded as your choice. Add language for a localized variant, which never replaces the default art. Add "store": true to have Ingenee copy the file into its own storage instead of linking to your host.

The response is { id, kind, isPrimary, url }: 201 for a new image, 200 when the same bytes were already on the title.

CallDoes
POST /v1/titles/{id}/images/{imageId}/primaryMakes an existing image the primary for its kind
POST /v1/titles/{id}/images/{imageId}/deleteRemoves it from the gallery. The title's posterUrl is unchanged
POST /v1/titles/{id}/images/auto-pickPicks the best poster and backdrop. A kind you set by hand is kept and reported locked: true. Add ?language= to choose a language

Versions

A title can carry alternate versions, each with its own label, runtime, rating, locale and captions.

curl -X POST https://dev.app.ingenee.ai/api/v1/titles/7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11/versions \
  -H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
  -d '{ "label": "Broadcast edit", "kind": "broadcast", "runtimeMin": 88, "rating": "TV-14", "locale": "en" }'

kind is alternate, broadcast, extended or dubbed. The call returns 201 with { "id": "…" }. GET /v1/titles/{id}/versions returns versions: the primary version (id: "primary", always present, derived from the title's own runtime and rating) followed by your alternates. Only alternates can be removed, with POST /v1/titles/{id}/versions/{vid}/delete. GET /v1/titles/{id}/media-versions lists the media versions.

Rights

Rights windows say where and for how long a title may be used. Rights are always entered by a person: Ingenee never infers them.

curl -X POST https://dev.app.ingenee.ai/api/v1/titles/7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11/rights \
  -H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
  -d '{ "scope": "AVOD", "mode": "include", "territories": ["US", "Canada"], "licenseStart": "2026-01-01", "licenseEnd": "2027-12-31" }'

scope is Theatrical, SVOD, TV, AVOD, FAST or TVOD. mode is all (worldwide), include (only the listed territories) or exclude (worldwide except the listed). Territories accept country codes, country names and regions such as North America; an include window with no recognizable territory returns 422. The call returns 201 with { "id": "…" }.

GET /v1/titles/{id}/rights returns rollup (state is active, expiring_soon, upcoming, expired or rights_unknown), the territory-by-scope matrix, windows, overrides, and any windows inherited from the catalog. Remove a window with POST /v1/titles/{id}/rights/{windowId}/delete.

Across the library, GET /v1/rights returns { counts, rows } and GET /v1/rights/expiring?days=30 returns { days, count, rows } (days from 1 to 365). A schedule or page that contains a title with expired rights fails its publish gate.

Catalogs

curl -X POST https://dev.app.ingenee.ai/api/v1/catalogs \
  -H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
  -d '{ "name": "Spring acquisitions" }'
201 Created
{ "id": "0e578f59-bf28-455f-bc82-f0639a7575fb", "name": "Spring acquisitions" }

GET /v1/catalogs returns { catalogs: [{ id, name, source, status, rowCount, titleCount, createdAt }] }, excluding archived catalogs. GET /v1/catalogs/{id} adds the catalog's titles (the newest 500). POST …/rename takes { "name": "…" }. POST …/archive hides a catalog from the list but keeps its titles in the Library; POST …/restore brings a deleted catalog and its trashed titles back.

Trash, restore and bulk actions

POST /v1/titles/{id}/trash moves a title to the trash ({ id, trashed: true, opId }). POST /v1/titles/{id}/restore brings it back ({ id, restored: true }). Permanent deletion isn't available through the API.

POST /v1/titles/bulk acts on up to 200 titles:

curl -X POST https://dev.app.ingenee.ai/api/v1/titles/bulk \
  -H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
  -d '{ "action": "move", "ids": ["7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11"], "catalogId": "0e578f59-bf28-455f-bc82-f0639a7575fb" }'

action is trash, reembed or move (catalogId null moves titles out of any catalog). The response is { action, total, ok, failed, opId }; reembed adds a per-title results list.

History, usage and scores

CallReturns
GET /v1/titles/{id}/audit{ events }: who changed what, with old and new values, newest first
GET /v1/titles/{id}/usagekpis and usage: channels airing it, shelves, collections, promotions, rights state
GET /v1/titles/{id}/scoresPriority score and the ratings it is built from
POST /v1/titles/{id}/scores/refreshRe-fetches ratings and recomputes the priority score

Not yet available

  • Creating or editing an individual season or episode. Seasons and episodes come from the series' metadata and appear in GET /v1/titles/{id}.
  • A JSON export of the whole library. Use the CSV export or walk the list.
  • Merging duplicates and permanently deleting titles.

Endpoints

OperationMethod + pathScope
List titlesGET /v1/titleslibrary:read
Library countsGET /v1/titles/summarylibrary:read
Look up a titleGET /v1/titles/lookuplibrary:read
Find duplicatesGET /v1/titles/duplicateslibrary:read
Create a titlePOST /v1/titleslibrary:write
Get a titleGET /v1/titles/{id}library:read
Update a titlePUT /v1/titles/{id}library:write
Lock a fieldPOST /v1/titles/{id}/locklibrary:write
Unlock a fieldPOST /v1/titles/{id}/unlocklibrary:write
Replace genresPUT /v1/titles/{id}/genreslibrary:write
Replace keywordsPUT /v1/titles/{id}/keywordslibrary:write
Replace cast and crewPUT /v1/titles/{id}/creditslibrary:write
Add an imagePOST /v1/titles/{id}/imageslibrary:write
Make an image primaryPOST /v1/titles/{id}/images/{imageId}/primarylibrary:write
Remove an imagePOST /v1/titles/{id}/images/{imageId}/deletelibrary:write
Pick images automaticallyPOST /v1/titles/{id}/images/auto-picklibrary:write
List versionsGET /v1/titles/{id}/versionslibrary:read
Add a versionPOST /v1/titles/{id}/versionslibrary:write
Remove a versionPOST /v1/titles/{id}/versions/{vid}/deletelibrary:write
List media versionsGET /v1/titles/{id}/media-versionslibrary:read
Trash a titlePOST /v1/titles/{id}/trashlibrary:write
Restore a titlePOST /v1/titles/{id}/restorelibrary:write
Bulk actionsPOST /v1/titles/bulklibrary:write
Change historyGET /v1/titles/{id}/auditlibrary:read
UsageGET /v1/titles/{id}/usagelibrary:read
Priority scoreGET /v1/titles/{id}/scoreslibrary:read
Refresh priority scorePOST /v1/titles/{id}/scores/refreshlibrary:write
Title rightsGET /v1/titles/{id}/rightslibrary:read
Add a rights windowPOST /v1/titles/{id}/rightslibrary:write
Remove a rights windowPOST /v1/titles/{id}/rights/{windowId}/deletelibrary:write
Rights across the libraryGET /v1/rightslibrary:read
Rights expiring soonGET /v1/rights/expiringlibrary:read
List catalogsGET /v1/catalogslibrary:read
Create a catalogPOST /v1/catalogslibrary:write
Get a catalogGET /v1/catalogs/{id}library:read
Rename a catalogPOST /v1/catalogs/{id}/renamelibrary:write
Archive a catalogPOST /v1/catalogs/{id}/archivelibrary:write
Restore a catalogPOST /v1/catalogs/{id}/restorelibrary:write

On this page