SceneContext · Partner integration

Enrichment API Handbook

Send one OpenRTB bid request. Get verified programme metadata back in the same HTTP response — contextual segments, IAB subcategories, normalised genres and a persistent content id.

Endpoint
POST https://rtd.scenecontext.io/v1/enrich
Auth
Bearer token, or ?api_key=
Body
OpenRTB 2.x bid request, up to 64 KB
Latency
p50 5 ms · p95 8 ms · p99 20 ms, in-region

Your key decides the contract

Three things behave differently depending on the profile behind your API key: what happens the first time we see a piece of content, what an unmatched request looks like, and which response shape you get. Everything else on this page is the same for everyone. Check which mode your key uses before you write the client — the retry logic differs.

Sync

resolve_mode: "" (default)

We resolve inside the request. First sight of new content costs the full resolution time, then answers from cache.

Use when your timeout has room and you want the complete answer on the first impression.

Cache only

resolve_mode: "cache_only"

Never waits. A cache miss returns 204 immediately with Retry-After: 1 and resolves in the background.

Use on a hard bid path. The content's next impression is the retry.

Fast

resolve_mode: "fast"

Never waits, but answers anyway: a cache miss returns classifier-tier segments in about a millisecond while full resolution warms behind it.

Use when something is always better than nothing.

The second setting is what an unmatched request looks like. Most profiles return 204 with no body. Some return 200 with {} instead, so that a match failure is never confusable with a transport error. Both are deliberate answers, not errors.

Key your client on Cache-Control, not on the status code. Every response says how long it is good for. That one rule makes your integration correct in all three modes without branching on which one you have.

Integration options

Most partners integrate over HTTP and never need anything else. The rest exist because an exchange or a bidder already speaks something specific, and we would rather match it than ask you to build an adapter.

SurfaceStatusWhen it fits
HTTP JSON
POST /v1/enrich
Live The default. OpenRTB in, enrichment out, one round trip.
Alternative response shapes Live A profile setting on your key, not a code change on your side: flat JSON, a ready-to-merge OpenRTB content.data[] block, a complete content object, or a bare segment id list.
Cached GET Live For platforms that fetch on their own schedule and cache: a GET with macro parameters instead of a posted body.
Exchange-native routes Live We already speak several exchange-native request and response shapes directly, so an existing integration keeps its wire format.
Prebid RTD provider On request A Prebid real-time-data provider exists for web inventory. Ask before you plan around it — it is not part of the CTV path and we would refresh it for you.
gRPC / Protobuf Scoped, not shipped We have the schema work mapped from an exchange integration, but there is no gRPC surface in production today. If you need one, we build it against your schema — tell us early, it changes the deployment shape below.

What we can tailor to you

Everything in this table is a setting on your key — not a code change on either side — and it takes effect within about fifteen seconds of us making it. If something here is wrong for you, it is a conversation, not a project.

What you can changeOptions
Request shapePosted OpenRTB, a GET with macro parameters, or a generic POST body.
Response shapeFlat JSON, a ready-to-merge OpenRTB content.data[] block, a complete content object, or a bare segment id list.
Which fields you receiveAny subset of the response fields, so you are not parsing past things you do not use.
Segment namesWe map our ids onto your own vocabulary. Anything unmapped is dropped rather than passed through, so you never receive an id you have no definition for.
How many segmentsA cap, if your downstream has one.
What an empty answer looks like204 with no body, or 200 with {}.
Whether we make you waitThe three modes above.
How long you may cacheSeparate lifetimes for resolved and unmatched answers.
Your ceilingsRate limit, and the latency budget we hold ourselves to.
PolicyBundle blocklists, and whether we require consent before anything identity-keyed runs.

This is not theoretical. Four partners run on this today and no two of them see the same response: one sends a GET and receives a bare list of ids, one receives protobuf on a seven-millisecond budget, one receives a complete OpenRTB content object. Same service, four contracts.

Authentication

Either form works on every endpoint. Use the header in production; the query parameter exists for environments where you cannot set headers.

Authorization: Bearer sc_dsp_xxxxxxxxxxxxxxxxxxxxxxxx

POST /v1/enrich?api_key=sc_dsp_xxxxxxxxxxxxxxxxxxxxxxxx

A missing or wrong key returns 401 with no body worth parsing. Keys carry your profile, your rate limit and your response format — treat them as environment-specific.

Request

POST the bid request as you received it from the exchange. No transformation on your side: we read the content object and ignore the rest. The content object is accepted at the top level or nested under app or site — both resolve identically.

Live channel

{
  "app":     { "bundle": "com.example.ctvapp" },
  "content": {
    "livestream": 1,
    "channel": { "id": "SPORT1.de", "name": "SPORT1" },
    "genre":   "sport"
  },
  "device":  { "geo": { "country": "DEU" } }
}

