Catalog Ingestion
Submit your media catalog in batches and poll the job for its outcome — the accept-then-poll ingestion surface.
POST /v1/catalog/ingest/jobs submits a batch of catalog titles for
ingestion; GET /v1/catalog/ingest/jobs/{jobId} reports what happened to it.
Ingestion is asynchronous: the submission is accepted immediately with a
job id, and the work runs in the background.
Both endpoints are machine-to-machine. They take no X-Session-Id — a
catalog load carries no passenger session. Your airline and your
content-rights ruleset are resolved from your credential and are never
fields on the request; there is nothing on these shapes on which to assert a
tenant.
Ingestion is the prerequisite for everything else. A title must be centrally ingested before a flight can list it in its active catalog — that push resolves external ids, it never creates titles.
Quickstart: ingest, then poll
1. Submit a batch.
202 Accepted:
The jobId is a server-minted UUID string, scoped to your airline. Treat it
as opaque.
2. Poll the job.
titleId is the platform id this title now has — an opaque integer we
assign. Store it and use it where our other endpoints take a titleId
(such as suggestions and
events), but don’t infer anything from its size
or range, and don’t assume it is near any id you have seen before.
A job that takes minutes does not need second-resolution progress — a few seconds between polls of the same job is plenty.
Submitting a batch
The body is capped at 8 MiB (413 request.malformed above it) — a
500-title batch of rich metadata is comfortably inside it.
sourceSystem is your id namespace. Every externalTitleId in the batch
is scoped to it: the same external id under two source systems is two
different identities. Use the same token later when you push a flight’s
active catalog — that is the join key
between the two calls.
Batch cap: 500 items per request. A larger catalog is split across requests. An oversized batch is rejected whole, never partially ingested.
Item fields
Genre vocabulary: Action, Adventure, Animation, Biography, Comedy,
Crime, Documentary, Drama, Family, Fantasy, History, Horror,
Music, Musical, Mystery, Romance, Sci-Fi, Sport, Thriller,
War, Western.
Send null for unknown, never a placeholder. An empty string, or a
sentinel like "OTHER" / "UNKNOWN" / "N/A", is rejected with
ingest.sentinel_value — it is not silently normalized away. A data gap
reported as a gap is usable; a placeholder that looks like data is not.
All four kinds ingest end to end
Movie, TV, Audio, and Game titles all ingest, and audio and game
titles are searchable and servable once they land.
Audio and game ratings are a known gap. Audio and game content ratings
carry no recognized scale today, so a submitted rating on those kinds
normalizes to null — and a title with no content rating is treated as
most-restrictive by the age gate. In practice
that means audio and game titles are hidden from Child, Tween, and
Unknown members until a real rating vocabulary lands. They are fully
visible to Adult members. This is fail-closed by design, not a defect —
but plan your UI for it rather than expecting parity with movie and TV.
Retrying safely: idempotency and correlation ids
Two headers matter on a retry, and they behave in opposite ways. Getting this backwards is the most common ingestion mistake, so it is worth stating as a rule: one idempotency key per batch, one correlation id per attempt.
Idempotency-Key(optional) — a caller-chosen key that makes a retried submission safe. Reuse it on every attempt of the same batch. A repeat with the same key resolves to the same job rather than ingesting twice. With no key sent, identical content is deduplicated instead.X-Correlation-Id(optional) — mint a fresh one on every attempt. It is a per-request cost-reservation key, not a request id you may replay. Reusing the correlation id of an attempt whose job already finished earns a429ceiling-exceeded response — a confusing denial for a submission that actually succeeded.
Reusing the correlation id is what a client naturally does on retry, which is exactly why this needs saying: keep the idempotency key, change the correlation id.
Polling the job
GET /v1/catalog/ingest/jobs/{jobId} returns:
A job id that is not your airline’s returns 404 ingest.job_not_found —
airline-scoped, never a leak of another tenant’s job.
The report
Low coverage is a real signal, not noise: semantic ranking quality is bounded
by the metadata you send. keywordCoveragePct in particular drives
franchise and entity queries.
Dispositions
Each entry in mappings[] says how the mapping came to be:
created— a new platform title was minted.linked— the submission matched a title we already had, by exact normalized fingerprint (kind + normalized title + year). There is no fuzzy auto-merge, ever.updated— an already-mapped title whose missing fields were filled.
Field conflicts: fill-nulls-only
Ingestion fills missing fields; it never overwrites existing ones. When a
submitted value differs from an existing non-empty platform value on a linked
title, the submitted value is not applied — the conflict is counted in
fieldConflictCount and sampled in the report rather than silently resolved
in either direction.
So a non-zero fieldConflictCount is not an error; it means your submission
and our stored record disagree about titles you linked to, and you may want
to look at which.
Per-item rejection codes
Rejections are per item: a bad item does not sink its siblings. The
code is the stable contract; the message is human-readable prose saying
what was wrong and what to send instead — safe to log and to surface to an
operator.
Field-level
Identity resolution
ingest.dangling_mapping never means “an earlier attempt didn’t
finish”. That case — a batch interrupted between recording a mapping and
storing the title — is recovered automatically when you resubmit, and is
never reported under this code. This code always means a title went missing
after the fact, so the remedy is different: investigate the missing title,
then resubmit. Resubmitting alone will not fix it.
An external id is resolvable only once its batch completes
An external id becomes resolvable — to the flight active-catalog push, for example — when the batch that submitted it completes, not when that batch is accepted. An id from an interrupted job reports as unresolved until the batch is resubmitted and finishes.
Read “unresolved” as “this batch has not finished”, and nothing more specific:
- It does not reliably mean the title has not been written. A submission that linked to a title you already had leaves that title present and fully servable — and its external id still reports unresolved until the batch completes.
- The platform will not report an id resolvable while any doubt remains.
- The remedy is the same either way: resubmit the ingest. Resubmission resumes; it does not duplicate.
Request rate limits
The partner surfaces carry per-client request-rate limits, and submission and status polling are separate classes with separate budgets:
- Job submission has its own per-client window, sized like the other partner write routes (the flight endpoints).
- Job status polling has its own, higher per-client window.
The partition is the point: a submission burst can never blind your status
polling, and a status sweep can never consume your submission headroom. Both
are deliberately generous — sized so that legitimate operational bursts (a
full-catalog resync submitting a batch train while sweeping every job’s
status) never see a 429.
Over the limit, either endpoint returns 429 rate.limited in the
standard error envelope with a Retry-After header.
Honor it and back off. The numeric limits are deployment-tuned and are never
disclosed in the response — Retry-After is the only retry hint you need.
429 has two distinct causes on the submission endpoint, with two
different bodies. Branch on the body shape, not on the status code:
- Usage ceiling — the ceiling-exceeded body:
{ "schemaVersion", "reason", "ruleId", "currentSpendUsd", "ceilingUsd", "resetAtUtc", "correlationId" }.resetAtUtctells you when the window clears. There is noRetry-Afterheader, and retrying sooner will not help. - Per-client request rate — the standard error envelope with
"code": "rate.limited"and aRetry-Afterheader.
The API Reference can only declare one schema per status code, so it documents the ceiling shape; this is the case it cannot express.
Planned capabilities
Catalog replace mode — planned. A future optional mode property on
the ingest job will let you reconcile your full catalog against a
complete submission rather than upserting into it: batches accumulate in
one job session, an explicit commit applies the result atomically, and
titles absent from the submission are deactivated, never deleted —
identifiers, external-id mappings, and personalization history are all
preserved.
The compatibility promise, so you can plan around it: mode is strictly
opt-in and additive. Omitting it keeps exactly today’s upsert behavior,
unchanged. And because deactivation preserves history, a full monthly
catalog refresh never cold-starts recommendations, and no intermediate
state ever serves an empty or partial catalog.
We are not publishing request or response shapes or a delivery date for this yet.
Errors
Standard envelope: 422 request.validation (bad
sourceSystem, empty items, an item missing externalTitleId / kind /
title), 422 ingest.batch_too_large (over 500 items),
404 ingest.job_not_found (an unknown or foreign job id),
429 (see the two causes above), 401 / 403,
413 request.malformed (body over 8 MiB), 500.
Full request and response schemas are in the API Reference.