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 }'
200 OK (trimmed)
{
  "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" } }
    ]
  }'
202 Accepted
{ "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.

200 OK (trimmed)
{
  "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.

GroupKeys
Identitytitle, originalTitle, altTitle, type, year, releaseDate, language, countryOfOrigin
Editorialdescription, shortDescription, logline, tagline, keywords, notes
Classificationgenres, subgenres, contentRating, certificationCountry, collection
Creditscast, director, writer, producer, studio, artist
MediaruntimeMinutes, posterUrl, backdropUrl, logoUrl, stillUrl, videoUrl
External idsimdbId, tmdbId, tvdbId, isrc, houseId
Rightsrights, 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 custom object keyed by your header, for example "custom": { "Internal ref": "HL-0042" }. They are kept against the title and never discarded.
  • noEnrich: true skips provider matching for that row, so the metadata you send stays authoritative.
  • A row with a tmdbId or imdbId is 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" }'
202 Accepted
{ "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.csv

The 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.

QueryEffect
format=xlsxReturns an Excel workbook instead of CSV
locale=esA standalone file with approved translations in place; blank where a field isn't translated yet
locales=es,teAdds approved title and synopsis columns for each language
sheets=es,teOne 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 '{}'
200 OK (trimmed)
{ "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

OperationMethod + pathScope
Validate rowsPOST /v1/imports/validateimports:write
Import rows into a catalogPOST /v1/catalogs/{id}/importsimports:write
Get an import jobGET /v1/imports/{id}imports:write
Stop an import jobPOST /v1/imports/{id}/stopimports:write
Resume an import jobPOST /v1/imports/{id}/resumeimports:write
Preview an MRSS feedPOST /v1/imports/mrss/previewimports:write
Import an MRSS feedPOST /v1/imports/mrssimports:write
Re-sync a catalog's MRSS feedPOST /v1/catalogs/{id}/mrss/syncimports:write
Get a catalog's S3 sourceGET /v1/catalogs/{id}/sourcelibrary:read
Set a catalog's S3 sourcePUT /v1/catalogs/{id}/sourceimports:write
Sync from the S3 sourcePOST /v1/catalogs/{id}/syncimports:write
Export the library as CSVGET /v1/exports/library.csvlibrary:read
Export rightsPOST /v1/exports/rightslibrary:read

On this page