← Back to product·Simulmedia VAMOS Docs·API reference / Catalog
API reference

Catalog

Publish your catalog

PUT/v1/me/catalogcatalog:write

Publish or update your products and taxonomy. Every update is integrity-checked at ingest, against one rule: a product offering the month grain must price every week of each month it covers alike. A month line locks one rate for the month, so a month whose weeks price differently has no rate to lock. A violation rejects the whole update with a conflict report naming the product and the month, before it can ever produce an order error. Two products covering the same network, days, and hours publish cleanly: overlap is ordinary, and how a seller manages the capacity behind it is the seller's business.

Request body

FieldTypeDescription
taxonomy_version requiredstringThe version this update publishes, e.g. acme-2026q4-v1. Selling titles are versioned data with effective windows, never enums; orders pin this version. See Selling titles.
products[] requiredarray of objectThe products to publish, full shape at product: type, definition, effective window, offered grains, currencies, buy types, and the optional market and visibility declarations.

Example request

PUT /v1/me/catalog

{
  "taxonomy_version": "acme-2026q4-v1",
  "products": [
    {
      "product_id": "acme-prime",
      "type": "daypart",
      "network": "ACME",
      "provider": "National ACME",
      "definition": {
        "selling_title": "Prime",
        "days": ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],
        "start": "20:00",
        "end": "23:00",
        "timezone": "America/New_York",
        "sample_programming": ["Gameday Live","Prime Movie","Late Slate"]
      },
      "effective": { "start": "2026-09-28", "end": "2026-12-27" },
      "grains": ["day","week"],
      "reporting_dayparts": ["primetime"],
      "currencies": ["hh","p2plus","a25_54"],
      "buy_types": ["preemptible","non_preemptible","fixed_position","audience_guaranteed"],
      "spot_lengths": [15, 30, 60],
      "advertiser_classes": ["general","dr","tune_in","competitive","theatrical"]
    }
  ]
}

Response

Products succeed per entry, under the one write model every publish surface shares: a malformed product is reported in errors[] and skipped (INVALID_ENTRY: an unknown type, a definition missing its selling_title, a bad time or timezone, a grains value outside day, week, month, a reporting_dayparts value outside the closed six), the valid products publish, and only an integrity failure rejects the whole update. The version envelope stays atomic either way: one update publishes exactly one taxonomy_version, which every product written by it is versioned under, and that is the version orders pin. On success the response acknowledges the version now current; buyers see it immediately through catalog reads.

What a skipped entry leaves behind is defined per key, by one rule: a failed entry is treated exactly as an entry the update did not provide. PUT is full-replace for the provided entries only, and products the update omits carry forward into the new current version unchanged; a product whose entry fails does the same, keeping its prior published definition under the new envelope version, and a failed entry for a product that never existed leaves nothing behind. So after a partial update, the catalog read returns the carried-forward definition under the new version label, a ?version= pinned read returns the historical snapshot unchanged, and order pins are untouched (the pin is by value at create, and one seller still has exactly one current version at any instant). The envelope becomes current whenever the request is not refused outright, which only the malformed-envelope 422 and the integrity rejection do.

A taxonomy_version is one-shot identity, exactly as a rate-card commit is. Once a version has become current, its content is frozen: re-sending the byte-identical update replays the stored response (a safe retry, nothing re-staged), and re-using the version with different content is 409 CONFLICT, because a ?version= snapshot and every order pinned to that value must never change after the fact (D7's frozen-by-value contract). So the errors[] entry on a partial write is the signal your intended change did not apply, and the correction is a new taxonomy_version carrying the fixed entry: not a replay of this update, a successor to it.

200 Response
{
  "taxonomy_version": "acme-2026q4-v1",
  "products_written": 24,
  "errors": [],
  "warnings": [
    {
      "code": "UNPRICED_DECLARATION",
      "product_id": "acme-prime",
      "spot_length": 60,
      "detail": "declared in spot_lengths; no rate entry prices it in the live card, so it cannot be ordered"
    },
    {
      "code": "UNPRICED_DECLARATION",
      "product_id": "acme-prime",
      "currency": "a18_49",
      "detail": "declared in currencies; no rate entry guarantees it in the live card, so this product cannot be bought on a guarantee in a18_49. Whatever else the card prices stays orderable"
    }
  ],
  "integrity_status": "pass"
}

Declared-but-unpriced values warn at commit. A product's spot_lengths, advertiser_classes, buy_types, and currencies bound what a rate card may price, and each can over-declare: a declared length with no rate entry cannot be ordered from the catalog, which is a disclosure rather than an error, but an over-declaration nobody notices is inventory a buyer can see and cannot buy. So the commit response warns (UNPRICED_DECLARATION), and the catalog read surfaces the same fact to buyers, so offered and merely declared are distinguishable on both sides.

What an unpriced value predicts depends on its axis. An unpriced spot_length, buy_type, or advertiser_class cannot be ordered: all three are matched exactly against the rate entry at order create, with no fallback. An unpriced currency blocks nothing: it withdraws the audience guarantee in that currency, and whatever else the card prices stays orderable. Note that this names no rate basis, on purpose. A currency goes unpriced two ways: on a spot-cost card, whose entries carry no audience_code at all and so can never satisfy one, and on a cpm-priced card that guarantees some declared audiences and not others, which has no per-unit entry anywhere. The first case is the one that recurs: a spot-cost product that measures an audience it does not guarantee names that audience here as a matter of course, which is the normal shape of scatter rather than a defect in the card.

The check is per axis value, deliberately coarse, so it is computable from the contract alone. A declared value is unpriced when no rate entry on the resolving card prices this product with that value in any ISO week of the product's effective window: each spot_lengths value against the entries' spot_length, each buy_types value against buy_type, each advertiser_classes value against advertiser_class, and each currencies audience code against the audience_code carried by cpm-priced and audience_guaranteed entries (spot-cost entries carry none, so they never satisfy a currency). Every warning and every unpriced[] element names exactly one {axis: value} pair, e.g. {"spot_length": 60}. Partially priced grids do not warn, on purpose: a length priced for any buy type in any week is priceable by this rule, because enumerating the full rate-key product (week x class x length x buy type x tier x market class) would bury the one signal worth having, declared and never priced anywhere. The resolving card differs by side, and that too is deliberate: the seller-side commit warning checks the seller's current standing open commit (quote commits never count: a per-campaign offer to one buyer is not the catalog's price), while the buyer-side unpriced[] is computed against the card as resolved for that buyer, the open card plus their agreement-scoped commits, so two buyers may legitimately read different unpriced[] on one product, exactly as they read different rates.