On-demand title

{
  "app":     { "bundle": "com.example.ctvapp" },
  "content": {
    "title":  "Mord mit Aussicht",
    "series": "Mord mit Aussicht",
    "genre":  "krimi"
  }
}

What we read

FieldWhat it does
content.idPlatform or publisher content id, or a SceneContext SC… id. Exact match, highest priority, instant.
content.channel.id
content.ext.channel_id
The best signal for live TV. Exact key into the schedule — resolves the programme airing right now. Send it whenever you have it.
content.channel.name
content.network.name
Channel identity by name. Matched strictly against the normalised name; a confident match resolves as well as an id.
content.livestream1 routes to the schedule; absent or 0 routes to title resolution.
content.title, content.seriesResolved against the title catalogue. series disambiguates episode-style titles.
content.genre, content.cat, content.cattaxYour declared genre and IAB categories. Feed the classifier tier and constrain the match.
content.keywordsSynopsis or keyword hints, when you have them.
app.bundle, app.id, app.nameApp identity and platform detection. app.name is also a channel fallback for editorial brands sent only there.
site.domain, site.pageSite and page identity for web inventory.
device.geo.countryISO 3166-1 country. Scopes channel identity and live context to the viewer's market — without it a channel name shared across countries cannot be resolved safely.

The highest-value thing on your side: send channel ids for live inventory, not just names. Ids are exact; names are matched strictly and will under-match by design. Ids are opaque by contract — several schemes coexist with inconsistent casing, so never parse or derive them. Look them up once by name through /v1/reference/channels and send them verbatim.

Response

Flat JSON by default. Fields are omitted when there is nothing behind them, so tolerate absence rather than expecting a fixed shape.

HTTP/1.1 200 OK
Cache-Control: max-age=86400

{
  "scid":     "SC4mNpQr7xKj2wBv-0",
  "segments": ["SC_TOPIC_ENTERTAINMENT_CRIME", "SC_GENRE_CRI_001",
               "SC_CHANNEL_DE_SPORT1", "SC_CTX_SAFE_PREMIUM"],
  "iab":      ["KHPC5A", "647"],
  "genres":   ["Crime", "Drama"],
  "signal_quality": { "score": 0.88 },
  "mood":     "tense",
  "audience": "general",
  "contextual_score": 0.71,
  "keywords": "ermittlung,kleinstadt,kommissarin"
}
FieldTypeMeaning
scidstringPersistent content id, stable across every publisher carrying the same programme. Use it as your aggregation and dedupe key.
segmentsstring[]Contextual targeting segments, best match first.
iabstring[]IAB Content Taxonomy ids of the resolved programme — not an echo of what you sent. Restoring
genresstring[]The programme's own genres, normalised. Restoring
signal_quality.scorefloatConfidence in the top segment, 0 to 1. Gate how aggressively you act on the rest.
mood, audience, contextual_score, keywords—Programme-level context. Present for live content whenever the channel has an active schedule slot.
_envstring"test" on test keys. Absent in production — use it to assert you are pointed at the right environment.

Alternative response shapes are a profile setting, not a code change on your side: a ready-to-merge OpenRTB content.data[] block, a complete content object, or a bare segment id list. Ask and we switch your key.

Live channel

A live request resolves against the schedule rather than the title catalogue, and the answer describes the programme airing at that moment. Four fields are live-only: mood, audience, contextual_score and keywords all describe what is on air right now, not the channel in general.

HTTP/1.1 200 OK
Cache-Control: max-age=86400

{
  "segments": ["SC_CHANNEL_DE_SPORT1", "SC_TOPIC_SPORT_FOOTBALL",
               "SC_GENRE_SPO_001", "SC_CTX_TIMEOFDAY_EVENING",
               "SC_CTX_SAFE_PREMIUM"],
  "genres":   ["Sport"],
  "iab":      ["483"],
  "signal_quality": { "score": 0.91 },
  "mood":     "energetic",
  "audience": "sports_fan",
  "contextual_score": 0.78,
  "keywords": "bundesliga,spieltag,analyse",
  "epg":      { "status": "covered", "slot_end": 1790199000 }
}

The optional epg block answers a question the segments cannot: whether we hold schedule coverage for this channel at all. status: "covered" with a slot_end means a live slot resolved and tells you when it turns over — cache no further than that. channel_unknown means we do not carry this channel, which is a signal to send us the channel id or ask us to add the feed, not a transient failure.

Two honest notes on the data fields.

genres and iab are marked Restoring above. They are part of the contract and they will be populated, but on the serving fleet as it stands today they come back empty — the metadata they are drawn from has not yet followed resolution onto the local serving nodes. If you are planning to act on them, ask us for the date before you build against them.

