Rates
Publish rate cards
PUT/v1/me/rate-cardscatalog:write
Publish a versioned rate card commit. Entries are live for buyers the moment the commit lands, and drift detection runs on every commit: buyers with in-flight orders referencing the superseded version receive catalog.stale_rate_card events with a per-line diff. See Rates for how entries resolve and the price-clearance invariant for what the version pin buys you.
Request body
| Field | Type | Description |
|---|---|---|
commit required | string | Your id for this card version, e.g. acme-2026q4-v2. Orders pin it as rate_version. |
supersedes | string | The commit this one replaces. Drift diffs are computed against it. |
effective.start required | ISO date | First day the card applies. |
effective.end required | ISO date | Last day the card applies. |
currency required | ISO 4217 | The trade currency: one per commit, echoed on every entry by read surfaces, locked into an order with its rates, carried and never converted. |
agreement_scope | string | Default: null, an open card. Set an agreement id to publish a buyer-scoped card; negotiated rates and quotes live on these. |
commit_kind | enum | standing (default) or quote. A quote commit is a per-campaign price minted inside a plan; it requires agreement_scope and quote_expires_at, and locks under the price-clearance invariant on confirm. See Disclosure. |
quote_expires_at | ISO datetime | Required on quote commits. After it, the entries are a stale card (STALE_RATE_CARD): the buyer re-requests, nothing books at a lapsed price. |
proposal_ref | string | On quote commits: the proposal order this quote answers. |
entries[] required | array of object | The rate entries. Fields below. |
entries[]
| Field | Type | Description |
|---|---|---|
product_id required | string | The product the rate prices. Must exist in your published catalog. |
week | ISO week | Omit for a rate that holds across the effective window; set it to price a single week. This is the write shape's one difference from the rate entry read shape: at commit the platform materialises a spanning entry into one entry per ISO week, and where a spanning entry and a week-specific entry collide on the same rate key, the week-specific one wins for that week. The expansion and precedence rules are defined once, on the object reference. |
advertiser_class required | enum | One of your declared advertiser classes. |
spot_length required | integer | Creative length in seconds. One entry prices one length: commit a separate entry for every length you sell, at the price you charge for it. |
buy_type required | enum | One of the buy types. |
clearance_tier | enum | One of the clearance tiers; valid only for preemptible. |
market_class required | enum | One of the market classes, the entry's market timing: scatter or discounted. |
gross_rate_unit, gross_rate_cpm | number | Exactly one of the two, or the entry is refused 422 INVALID_INPUT. The one present determines the entry's rate_basis, which read surfaces emit; a rate_basis sent here is ignored rather than validated, so a write cannot contradict itself. |
audience_code | string | Required beside gross_rate_cpm, and on audience_guaranteed entries; the audience the rate prices or guarantees. |
Example request
PUT /v1/me/rate-cards
{
"commit": "acme-2026q4-v2",
"supersedes": "acme-2026q4-v1",
"effective": { "start": "2026-09-28", "end": "2026-12-27" },
"currency": "USD",
"agreement_scope": null,
"entries": [
{
"product_id": "acme-prime",
"week": "2026-W41",
"advertiser_class": "general",
"spot_length": 30,
"buy_type": "preemptible",
"clearance_tier": "P2",
"market_class": "scatter",
"gross_rate_unit": 405.00
}
]
}
There is no weekly rollup field. A cell prices one unit at one length; a seller who sells a weekly block prices it as its own entry whose unit is the week, rather than publishing a second figure whose denominator ("a full week of units") nothing defines.
Response
Entries succeed per entry, under the one write model every publish surface shares: an entry that fails validation is reported in errors[] and skipped, the valid entries are written, and only an integrity failure rolls the whole request back. Each errors[] element names the entry by its rate key and carries a code: UNKNOWN_PRODUCT_ID for a product not in your published catalog, INVALID_ENTRY for a bad ISO week, a missing rate-key part, or an entry carrying both or neither of gross_rate_unit and gross_rate_cpm. An unknown product_id is an error, never a warning: a skipped price a seller does not notice is a cell buyers cannot buy.
The response also acknowledges the commit as the current version and reports drift detection: stale_notifications_sent counts buyers whose in-flight order lines have a locked price differing from this commit's current price for that same line, each of whom receives a catalog.stale_rate_card event with the per-line diff. A buyer whose lines are unaffected by the change is not notified, even though their rate_version no longer names the live commit: the price-at-order-time lock means their booked price was never contingent on staying current. Buyers read the resulting rate entries through avails and catalog surfaces.
200 Response
{
"commit": "acme-2026q4-v2",
"supersedes": "acme-2026q4-v1",
"entries_written": 839,
"errors": [
{
"code": "UNKNOWN_PRODUCT_ID",
"product_id": "acme-primetime",
"week": "2026-W41"
}
],
"warnings": [],
"integrity_status": "pass",
"stale_notifications_sent": 3
}
Errors
Error bodies carry the structured shape described in Errors; the conflict report on a rejected commit names every product in the overlapping set. Statuses here refuse the request: a single bad entry is not one of them, it is an errors[] element on a 200 per the write model above.
Why a price publish can return a catalog error. The catalog integrity rule is about prices, so a rate-card commit is exactly what can break it: a product offering the month grain has to price every week of each month it covers alike, and a card that prices one week of a month differently from another leaves that product with no rate a month line could lock. The commit is refused with a conflict report naming the product and the month. Price the month uniformly, or stop offering the month grain on that product, then re-publish the card.
| Status | Code | When |
|---|---|---|
| 422 | INVALID_INPUT | A malformed commit envelope: a missing commit, a bad effective window, a quote with no quote_expires_at, or no entries[] at all. Entry-level faults are not here: they report per entry in errors[]. |
| 409 | GRAIN_CONFLICT | A product offering the month grain would price some month's weeks unequally. The integrity exception: the whole commit rejects with a conflict report naming the product and the month; see the note above. |
Update a rate card incrementally
PATCH/v1/me/rate-cards/{commit}catalog:write
Reprice a few cells without assembling the whole card. The path names your currently live commit; the body names the new commit this mints.
A patch mints a new commit. It never mutates one. rate_version is what an order pins, and the price-clearance invariant treats it as stable identity: two reads of one rate_version must never return different prices. So the base commit is untouched and stays permanently resolvable by any order citing it, and a body commit equal to the path {commit} is 422 INVALID_INPUT.
| Field | Type | Description |
|---|---|---|
commit required | string | The new commit id. Must differ from the path commit. |
entries_upsert | array of object | Entries to set, in the same shape PUT takes. An entry whose rate key already exists replaces it; one whose key is new is added. |
entries_remove | array of object | Entries to drop, named by rate key: {product_id, week?, advertiser_class, spot_length, buy_type, clearance_tier?, market_class}. |
PATCH /v1/me/rate-cards/acme-2026q4-v2
{
"commit": "acme-2026q4-v3",
"entries_upsert": [
{
"product_id": "acme-prime",
"week": "2026-W41",
"advertiser_class": "general",
"spot_length": 30,
"buy_type": "preemptible",
"clearance_tier": "P2",
"market_class": "scatter",
"gross_rate_unit": 450.00
}
],
"entries_remove": []
}
The platform computes the resulting card, the base commit's entries with your upserts applied and your removals dropped, and publishes it through the same write path PUT uses. So locked_rate_id minting, entry validation, the catalog integrity check, and drift notification all behave identically here, and a cell your patch leaves alone keeps the locked_rate_id a buyer is already holding.
Response
The same body PUT returns, reporting the new commit and the commit it supersedes.
200 Response
{
"commit": "acme-2026q4-v3",
"supersedes": "acme-2026q4-v2",
"entries_written": 840,
"integrity_status": "pass",
"stale_notifications_sent": 1
}
Retries are safe: a byte-identical patch replays the stored response with X-Idempotent-Replay: true rather than minting a second commit, and reusing a commit id with different content returns 409 CONFLICT.
Errors
| Status | Code | When |
|---|---|---|
| 422 | INVALID_INPUT | The body commit equals the path commit, or the patch envelope is malformed. Entry-level faults in entries_upsert report per entry in errors[] exactly as on PUT, and validation is on the card the patch would produce, not on the delta. |
| 404 | NOT_FOUND | The path commit is not your live one. Nothing is created. |
| 409 | INVALID_STATE | The path commit is a quote. Carries details.commit_kind; nothing is computed or written. |
| 409 | CONFLICT | That commit id was already minted from a different patch. |
| 409 | GRAIN_CONFLICT | The resulting card fails the catalog integrity check, exactly as it would on PUT. |
Quotes cannot be patched. A quote commit is a price offered to one buyer, for one campaign, until it expires. Editing one incrementally could change terms after they were quoted. To change quoted terms, publish a new quote with PUT and commit_kind: "quote"; the old one stands until it expires.
Read your live rate card
GET/v1/me/rate-cardscatalog:read
Your card as it stands: the commit in force and the rate card records making it up. Read it before a PATCH, because the path commit that call takes is your currently live one, and read it after any publish to confirm what landed.
Pure read: nothing is written, no commit is minted, and no event fires. A buyer credential is 403, as on every /me route.
Response
| Field | Type | Description |
|---|---|---|
seller | string | Your seller id, echoed as the catalog and avails reads echo it. |
commit | string or null | The commit this response describes. Every record in cards[] carries it. null only when cards[] is empty. |
cards[] | array of object | The rate card records in force, each verbatim, entries included with the locked_rate_id the platform minted. A list, because a commit is a set of records: one commit may hold several records with different effective windows or agreement scopes. Empty, never absent, for a seller with no card at all. Its order is the order the platform holds the records in and carries no meaning: resolve a cell by its rate key, never by position. |
200 Response
{
"seller": "acme",
"commit": "acme-2026q4-v1",
"cards": [
{
"commit": "acme-2026q4-v1",
"currency": "USD",
"effective": { "start": "2026-09-28", "end": "2026-12-27" },
"agreement_scope": null,
"entries": [
{
"product_id": "acme-prime",
"week": "2026-W41",
"advertiser_class": "general",
"spot_length": 30,
"buy_type": "preemptible",
"clearance_tier": "P2",
"market_class": "scatter",
"rate_basis": "per_unit",
"gross_rate_unit": 405.00,
"currency": "USD",
"commit": "acme-2026q4-v1",
"commit_kind": "standing",
"agreement_scope": null,
"locked_rate_id": "lr_pr41p"
}
]
}
]
}
Errors
| Status | Code | When |
|---|---|---|
| 403 | The credential is not a seller credential. |
Read one commit
GET/v1/me/rate-cards/{commit}catalog:read
One named version, live or superseded. An order pins rate_version for its whole life and the price-clearance invariant keeps that commit permanently resolvable, so this is how you read back the card a buyer actually cited, months after you replaced it.
Same envelope as the collection read, with commit echoing the path value. Pure read, on the same authorization.
200 Response
{
"seller": "acme",
"commit": "acme-2026q4-v2",
"cards": [
{
"commit": "acme-2026q4-v2",
"supersedes": "acme-2026q4-v1",
"currency": "USD",
"effective": { "start": "2026-09-28", "end": "2026-12-27" },
"agreement_scope": null,
"entries": [ "..." ]
}
]
}
Errors
| Status | Code | When |
|---|---|---|
| 403 | The credential is not a seller credential. | |
| 404 | NOT_FOUND | You have no commit at that id. Carries details.commit. Another seller's commit answers the same way: not found rather than forbidden, so the endpoint cannot be used to learn that someone else's commit exists. |