Errors

Error bodies carry the structured shape described in Errors. Statuses here refuse the request; a single malformed product is instead an errors[] element on a 200, per the write model above.

StatusCodeWhen
422INVALID_INPUTA malformed update envelope: a missing taxonomy_version, or no products[] at all.
409CONFLICTThe taxonomy_version was already made current by an update with different content. Byte-identical resubmission replays instead; a correction mints a new version. See the one-shot identity rule above.
409GRAIN_CONFLICTA product offering the month grain prices some month's weeks unequally. The conflict report names the product and the month; nothing from the update is applied.
Publishing for the first time? Publish your catalog walks the full flow: products, rate card, first avails push.

List sellers

GET/v1/sellerscatalog:read

Discovery within your authorized scope: private catalogs your agreements unlock, plus open catalogs.

Response

One entry per seller you can transact with, with its networks, capability level, and catalog location.

200 Response
{
  "sellers": [
    {
      "seller": "acme",
      "name": "Acme Broadcasting",
      "networks": ["ACME"],
      "level": 2,
      "catalog_url": "/v1/sellers/acme/catalog"
    }
  ]
}

Read a seller's catalog

GET/v1/sellers/{seller}/catalogcatalog:read

Returns the seller's products, taxonomy version, buy types, and rate class availability. Respects your agreement scope: buyer-scoped products appear only when your agreement carries them.

Query parameters

FieldTypeDescription
as_ofISO dateFilter to products effective on this date. Default: today.
versionstringPin to a specific taxonomy_version, e.g. acme-2026q4-v1.
typeenumFilter by product type, e.g. ?type=daypart.
buy_typeenumFilter to products supporting a buy type, for example ?buy_type=preemptible. The retired deal_type parameter is rejected with 422 INVALID_INPUT; details.parameter identifies it and names buy_type as the replacement.
marketstringFilter by market as scheme:code, e.g. ?market=nielsen_dma:501. Products with no market are national.
distributionenumFilter by the inventory layer, one of the distributions, for example ?distribution=syndication. A value off the enum is rejected with 422 INVALID_INPUT and details.parameter. Products with no distribution have not stated one and match no value.

Example request

GET /v1/sellers/acme/catalog?as_of=2026-10-05

Response

Each entry in products[] is a product; the field-by-field reference lives there. The interesting parts below: the definition resolves days and local times against the declared timezone, sample_programming lists representative titles (required for ROS and rotator selling titles), and spot_lengths and advertiser_classes bound what a rate card can price. Where a declared combination has no rate entry in the live card, the product carries a read-side, derived unpriced[] naming it (e.g. {"spot_length": 60}), so a buyer can tell offered from merely declared without diffing the card; see the commit-side warning.

200 Response
{
  "seller": "acme",
  "taxonomy_version": "acme-2026q4-v1",
  "effective": {
    "start": "2026-09-28",
    "end": "2026-12-27"
  },
  "products": [
    {
      "product_id": "acme-prime",
      "type": "daypart",
      "network": "ACME",
      "provider": "National ACME",
      "definition": {
        "selling_title": "Prime",
        "days": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
        "start": "20:00",
        "end": "23:00",
        "timezone": "America/New_York",
        "sample_programming": ["Gameday Live", "Prime Movie", "Late Slate"]
      },
      "effective": {
        "start": "2026-09-28",
        "end": "2026-12-27"
      },
      "grains": ["day", "week"],
      "reporting_dayparts": ["primetime"],
      "currencies": ["hh", "p2plus", "a25_54", "a18_49"],
      "buy_types": ["preemptible", "non_preemptible", "fixed_position", "audience_guaranteed"],
      "spot_lengths": [15, 30, 60],
      "advertiser_classes": ["general", "dr", "tune_in", "competitive", "theatrical", "political_candidate_lur", "political_issue"]
    }
  ]
}

The catalog you get is the catalog you are entitled to. A product marked visibility: "by_agreement" lists only for a buyer the seller's relationship grants it to, and a product not granted to you is simply absent from this response: not refused, not flagged, absent. A direct read of one returns 404, the same answer a product that does not exist gets. You never see who else was granted it. See who can see a product.

Optional declarations appear when the seller makes them: market ({scheme, code, name, country}) scopes a product to a local market, grains names the purchase periods it instantiates at, visibility states whether it lists openly or by agreement, distribution names the inventory layer, separation_policy states the seller's competitive-separation rule, posting_policy states when a shortfall owes a make-good, and a sponsorship product may carry a billboard block. A program airing on irregular dates defines itself by occurrences instead of days, start, end:

"definition": {
  "selling_title": "Aces Baseball",
  "timezone": "America/Chicago",
  "occurrences": [
    { "date": "2027-04-09", "start": "18:30", "end": "22:00", "label": "home opener" }
  ]
}