The resolved programme title is not returned at all today. We match your request to a programme and can tell you a great deal about it, but not its name. If your bidder wants the title back, for logging, reporting or your own targeting, say so — it is a small change on the on-demand path and a scheduled one on the live path, and we would rather build it than have you infer it.

Segment families

PrefixWhat it carries
SC_CHANNEL_*Channel identity, country-coded and gated by device.geo
SC_TOPIC_*Content topics — drama, documentary, sport
SC_GENRE_*, SC_IAB_*Genre and IAB taxonomy codes
SC_CTX_*Context — mood, brand safety, time of day, trends
SC_AUD_*, SC_PER_*Audience affinity and persona signals

The live vocabulary is available at /v1/reference/segments — fetch it rather than hard-coding a snapshot.

Status codes

CodeWhenWhat to do
200EnrichedUse it. Cache-Control says for how long.
200 {}No match, on profiles configured that wayBid unenriched. Never an error.
204No body — two distinct meaningsRead Cache-Control, see below.
400Body is not valid JSON, or over 64 KBFix the payload.
401Missing or invalid keyCheck the key and the environment.
405Wrong method for this endpoint/v1/enrich is POST.
429Over your rate limitBack off. Ask us to raise it.
500Our sideBid unenriched. Tell us if it persists.

The two meanings of 204

HeadersMeaningWhat to do
no-cache
Retry-After: 1
Not available yet. First sight of this content. Do not cache. Bid unenriched; the content's next impression is the retry. No special retry logic needed on a bid path.
max-age=300 No confident match. A deliberate answer — we return nothing rather than pad the response. Cache per content. It refreshes within minutes, so new coverage is picked up on its own.
max-age=86400 Nothing to work with: no title, id, genre, bundle or channel in the request. Cache it. This one can never resolve — do not retry.

Latency and your timeout

PercentileIn-region, persistent connection
p505 ms
p958 ms
p9920 ms

These hold for every response — enriched, no-match and first-sight alike. Calling from another data centre adds your network round trip; cross-region within the EU is typically 20 to 30 ms on top. The TLS handshake costs about 50 ms once per connection, so use a shared client with keep-alive.

Budget rule. p99 is the contract. Set a hard 20 ms timeout, plus your network round trip if you call cross-region, and treat anything slower exactly like a 204: drop it and bid unenriched. An unenriched bid is always better than a late one. Add a circuit breaker that skips enrichment entirely while we are repeatedly slow.

Where we run

The published figures above are for a caller in the same region. If your bidders sit somewhere else, the network round trip is added on top and it will dominate everything we do. That is worth solving at the deployment layer rather than in the API.

OptionWhat it removesLead time
Shared regional endpoint
Frankfurt, eu-central-1
Nothing to remove — in-region callers already get the published figures. Available now.
Dedicated node in your region The cross-region round trip, typically 20 to 30 ms within the EU and more across an ocean. About half a day to stand up, once we hold compute quota in that region. Requesting the quota takes a few days and costs nothing, so we do it as soon as you name the region.
Node inside your VPC The public network path entirely. Traffic never leaves your VPC; we are reachable on a private address and allowlisted to your security group. Agreed per deployment. We already run this way for another partner.

Each serving node is self-sufficient — it answers from local data, with no call to a central service on the request path — which is what makes putting one next to you a deployment decision rather than an architecture change. Tell us the region and the network you bid from and we will tell you which of the three applies and what latency to write into the contract.

Caching

On-demand content is cacheable per content for about 24 hours, and the header says so explicitly. Because scid is stable across publishers, one resolution serves every request for the same programme wherever it airs.

Live TV is the exception. The programme on a channel changes with the schedule, so a cached answer goes stale the moment the slot turns over. Cache live-channel responses for minutes, not hours, or re-request per ad break.

Reference endpoints

Automate your channel mapping instead of consuming snapshots. Same auth and rate limit as enrich.

EndpointReturns
GET /v1/reference/channels?q=sport Channels in the currently loaded schedule as {id, name, entries}. q filters by name or id. A channel missing here is a gap in the upstream feed, not a wrong id on your side.
GET /v1/reference/segments The segment taxonomy as {id, match_type, parent}.

Both send an ETag keyed to the underlying data version. Poll with If-None-Match and you get a cheap 304 until something actually changes.

Data freshness

First call

Confirm your key and environment before writing any integration code. On a test key the response carries "_env": "test".

curl -s -X POST https://rtd.scenecontext.io/v1/enrich \
  -H "Authorization: Bearer $SC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"app":{"bundle":"com.example.ctvapp"},
       "content":{"title":"Mord mit Aussicht","genre":"krimi"}}'

Then wire it into your bid evaluation with a shared keep-alive client, a 20 ms timeout and the circuit breaker described above. Integration examples in Go and Java are available on request.