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
catalogIdornewCatalogNamereturns422 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"{
"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:
| Key | Contents |
|---|---|
locks | Names of locked fields |
overrides | Fields a person has set. The AI and providers won't overwrite these |
externalIds | source and externalId pairs (TMDB, IMDb …) |
genres, keywords, credits, companies | Each genre and credit with its source where known |
images | Posters, backdrops and other art, with isPrimary, language, size |
suggestions | Pending AI proposals with confidence and policy |
pending | Pending changes awaiting a decision |
ai | Themes, moods, tone, audience, summary and content warnings, or null |
runtime | Which source the runtime comes from |
usage | Airings, shelves and channels using the title |
seasons | For 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" }'{ "id": "7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11", "title": "The Matrix" }source | Also send |
|---|---|
tmdb | kind (movie or tv) and tmdbId |
imdb | imdbId, for example tt0133093 |
manual | title (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" }'{ "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" }'{ "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:
{ "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.
| Call | Does |
|---|---|
POST /v1/titles/{id}/images/{imageId}/primary | Makes an existing image the primary for its kind |
POST /v1/titles/{id}/images/{imageId}/delete | Removes it from the gallery. The title's posterUrl is unchanged |
POST /v1/titles/{id}/images/auto-pick | Picks 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" }'{ "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
| Call | Returns |
|---|---|
GET /v1/titles/{id}/audit | { events }: who changed what, with old and new values, newest first |
GET /v1/titles/{id}/usage | kpis and usage: channels airing it, shelves, collections, promotions, rights state |
GET /v1/titles/{id}/scores | Priority score and the ratings it is built from |
POST /v1/titles/{id}/scores/refresh | Re-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
| Operation | Method + path | Scope |
|---|---|---|
| List titles | GET /v1/titles | library:read |
| Library counts | GET /v1/titles/summary | library:read |
| Look up a title | GET /v1/titles/lookup | library:read |
| Find duplicates | GET /v1/titles/duplicates | library:read |
| Create a title | POST /v1/titles | library:write |
| Get a title | GET /v1/titles/{id} | library:read |
| Update a title | PUT /v1/titles/{id} | library:write |
| Lock a field | POST /v1/titles/{id}/lock | library:write |
| Unlock a field | POST /v1/titles/{id}/unlock | library:write |
| Replace genres | PUT /v1/titles/{id}/genres | library:write |
| Replace keywords | PUT /v1/titles/{id}/keywords | library:write |
| Replace cast and crew | PUT /v1/titles/{id}/credits | library:write |
| Add an image | POST /v1/titles/{id}/images | library:write |
| Make an image primary | POST /v1/titles/{id}/images/{imageId}/primary | library:write |
| Remove an image | POST /v1/titles/{id}/images/{imageId}/delete | library:write |
| Pick images automatically | POST /v1/titles/{id}/images/auto-pick | library:write |
| List versions | GET /v1/titles/{id}/versions | library:read |
| Add a version | POST /v1/titles/{id}/versions | library:write |
| Remove a version | POST /v1/titles/{id}/versions/{vid}/delete | library:write |
| List media versions | GET /v1/titles/{id}/media-versions | library:read |
| Trash a title | POST /v1/titles/{id}/trash | library:write |
| Restore a title | POST /v1/titles/{id}/restore | library:write |
| Bulk actions | POST /v1/titles/bulk | library:write |
| Change history | GET /v1/titles/{id}/audit | library:read |
| Usage | GET /v1/titles/{id}/usage | library:read |
| Priority score | GET /v1/titles/{id}/scores | library:read |
| Refresh priority score | POST /v1/titles/{id}/scores/refresh | library:write |
| Title rights | GET /v1/titles/{id}/rights | library:read |
| Add a rights window | POST /v1/titles/{id}/rights | library:write |
| Remove a rights window | POST /v1/titles/{id}/rights/{windowId}/delete | library:write |
| Rights across the library | GET /v1/rights | library:read |
| Rights expiring soon | GET /v1/rights/expiring | library:read |
| List catalogs | GET /v1/catalogs | library:read |
| Create a catalog | POST /v1/catalogs | library:write |
| Get a catalog | GET /v1/catalogs/{id} | library:read |
| Rename a catalog | POST /v1/catalogs/{id}/rename | library:write |
| Archive a catalog | POST /v1/catalogs/{id}/archive | library:write |
| Restore a catalog | POST /v1/catalogs/{id}/restore | library:write |