Enrichment & review
Confirm metadata matches, re-run enrichment, decide AI suggestions and approve translations.
When a title is created or imported, Ingenee enriches it automatically: it matches the title to a metadata provider, fills in missing fields, and adds AI-derived themes, moods, keywords and an embedding for search. Anything the AI produces is a proposal. These endpoints let your system confirm matches, trigger a re-run, and accept or reject what the AI proposed.
Rules the server enforces
- AI proposes, a person decides. Long-form prose such as a synopsis is never published automatically, whatever the confidence.
- Some fields are always decided one at a time. Age rating, content advisories, territory, license and availability are never accepted in bulk.
- Locked and person-set fields are immutable to enrichment. A re-run keeps them, and a pending change to a locked field can't be accepted.
- Your API key is the person. A decision made through the API is recorded in the audit history under the key's creator, together with the key.
Confirm a metadata match
Enrichment auto-attaches a provider match only when it is confident. When it isn't, the title waits for a decision with ranked candidates.
curl https://dev.app.ingenee.ai/api/v1/titles/7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11/match \
-H "Authorization: Bearer $INGENEE_API_KEY"{
"decision": "refused",
"confidence": "mid",
"reason": "year differs",
"candidates": [
{ "tmdbId": "603", "title": "The Matrix", "year": 1999, "confidence": "mid", "kind": "movie", "why": "year differs" }
],
"rejectedIds": []
}Then decide. To accept, send one of the listed candidates' ids; to reject, send reject (optionally with the tmdbId you reject).
curl -X POST https://dev.app.ingenee.ai/api/v1/titles/7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11/match \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{ "action": "accept", "tmdbId": "603" }'Accepting an id that isn't one of the candidates returns 422, and deciding a title that has no refused match returns
409 CONFLICT. Only films and TV titles take a TMDB match.
Name the title yourself
If the right title isn't among the candidates, say which film or series this is. Ingenee checks the id against TMDB before it writes anything, so a typo can't enrich a title from the wrong film.
curl -X POST https://dev.app.ingenee.ai/api/v1/titles/7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11/identity \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{ "tmdbId": "603", "imdbId": "tt0133093" }'{ "id": "7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11", "tmdbId": "603", "imdbId": "tt0133093", "verifiedAs": "The Matrix", "verifiedYear": 1999 }Send null to clear an id, which also undoes what that id had written. Replacing an id does the same for the old one. Songs
and web titles don't take a TMDB id (409 CONFLICT). An id TMDB doesn't know returns 422.
Re-run enrichment
POST /v1/titles/{id}/reembed runs enrichment again for one title: metadata providers, AI themes, moods and keywords, and
the embedding. Locked and person-set fields are kept. It runs while you wait and can take several seconds.
curl -X POST https://dev.app.ingenee.ai/api/v1/titles/7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11/reembed \
-H "Authorization: Bearer $INGENEE_API_KEY"{ "id": "7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11", "reembedded": true }reembedded: true means the pass ran, not that every step succeeded. Read the title afterwards and check status
(enriched, partial or error) and lastEnrichedAt. For many titles, use POST /v1/titles/bulk with
"action": "reembed" and up to 200 ids; the response lists each id with ok or an error.
Decide AI suggestions
GET /v1/review groups pending AI suggestions by field and value, across all titles:
{
"summary": { "proposals": 42, "titles": 18, "groups": 9, "auto": 12, "high": 4, "mid": 3, "low": 2, "stuck": 1 },
"groups": [
{ "field": "mood", "fieldLabel": "Mood", "value": "tense", "titles": 11, "confidence": 0.93, "bucket": "high", "policy": "auto" },
{ "field": "content_rating", "fieldLabel": "Age rating", "value": "TV-14", "titles": 1, "confidence": 0.81, "bucket": "mid", "policy": "always_human" }
],
"stuckTitles": 1
}Add ?catalog={catalogId} to count only one catalog's titles. GET /v1/review/{titleId} is the per-title view: pending
suggestions grouped by field with ranked alternates and confidence, pending changes, locked fields, and the title's place in
the queue.
Every suggestion has a policy, which is what your integration should branch on rather than the raw confidence:
policy | Applies to | Meaning |
|---|---|---|
auto | Confidence of 0.90 or more on an ordinary field (themes, moods, keywords …) | Safe to accept in bulk |
review | Lower confidence, or no confidence given | A person should look |
read_through | Synopsis, short description, tagline | Long-form prose: read it, then decide it on its own |
always_human | Age rating, content advisories, audience fit, territory, license, availability | Decide one at a time; never bulk-accepted |
Accept or reject by field and value. Add assetId to decide for one title only; leave it out to decide the whole group.
curl -X POST https://dev.app.ingenee.ai/api/v1/review/accept \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{ "field": "mood", "value": "tense", "assetId": "7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11" }'{ "decided": 1, "action": "accepted" }POST /v1/review/reject takes the same body and also removes the value from the title's AI attributes so it stops
appearing. The response is { "decided": 0, … } if nothing matched, which usually means it was already decided.
Pending changes to fields
Some proposals are changes to a canonical field, shown as current and proposed value side by side.
GET /v1/modifications returns { count, rows } (up to 500 pending, newest first); each row has id, assetId,
assetTitle, field, currentValue, proposedValue, source, confidence and proposedAt.
curl -X POST https://dev.app.ingenee.ai/api/v1/modifications/3f6b1c2e-8a4d-4e0b-9c7a-5d1e2f3a4b5c \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{ "action": "accept" }'The response is { "id": "…", "status": "…" }. Accepting writes the proposed value to the field, unless the field is
locked (refused) or the change was already decided (409 CONFLICT).
Translate a title's metadata
Translation creates proposals per language and field. Nothing reaches the title's localized metadata until you approve it.
curl -X POST https://dev.app.ingenee.ai/api/v1/titles/7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11/localize \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{ "locales": ["es", "te"], "fields": ["title", "synopsis", "genres"], "skipExisting": true }'{ "created": [ { "locale": "es", "field": "title" }, { "locale": "es", "field": "synopsis" } ],
"skipped": [ { "locale": "te", "field": "title", "reason": "locked" } ] }locales takes 1 to 54 language codes. fields can be title, synopsis, short_description, keywords, genres,
cast, editorial_summary, themes, moods, audience and tone. A skipped reason is source (the target is the
text's own language), locked, or exists (with skipExisting). onlyOutdated regenerates only proposals whose source
text changed. A 403 ENTITLEMENT means AI localization isn't enabled for your organization.
GET /v1/titles/{id}/localizations shows, per language, every field's source, proposedValue, status, confidence,
locked and whether it is outdated (the source text changed since). Approve what you've reviewed:
curl -X POST https://dev.app.ingenee.ai/api/v1/titles/7c1a3f0e-5d2b-4f6a-9a41-2f0e9d3b8c11/localizations/approve \
-H "Authorization: Bearer $INGENEE_API_KEY" -H "content-type: application/json" \
-d '{ "locale": "es" }'{ "approved": 3, "skipped": [], "locale": "es", "scope": "language" }Leave out locale to approve every pending language (scope: "all-languages"). Locked proposals are skipped and reported.
Approving applies to all pending proposals in scope at once, so read them first. Editing or rejecting a single proposal
isn't available through the API.
Not yet available
- A dedicated "re-run metadata matching only" call.
reembedre-runs the whole enrichment pass;matchandidentitychange which provider record a title is tied to. - Accepting or editing a single translation proposal. Approval is per language or for all.
- Bulk approving every pending AI proposal in one call (the console's "Approve all").
Endpoints
| Operation | Method + path | Scope |
|---|---|---|
| Get match candidates | GET /v1/titles/{id}/match | library:read |
| Accept or reject a match | POST /v1/titles/{id}/match | enrichment:write |
| Set TMDB / IMDb identity | POST /v1/titles/{id}/identity | enrichment:write |
| Re-run enrichment | POST /v1/titles/{id}/reembed | enrichment:write |
| Translate metadata | POST /v1/titles/{id}/localize | enrichment:write |
| List translation proposals | GET /v1/titles/{id}/localizations | library:read |
| Approve translations | POST /v1/titles/{id}/localizations/approve | enrichment:write |
| List AI suggestions | GET /v1/review | library:read |
| Get a title's suggestions | GET /v1/review/{id} | library:read |
| Accept suggestions | POST /v1/review/accept | enrichment:write |
| Reject suggestions | POST /v1/review/reject | enrichment:write |
| List pending modifications | GET /v1/modifications | library:read |
| Resolve a modification | POST /v1/modifications/{modId} | enrichment:write |