Architecture: CTV Ad Server, SSP, and DSP
How LtvAdx's control plane, ad server, and event pipeline fit together — useful for planning an integration and for debugging one.
The three services
Control plane (backend + dashboard)
Publishers create channels; advertisers create campaigns, line items and creatives; admins approve channels, creatives and accounts. Everything is stored in MongoDB and scoped by networkId. There is no separate publish step: a saved change reaches the ad server at its next snapshot pull, within about 15 seconds.
Ad server
The ad server answers /vast, /vmap, /rtb/bid and tracking beacons. It decides every ad from an in-memory snapshot that it pulls from the backend every 15 seconds — no database read while deciding. The only lookups at decision time are Redis reads for frequency caps, audience-segment membership and spend counters.
Event pipeline
Each tracking beacon is written to a Redis stream; the backend stores it every 2 seconds and adds it to the daily reporting summary every 5 minutes. Impression billing travels on a second stream and is applied at least once, never twice for the same request.
Request flow
- The CTV app calls
GET https://ads.ltvadx.com/vast?channelId=…&householdId=…&platform=…&breakType=… - The channel must be live and approved; otherwise the response is an empty VAST.
- A live direct-sold campaign on the channel wins outright.
- Otherwise every line item is checked against 26 filters (status, flight, channel, break type, duration, platform, country, content model, audience segments, ad format, floor CPM, frequency cap, budget, wallet…). The highest deal priority wins, then the highest bid CPM.
- If RTB fan-out is configured, an open-auction winner can be beaten by an external DSP bid (100 ms deadline).
- The winner's approved creative is returned as VAST 4.2 with signed tracking URLs.
- The player fires the impression and quartile beacons; they are recorded asynchronously.
Every response is 200
A request that gets no ad receives an empty <VAST version="4.2"/> with status 200 — never 204. Both filled and empty responses carry an X-Request-Id header; quote it when asking us why a request did not fill.
Deal priority
DIRECT_SOLD (6) > PROGRAMMATIC_GUARANTEED (5) > DIRECT_IO (4) > ADDRESSABLE (3) > PMP (2) > OPEN_AUCTION (1) Within the highest tier present: highest bid CPM wins.
Household identity
The ad server does not resolve households. Your player (or its backend) sends a stable householdId on every request; frequency caps and audience targeting key on it. A request without one is treated as anonymous — no caps, no segment targeting. The Identity API can create or look up a household id from a device id or IP — see the Identity API guide.
What is not built yet
- SSAI manifest stitching — the SSAI endpoints return a fixed template (see SSAI guide).
- Pod assembly — each
/vastcall returns one ad. - Geo resolution — country targeting applies only when the request carries a country.
- Consent strings (TCF / US Privacy / GPP) are not read at serve time.
Multi-tenancy
Every record is scoped by networkId; publishers, advertisers, agencies and operators see only their own data, and roles are enforced on every API call from the JWT.
API versioning
All control-plane APIs are under /api/v1/. Ad-serving endpoints (/vast, /vmap, /track, /rtb/bid) are not versioned in the path.