> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apostra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Channel groups

> Express composite inventory intent, compile it into media buys, and preserve the same grouping in reporting

A **channel group** is one coherent inventory selection inside a campaign. It
combines AdCP inventory dimensions without inventing new AdCP channel values.
For example, mobile web display means:

```json theme={null}
{
  "channels": ["display"],
  "propertyTypes": ["website"],
  "deviceTypes": ["mobile"]
}
```

It does not mean `channel: "mobile_web"`. `mobile_web` is not an AdCP media
channel.

<Warning>
  Interchange uses the channel-group ID to split and label media buys, and a V3
  `request_proposals` round uses the groups to choose which sellers to ask and
  which returned products to keep (see
  [Proposal rounds follow your channel groups](#proposal-rounds-follow-your-channel-groups)).
  It does not validate products you select against the group's inventory
  definition, or translate that definition into package targeting. Murph and API
  clients must verify discovery evidence and seller product details before
  assigning `channelGroupId`.
</Warning>

Within one inventory dimension, values are alternatives (OR). Across populated
dimensions, declared values combine (AND). A group with `channels: ["ctv",
"olv"]` and `deviceTypes: ["ctv", "mobile"]` therefore allows either channel
and either device, while still declaring both a listed channel and a listed
device. If you need pairwise logic such as “CTV on CTV devices OR OLV on mobile
devices”, make two channel groups.

## Why groups create media-buy boundaries

One downstream AdCP media buy cannot preserve arbitrary Boolean combinations
across channel, property type, device type, and format. Interchange therefore
uses this compilation rule:

```text theme={null}
channel group × seller × settlement currency = one media buy
```

If a campaign contains `mobile-web-display` and `ctv`, products assigned to
those groups become separate media buys even when they use the same seller and
currency. They remain part of one campaign.

## Save pre-built groups

Pass `channelGroups` to `save_campaign` on `/mcp/v3`. A preset is authoring
shorthand. Interchange expands and saves the full definition with the preset
version, so later preset changes do not silently change an existing campaign.

```json theme={null}
{
  "advertiserId": "123",
  "name": "Streaming launch",
  "flight": {
    "startAt": "2027-02-01T00:00:00Z",
    "endAt": "2027-02-28T23:59:59Z"
  },
  "budget": { "total": 50000, "currency": "USD" },
  "channelGroups": [
    {
      "channelGroupId": "mobile-web-display",
      "presetId": "mobile_web_display"
    },
    {
      "channelGroupId": "ctv",
      "presetId": "ctv"
    }
  ],
  "idempotencyKey": "streaming-launch-2027-02"
}
```

The available presets expand as follows:

| Preset | Saved inventory definition |
| - | - |
| `display` | `channels: ["display"]` |
| `olv` | `channels: ["olv"]` |
| `ctv` | `channels: ["ctv"]` |
| `mobile_web_display` | `channels: ["display"]`, `propertyTypes: ["website"]`, `deviceTypes: ["mobile"]`, and AdCP display `formatKinds` (`image`, `html5`, `display_tag`, `image_carousel`, `native_in_feed`, `sponsored_placement`, `responsive_creative`, `custom`) |
| `mobile_web_olv` | `channels: ["olv"]`, `propertyTypes: ["website"]`, `deviceTypes: ["mobile"]` |

The simple channel presets deliberately make no property, device, or format
assumption. For example, `ctv` means the AdCP CTV channel; it does not silently
mean only a physical television device or only a CTV app.

## Define your own group

You can provide the same AdCP-aligned dimensions directly:

```json theme={null}
{
  "channelGroupId": "premium-streaming-video",
  "name": "Premium streaming video",
  "inventory": {
    "channels": ["ctv", "olv"],
    "propertyTypes": ["ctv_app", "website"],
    "deviceTypes": ["ctv", "desktop"],
    "formatKinds": ["video_hosted", "video_vast"]
  }
}
```

Use separate groups when combinations must not collapse into a cross-product.
Do not put an alias such as `mobile_web` into `channels`; aliases belong in the
group name or preset ID, while the saved inventory definition stays aligned to
AdCP.

## Proposal rounds follow your channel groups

When a campaign has channel groups, a V3 `request_proposals` round uses them in
three ways:

1. **Which sellers are asked.** A seller is contacted only when at least one of
   the channels it declares can match a channel in one of your groups. Channel
   aliases match their AdCP value: a seller listed as `audio` matches
   `streaming_audio`. For an official platform integration, such as Spotify or
   Amazon, the platform's own channel coverage is used. A seller that declares
   no channels is still asked, because nothing shows it cannot match.
2. **What each seller is told.** Every contacted seller receives the union of
   your groups' channels as the AdCP `channels` filter on its request. If a
   group names no channel, no channel filter is sent, because that group
   accepts every channel.
3. **Which products come back.** A returned product whose channels, or whose
   creative format kinds, fit none of your groups is dropped, together with any
   proposal that allocates budget to it. The seller's outcome reports the
   number dropped as `channelExcludedProductCount`. A product that does not
   declare a channel or format is kept.

The response lists the eligible sellers that were not contacted in
`skippedSellers`: each entry names the seller, the channels it declares, and
the reason `channel_mismatch`. At most 25 sellers are listed; `total` counts
them all. If no eligible seller can match any group, the round ends `failed`
with `cohortError.code` `NO_ELIGIBLE_SELLERS` and the same `skippedSellers`
list. To include more sellers, add a group for their channel and start a new
round with the new campaign revision.

Seller narrowing needs every group to name at least one channel. If one group
bounds only property type, device type, or format, that group admits sellers of
any channel, so the round asks every eligible seller and sends no channel
filter. Product filtering still
applies the channel and format bounds of the groups that have them. A group
that bounds only property type or device type turns filtering off, because
neither sellers nor returned products declare those dimensions reliably.

A round whose one seller is fixed by the buyer's account, such as a sponsored
buyer or an own-supply sandbox campaign, never lists that seller in
`skippedSellers`. The seller still receives the `channels` filter, and its
returned products are still filtered.

## Assign products when creating a media buy

Discover products, verify that the seller evidence matches one group, then pass
that group's ID when creating the draft. With `save_media_buy`,
`channelGroupId` applies to all products in that creation call:

```json theme={null}
{
  "campaignId": "cmp_123",
  "sellerId": "42",
  "channelGroupId": "mobile-web-display",
  "products": [
    {
      "productId": "sf1:42:bW9iaWxlLWRpc3BsYXk",
      "budget": 15000
    }
  ],
  "idempotencyKey": "pq_123"
}
```

When using the batch `create_media_buys` operation directly, put
`channelGroupId` on each product or proposal selection. Every selection needs a
group ID when its campaign defines channel groups. A media buy records the
group ID and name that created it. Define groups before the campaign creates
its first ungrouped media buy. Once a saved selection or media buy references a
group, keep it immutable; add a new group instead of removing or redefining it.

The batch operation also accepts fully expanded `channelGroups` in
`campaign.create`. Preset shorthand is available through V3 `save_campaign`;
REST clients either send the canonical saved shape inline or create the
campaign first and pass its `campaignId`.

REST does not accept preset shorthand. Send `name`, `source`, and the expanded
inventory snapshot:

```json theme={null}
{
  "channelGroups": [
    {
      "channelGroupId": "mobile-web-display",
      "name": "Mobile web display",
      "source": {
        "kind": "preset",
        "presetId": "mobile_web_display",
        "presetVersion": 1
      },
      "inventory": {
        "channels": ["display"],
        "propertyTypes": ["website"],
        "deviceTypes": ["mobile"],
        "formatKinds": [
          "image",
          "html5",
          "display_tag",
          "image_carousel",
          "native_in_feed",
          "sponsored_placement",
          "responsive_creative",
          "custom"
        ]
      }
    }
  ]
}
```

Always pass product and proposal IDs exactly as discovery returned them. The
qualified `sf1:` value in the example is illustrative; do not construct or
decode it in a client.

## Report by the same group

Use the V3 `get_delivery` tool with the `campaign_delivery` report and the
`channel_group` dimension:

```json theme={null}
{
  "report": "campaign_delivery",
  "metrics": ["impressions", "spend", "clicks"],
  "dimensions": ["channel_group"],
  "range": {
    "startDate": "2026-09-01",
    "endDate": "2026-09-30"
  },
  "filters": {
    "campaignId": "cmp_123"
  }
}
```

To read one group only, add `filters.channelGroupId`. Ungrouped legacy media
buys roll up under a null channel group rather than being guessed from their
names, products, or delivery.

Channel groups answer “which coherent inventory selection did the buyer ask
for?” Campaign targeting answers “who, where, or when should it reach?” Keep
those concepts separate even when both mention a dimension such as device.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.