Creative Management¶
Experimental — not operational
Creative management is models and CRUD stores only, not a running pipeline. There is no validator that checks a creative against IAB format specs, no trafficking logic, no Innovid or Flashtalking ad-server client, and no code that emits the creative lifecycle events listed below. AdServerStore is never constructed outside tests. validation_status only changes when something calls update_creative_asset() by hand; nothing checks a creative automatically. Everything below describes the design intent for this subsystem — what it is meant to do once it is built out and wired into a live path — not current runtime behavior. It is retained for future development.
Creative management is designed to cover the lifecycle of ad creative assets within the buyer system --- from uploading and storing assets, through format validation against IAB standards, to binding creatives to deals on external ad servers. Today the system only tracks creative assets per campaign via plain CRUD; it does not validate them, and it does not maintain any live integration with ad server platforms like Innovid and Flashtalking. Model classes and schema tables that reference those platforms exist, but no client code talks to them.
Core Concepts¶
Creative Assets¶
A CreativeAsset represents a single ad creative --- a display banner, video, audio clip, interactive unit, or native ad. Each asset belongs to a campaign and carries metadata about its format, validation status, and source location.
| Field | Type | Description |
|---|---|---|
asset_id |
str |
Unique identifier (UUID, auto-generated) |
campaign_id |
str |
The campaign this asset belongs to |
asset_name |
str |
Human-readable name |
asset_type |
AssetType |
display, video, audio, interactive, or native |
format_spec |
dict |
Format-specific metadata (varies by type) |
source_url |
str |
URL where the creative file is hosted |
validation_status |
ValidationStatus |
pending, valid, or invalid |
validation_errors |
list[str] |
Validation error/warning messages |
created_at |
datetime |
When the asset was created |
Asset Types¶
The system supports five asset types, each with its own format specification structure:
| Type | asset_type |
Typical format_spec Fields |
|---|---|---|
| Display | display |
width, height, file_format (e.g., {"width": 300, "height": 250, "file_format": "png"}) |
| Video | video |
duration_sec, vast_version, resolution (e.g., {"duration_sec": 30, "vast_version": "4.2", "resolution": "1920x1080"}) |
| Audio | audio |
duration_sec, bitrate, file_format (e.g., {"duration_sec": 30, "bitrate": 192, "file_format": "mp3"}) |
| Interactive | interactive |
width, height, simid_version (e.g., {"width": 300, "height": 250, "simid_version": "1.1"}) |
| Native | native |
headline_length, body_length, image_dimensions |
Validation Status (Design Intent)¶
Every asset starts in pending status and is intended to pass validation before it is attached to a deal. Today there is no automated IAB spec check and no gate that enforces this: validation_status only ever changes when a caller sets it explicitly through update_creative_asset(), and nothing in the live application currently blocks an unvalidated creative from being associated with a deal.
stateDiagram-v2
[*] --> pending : asset created
pending --> valid : passes IAB spec check (not implemented)
pending --> invalid : fails IAB spec check (not implemented)
invalid --> pending : re-upload / fix
Working with Creative Assets¶
Creating Assets¶
Use CampaignStore to create and manage creative assets:
from ad_buyer.storage.campaign_store import CampaignStore
import json
store = CampaignStore("sqlite:///./ad_buyer.db")
store.connect()
# Create a display creative
asset_id = store.save_creative_asset(
campaign_id="campaign-abc",
asset_name="Q3 Hero Banner 300x250",
asset_type="display",
format_spec=json.dumps({
"width": 300,
"height": 250,
"file_format": "png",
"file_size_kb": 45,
}),
source_url="https://cdn.example.com/creatives/hero-300x250.png",
)
# Create a video creative
video_id = store.save_creative_asset(
campaign_id="campaign-abc",
asset_name="Q3 Brand Spot 30s",
asset_type="video",
format_spec=json.dumps({
"duration_sec": 30,
"vast_version": "4.2",
"resolution": "1920x1080",
"codec": "h264",
"bitrate_kbps": 5000,
}),
source_url="https://cdn.example.com/creatives/brand-spot-30s.mp4",
)
Listing and Filtering¶
# List all creatives for a campaign
assets = store.list_creative_assets(campaign_id="campaign-abc")
for a in assets:
print(f" {a['asset_name']} ({a['asset_type']}) - {a['validation_status']}")
# Filter by asset type
video_assets = store.list_creative_assets(
campaign_id="campaign-abc",
asset_type="video",
)
Updating Validation Status¶
There is no automated spec validation to run. The store lets you set the status directly, which is useful once a validator exists but today only reflects whatever a caller decides to record:
# Mark as valid
store.update_creative_asset(
asset_id=asset_id,
validation_status="valid",
)
# Mark as invalid with errors
store.update_creative_asset(
asset_id=video_id,
validation_status="invalid",
validation_errors=json.dumps([
"Video duration 45s exceeds maximum 30s for pre-roll placement",
"Missing VPAID companion banner",
]),
)
Using the CreativeAsset Model¶
For programmatic use outside the store's dict-based interface, the CreativeAsset dataclass provides serialization helpers:
from ad_buyer.models.creative_asset import CreativeAsset, AssetType, ValidationStatus
# Create from code
asset = CreativeAsset(
campaign_id="campaign-abc",
asset_name="Q3 Hero Banner",
asset_type=AssetType.DISPLAY,
format_spec={"width": 300, "height": 250, "file_format": "png"},
source_url="https://cdn.example.com/creatives/hero.png",
)
# Serialize to dict (for JSON encoding, API responses, etc.)
asset_dict = asset.to_dict()
# Reconstruct from dict
restored = CreativeAsset.from_dict(asset_dict)
Ad Server Integration (Design Intent, Not Implemented)¶
The design intent is that after creatives are validated, they get trafficked to an external ad server for delivery, with the buyer system integrating with Innovid and Flashtalking through the ad server campaign binding model below. None of this exists today: there is no Innovid or Flashtalking client, and nothing in the live application calls any trafficking or sync logic. What does exist is the data model and a CRUD store (AdServerStore) to hold records in this shape, but AdServerStore is never constructed outside tests, so nothing populates these tables in production either.
How It Works¶
An AdServerCampaign record is designed to link a buyer campaign to its representation on the ad server. Within that record, AdServerBinding entries would map individual deal + creative pairs to ad server line items --- once something exists to create and sync them.
flowchart TD
Campaign["Buyer Campaign"]
ASC["AdServerCampaign\n(Innovid or Flashtalking)"]
B1["Binding: deal-001 + creative-A\n→ ad server line 12345"]
B2["Binding: deal-002 + creative-B\n→ ad server line 12346"]
Delivery["AdServerDelivery\n(impressions, spend, discrepancy)"]
Campaign --> ASC
ASC --> B1
ASC --> B2
ASC --> Delivery
Data Models¶
AdServerCampaign --- the top-level integration record:
| Field | Type | Description |
|---|---|---|
id |
str |
Record UUID |
campaign_id |
str |
FK to the buyer campaign |
ad_server |
AdServerType |
INNOVID or FLASHTALKING |
ad_server_campaign_id |
str |
The campaign ID on the ad server |
status |
AdServerCampaignStatus |
PENDING, ACTIVE, PAUSED, COMPLETED, or ERROR |
bindings |
list[AdServerBinding] |
Deal-to-line-item mappings |
delivery |
AdServerDelivery? |
Aggregated delivery data |
created_at |
datetime |
When the record was created |
AdServerBinding --- one deal + creative mapped to an ad server line:
| Field | Type | Description |
|---|---|---|
deal_id |
str |
The buyer deal ID |
creative_id |
str |
The creative asset ID |
ad_server_line_id |
str |
Line item ID on the ad server |
serving_status |
BindingServingStatus |
ACTIVE, PAUSED, or ERROR |
last_sync_at |
datetime |
Last synchronization timestamp |
AdServerDelivery --- aggregated delivery data for discrepancy detection:
| Field | Type | Description |
|---|---|---|
impressions_served |
int |
Impressions reported by the ad server |
spend_reported |
float |
Spend reported by the ad server |
last_report_at |
datetime |
When the last report was received |
discrepancy_pct |
float |
Discrepancy between buyer-side and ad-server-side counts |
Creating Ad Server Bindings¶
These are CRUD calls against local storage only --- calling them does not talk to Innovid, Flashtalking, or any other ad server. Setting status="ACTIVE" records that value in the local table; it does not mean anything was actually trafficked.
Use CampaignStore or the dedicated AdServerStore:
from ad_buyer.storage.campaign_store import CampaignStore
store = CampaignStore("sqlite:///./ad_buyer.db")
store.connect()
# Create an ad server campaign binding record (local storage only)
binding_id = store.save_ad_server_campaign(
campaign_id="campaign-abc",
ad_server="INNOVID",
external_campaign_id="innovid-camp-789",
status="PENDING",
creative_assignments=json.dumps({
"deal-001": "asset-aaa",
"deal-002": "asset-bbb",
}),
)
# Manually record a status change --- no real trafficking happens here
store.update_ad_server_campaign(
binding_id=binding_id,
status="ACTIVE",
last_sync_at=datetime.now(timezone.utc).isoformat(),
)
For the Pydantic model-based interface with full serialization:
from ad_buyer.models.campaign import (
AdServerCampaign,
AdServerType,
AdServerBinding,
)
from ad_buyer.storage.adserver_store import AdServerStore
adserver_store = AdServerStore("sqlite:///./ad_buyer.db")
adserver_store.connect()
record = AdServerCampaign(
campaign_id="campaign-abc",
ad_server=AdServerType.FLASHTALKING,
ad_server_campaign_id="ft-camp-456",
bindings=[
AdServerBinding(
deal_id="deal-001",
creative_id="asset-aaa",
ad_server_line_id="ft-line-001",
),
AdServerBinding(
deal_id="deal-002",
creative_id="asset-bbb",
ad_server_line_id="ft-line-002",
),
],
)
adserver_store.save_ad_server_campaign(record)
# Query by campaign
records = adserver_store.list_ad_server_campaigns(campaign_id="campaign-abc")
Creative Events (Design Intent, Not Implemented)¶
These five event types exist in the EventType enum, but nothing in the live application publishes any of them. There is no code that emits creative.uploaded when save_creative_asset() is called, no validator to emit creative.validated, and no trafficking logic to emit creative.matched or creative.ad_server_pushed.
| Event | When (once implemented) | Payload |
|---|---|---|
creative.uploaded |
A new creative asset is stored | Asset metadata |
creative.validated |
Spec validation completes (pass or fail) | Validation result, errors |
creative.matched |
A creative is assigned to a deal | Deal ID, creative ID |
creative.rotation_updated |
Rotation rules change | Rotation config |
creative.ad_server_pushed |
Creative trafficked to ad server | Ad server, line ID |
Integration with the Campaign Workflow (Design Intent, Not Implemented)¶
The design intent is for creative management to integrate with the campaign workflow at two points, but neither is implemented today:
-
Brief submission --- The campaign brief can include
creative_idsreferencing pre-uploaded assets. The intent is for these to be validated and associated with the campaign during ingestion; today there is no automated validation step, so this is bookkeeping only. -
Approval gate --- The
CREATIVEapproval stage exists as an enum value and can be enabled in the brief'sapproval_config, but since nothing pushes creatives to ad servers, there is currently nothing for this stage to gate.
There is no code today that performs creative-to-deal binding or ad-server trafficking after deals are booked. valid/invalid counts you see anywhere in reporting reflect only whatever a caller set by hand via update_creative_asset().
Persistence¶
Creative data lives in two SQLite tables managed by CampaignStore:
| Table | Purpose |
|---|---|
creative_assets |
Creative files, format specs, and validation status |
ad_server_campaigns |
Ad server integration records with deal-to-line bindings |
The AdServerStore provides a separate, model-aware interface to the ad_server_campaigns table with full Pydantic serialization/deserialization of bindings and delivery data.
Related¶
- Architecture Overview --- Agent hierarchy and system design
- Deals API --- Deal booking and management
- Budget Pacing & Reallocation --- Pacing engine design (also experimental, not operational)