Imports & exports
Bulk-load a catalog from rows, an MRSS feed or an S3 bucket, and export the library and rights.
Use imports to load many titles at once into a catalog you choose. Each row is matched, enriched and added; a row that matches a title you already have is merged into it instead of duplicated. Use exports to hand your library or rights back out as CSV, Excel or JSON.
Rules the server enforces
- Every import lands in a catalog. Create the catalog first, then import into it.
- Imports never overwrite a human decision. A merged row only fills fields that are empty, and never touches a field a person has locked or set.
- AI output stays a proposal. Enrichment that runs after an import creates suggestions in the review queue; nothing long-form is published without a person.
Import rows
Create a catalog
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" }'Check the rows (optional)
POST /v1/imports/validate reports a warning per row and never blocks. Rows without a title are the only ones that
won't import.
curl -X POST https://dev.app.ingenee.ai/api/v1/imports/validate \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{ "rows": [ { "title": "The Long Night", "year": 2019, "imdbId": "tt1234567" } ], "checkUrls": true }'{
"rows": [ { "willImport": true, "warnings": [] } ],
"summary": { "total": 1, "willImport": 1, "wontImport": 0, "rowsWithWarnings": 0, "totalWarnings": 0, "urlsProbed": 0, "urlProbeCapped": 0 }
}checkUrls (default true) also resolves up to 80 distinct URLs per call; private or unresolvable addresses come back as
unreachable-url warnings. Nothing is downloaded.
Start the import
curl -X POST https://dev.app.ingenee.ai/api/v1/catalogs/0e578f59-bf28-455f-bc82-f0639a7575fb/imports \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{
"filename": "spring-2026.csv",
"rows": [
{ "title": "The Long Night", "year": 2019, "type": "movie", "imdbId": "tt1234567",
"genres": "Thriller; Drama", "contentRating": "TV-14",
"rights": [ { "scope": "AVOD", "territory": "US", "licenseStart": "2026-01-01", "licenseEnd": "2027-12-31" } ] },
{ "title": "Harbor Lights", "year": 2021, "type": "series", "custom": { "Internal ref": "HL-0042" } }
]
}'{ "jobId": "5a1d8c52-0b7e-4c3a-8d1f-6e2a9b4c7d10", "total": 2, "status": "processing", "catalogId": "0e578f59-bf28-455f-bc82-f0639a7575fb" }The call returns at once and the rows are processed in the background, one at a time, so a bad row never fails the batch.
Wait for the result
Register an ingest-finished webhook and Ingenee will tell you when the job ends. The event carries
jobId, catalogId, status (done or completed_with_errors), processedRows and errorRows. It is not sent for a
job you stop.
If you can't receive webhooks, poll GET /v1/imports/{jobId} every few seconds until status leaves processing.
{
"id": "5a1d8c52-0b7e-4c3a-8d1f-6e2a9b4c7d10",
"status": "completed_with_errors",
"catalogId": "0e578f59-bf28-455f-bc82-f0639a7575fb",
"catalogName": "Spring acquisitions",
"totalRows": 2, "processedRows": 1, "errorRows": 1, "skippedRows": 0, "stoppedRows": 0,
"percent": 100,
"reviewCount": 1,
"rows": [
{ "rowIndex": 0, "status": "done", "assetId": "7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11", "title": "The Long Night", "error": null },
{ "rowIndex": 1, "status": "error", "assetId": null, "title": null, "error": "Missing title" }
]
}status is processing, done, completed_with_errors or stopped. rows shows only the 12 most recently processed
rows, so use the counts for totals. reviewCount is the number of titles from this catalog with AI proposals waiting for
review. A row's status is pending, processing, done, error, skipped, merged or stopped.
Row format
A row is a JSON object. Use these keys; the API does not map column names for you, so send imdbId, not IMDb ID.
Only title is required.
| Group | Keys |
|---|---|
| Identity | title, originalTitle, altTitle, type, year, releaseDate, language, countryOfOrigin |
| Editorial | description, shortDescription, logline, tagline, keywords, notes |
| Classification | genres, subgenres, contentRating, certificationCountry, collection |
| Credits | cast, director, writer, producer, studio, artist |
| Media | runtimeMinutes, posterUrl, backdropUrl, logoUrl, stillUrl, videoUrl |
| External ids | imdbId, tmdbId, tvdbId, isrc, houseId |
| Rights | rights, an array of { scope, territory or territories, licenseStart, licenseEnd } |
- List fields (
genres,keywords,cast…) are one string with values separated by commas, semicolons or pipes. - Your own columns go in a
customobject keyed by your header, for example"custom": { "Internal ref": "HL-0042" }. They are kept against the title and never discarded. noEnrich: trueskips provider matching for that row, so the metadata you send stays authoritative.- A row with a
tmdbIdorimdbIdis matched exactly instead of searched by title.
Existing titles
A row whose tmdbId/imdbId or whose title and year match a title already in your library is merged, not created
again: its values are stored with the title, and empty canonical fields are filled in. Its status is merged. Rows are
not rejected as duplicates, so re-sending a corrected sheet is safe.
Limits
One call holds at most 100,000 rows; a larger call returns 422 VALIDATION and imports nothing. Trial organizations
have a title cap that can refuse an import before it starts.
Stop and resume
POST /v1/imports/{id}/stop stops a processing job: rows not yet started become stopped, and nothing already
imported is undone. POST /v1/imports/{id}/resume puts them back. Stopping a job that isn't processing, or resuming one
that isn't stopped, returns 409 CONFLICT.
Import an MRSS feed
Point Ingenee at a Media RSS feed and it creates one title per item. No media is downloaded and no paid analysis is started; the item's video URL is stored for later.
curl -X POST https://dev.app.ingenee.ai/api/v1/imports/mrss/preview \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{ "url": "https://cms.example.com/feeds/library.mrss" }'The preview reads the feed and returns feed, itemCount, have (how many items carry a video URL, duration, poster,
genres, rating, credits and episode numbers), alreadyExists, samples (every item read) and warnings. Nothing is
written. Send xml instead of url to preview a pasted document. A link that is slow or very large returns
MRSS_TIMEOUT or MRSS_TOO_LARGE with details.canImportInBackground: true.
curl -X POST https://dev.app.ingenee.ai/api/v1/imports/mrss \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{ "url": "https://cms.example.com/feeds/library.mrss", "newCatalogName": "CMS feed" }'{ "catalogId": "9d2e4b6a-1c3f-4a5b-8c7d-0e1f2a3b4c5d", "sourceSaved": true, "syncing": true, "source": { "url": "https://cms.example.com/feeds/library.mrss" } }Pass catalogId to import into an existing catalog, or newCatalogName to create one; one of them is required. The feed
is read in the background and remembered on the catalog. Re-read it with POST /v1/catalogs/{id}/mrss/sync (also 202;
409 while a sync is running or if the feed is switched off). A pasted xml document imports as a normal job
(jobId in the response) and saves no source, because there is no address to re-read.
By default, new titles from a feed link wait for a person to approve them in Ingenee before they appear in the Library.
Approving them isn't available through the API yet. Plan for imported feed titles to appear in GET /v1/titles only
after someone approves them.
Import from an S3 bucket
Register a bucket on a catalog, then sync it whenever you like.
curl -X PUT https://dev.app.ingenee.ai/api/v1/catalogs/0e578f59-bf28-455f-bc82-f0639a7575fb/source \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{ "sourceType": "s3", "bucket": "acme-delivery", "region": "us-east-1", "prefix": "spring/", "mode": "both", "format": "auto",
"accessKeyId": "AKIAEXAMPLE0000000", "secretAccessKey": "…" }'mode is manifest, media or both; format is auto, csv, json or xml. Ingenee tests the connection when you
save, and a failure returns 400 UPSTREAM with an actionable message (wrong region, bad key, no access). The secret is
stored encrypted and never returned; omit secretAccessKey on a later save to keep it.
GET …/source shows the saved settings (access key masked, hasSecret, lastSyncedAt). POST /v1/catalogs/{id}/sync reads the
bucket and starts an import job: { "jobId": "…", "rows": 120 }, or { "jobId": null, "rows": 0 } when nothing matched.
Poll the job like any other.
Export the library
curl "https://dev.app.ingenee.ai/api/v1/exports/library.csv" \
-H "Authorization: Bearer $INGENEE_API_KEY" -o library.csvThe export lists every non-trashed title with all canonical fields, external ids (one column per source), genres, keywords, credits (one column per role), the AI-derived layer, rights summary and series counts. It is a download, not a paged list.
| Query | Effect |
|---|---|
format=xlsx | Returns an Excel workbook instead of CSV |
locale=es | A standalone file with approved translations in place; blank where a field isn't translated yet |
locales=es,te | Adds approved title and synopsis columns for each language |
sheets=es,te | One workbook with a Master sheet and one sheet per language (up to 40) |
Export rights
POST /v1/exports/rights returns rights as JSON rows, one per rights window. coverage reads Worldwide, Worldwide except …, or the listed territories. Send assetIds (up to 20,000) to limit it
to those titles; an empty body exports everything.
curl -X POST https://dev.app.ingenee.ai/api/v1/exports/rights \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" -d '{}'{ "rows": [ { "title": "The Long Night", "catalog": "Spring acquisitions", "type": "movie", "scope": "AVOD",
"coverage": "US | CA", "territories": "US | CA", "start": "2026-01-01", "end": "2027-12-31",
"state": "active", "source": "manual" } ] }A title with no windows appears once with an empty scope and state: "rights_unknown".
Not yet available
- A JSON export of the library (only CSV and Excel).
- Approving feed titles that are waiting for approval.
- Importing into a brand-new catalog from rows in one call. Create the catalog first, then import.
Endpoints
| Operation | Method + path | Scope |
|---|---|---|
| Validate rows | POST /v1/imports/validate | imports:write |
| Import rows into a catalog | POST /v1/catalogs/{id}/imports | imports:write |
| Get an import job | GET /v1/imports/{id} | imports:write |
| Stop an import job | POST /v1/imports/{id}/stop | imports:write |
| Resume an import job | POST /v1/imports/{id}/resume | imports:write |
| Preview an MRSS feed | POST /v1/imports/mrss/preview | imports:write |
| Import an MRSS feed | POST /v1/imports/mrss | imports:write |
| Re-sync a catalog's MRSS feed | POST /v1/catalogs/{id}/mrss/sync | imports:write |
| Get a catalog's S3 source | GET /v1/catalogs/{id}/source | library:read |
| Set a catalog's S3 source | PUT /v1/catalogs/{id}/source | imports:write |
| Sync from the S3 source | POST /v1/catalogs/{id}/sync | imports:write |
| Export the library as CSV | GET /v1/exports/library.csv | library:read |
| Export rights | POST /v1/exports/rights | library:read |