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

# create_media_buy

> create_media_buy task — create advertising campaigns in AdCP from discovered products. Handles packages, budgets, flight dates, governance rules, and approval workflows.

Create a media buy from selected packages or execute a proposal. Handles validation, approval if needed, and campaign creation.

Supports two modes:

* **Manual Mode**: Provide `packages` array with explicit line item configurations
* **Proposal Mode**: Provide `proposal_id` and `total_budget` to execute a proposal from `get_products`

**Response Time**: Instant to days (returns `completed`, `working` \< 120s, or `submitted` for hours/days)

**Request Schema**: [`/schemas/v2/media-buy/create-media-buy-request.json`](https://adcontextprotocol.org/schemas/v2/media-buy/create-media-buy-request.json)
**Response Schema**: [`/schemas/v2/media-buy/create-media-buy-response.json`](https://adcontextprotocol.org/schemas/v2/media-buy/create-media-buy-response.json)

## Quick Start

Create a simple media buy with two packages:

<CodeGroup>
  ```javascript JavaScript test=false theme={null}
  import { testAgent } from '@adcp/client/testing';
  import { CreateMediaBuyResponseSchema } from '@adcp/client';

  // Calculate dates dynamically - start tomorrow, end in 90 days
  const tomorrow = new Date();
  tomorrow.setDate(tomorrow.getDate() + 1);
  tomorrow.setHours(0, 0, 0, 0);
  const endDate = new Date(tomorrow);
  endDate.setDate(endDate.getDate() + 90);

  const result = await testAgent.createMediaBuy({
    buyer_ref: 'summer_campaign_' + Date.now(),
    brand: {
      domain: 'acmecorp.com'
    },
    packages: [
      {
        buyer_ref: 'display_package_1',
        product_id: 'prod_d979b543',
        pricing_option_id: 'cpm_usd_auction',
        format_ids: [
          {
            agent_url: 'https://creative.adcontextprotocol.org',
            id: 'display_300x250_image'
          }
        ],
        budget: 2500,
        bid_price: 5.00
      },
      {
        buyer_ref: 'display_package_2',
        product_id: 'prod_e8fd6012',
        pricing_option_id: 'cpm_usd_auction',
        format_ids: [
          {
            agent_url: 'https://creative.adcontextprotocol.org',
            id: 'display_300x250_html'
          }
        ],
        budget: 2500,
        bid_price: 4.50
      }
    ],
    start_time: tomorrow.toISOString(),
    end_time: endDate.toISOString()
  });

  if (!result.success) {
    throw new Error(`Request failed: ${result.error}`);
  }

  // Validate response against schema
  const validated = CreateMediaBuyResponseSchema.parse(result.data);

  // Check for errors (discriminated union response)
  if ('errors' in validated && validated.errors) {
    throw new Error(`Failed to create media buy: ${JSON.stringify(validated.errors)}`);
  }

  if ('media_buy_id' in validated) {
    console.log(`Created media buy ${validated.media_buy_id}`);
    console.log(`Upload creatives by: ${validated.creative_deadline}`);
    console.log(`Packages created: ${validated.packages.length}`);
  }
  ```

  ```python Python test=false theme={null}
  import asyncio
  import time
  from datetime import datetime, timedelta, timezone
  from adcp.testing import test_agent

  async def create_campaign():
      # Calculate dates dynamically - start tomorrow, end in 90 days
      tomorrow = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
      end_date = tomorrow + timedelta(days=90)

      result = await test_agent.simple.create_media_buy(
          buyer_ref=f'summer_campaign_{int(time.time() * 1000)}',
          brand={
              'domain': 'acmecorp.com'
          },
          packages=[
              {
                  'buyer_ref': 'ctv_package',
                  'product_id': 'prod_d979b543',
                  'pricing_option_id': 'cpm_usd_auction',
                  'format_ids': [
                      {
                          'agent_url': 'https://creative.adcontextprotocol.org',
                          'id': 'display_300x250_image'
                      }
                  ],
                  'budget': 2500,
                  'bid_price': 5.00
              },
              {
                  'buyer_ref': 'audio_package',
                  'product_id': 'prod_e8fd6012',
                  'pricing_option_id': 'cpm_usd_auction',
                  'format_ids': [
                      {
                          'agent_url': 'https://creative.adcontextprotocol.org',
                          'id': 'display_300x250_html'
                      }
                  ],
                  'budget': 2500,
                  'bid_price': 4.50
              }
          ],
          start_time=tomorrow.isoformat().replace('+00:00', 'Z'),
          end_time=end_date.isoformat().replace('+00:00', 'Z')
      )

      # Check for errors (discriminated union response)
      if hasattr(result, 'errors') and result.errors:
          raise Exception(f"Failed to create media buy: {result.errors}")

      print(f"Created media buy {result.media_buy_id}")
      print(f"Upload creatives by: {result.creative_deadline}")
      print(f"Packages created: {len(result.packages)}")

  asyncio.run(create_campaign())
  ```

  ```bash CLI test=false theme={null}
  npx adcp \
    https://test-agent.adcontextprotocol.org/mcp \
    create_media_buy \
    '{"buyer_ref":"summer_campaign_2025","brand":{"domain":"acmecorp.com"},"packages":[{"buyer_ref":"display_package_1","product_id":"prod_d979b543","pricing_option_id":"cpm_usd_auction","format_ids":[{"agent_url":"https://creative.adcontextprotocol.org","id":"display_300x250_image"}],"budget":30000,"bid_price":5.00},{"buyer_ref":"display_package_2","product_id":"prod_e8fd6012","pricing_option_id":"cpm_usd_auction","format_ids":[{"agent_url":"https://creative.adcontextprotocol.org","id":"display_300x250_html"}],"budget":20000,"bid_price":4.50}],"start_time":"2025-06-01T00:00:00Z","end_time":"2025-08-31T23:59:59Z"}' \
    --auth 1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ
  ```
</CodeGroup>

## Request Parameters

| Parameter            | Type                                                                                             | Required | Description                                                                                                                                                                          |
| -------------------- | ------------------------------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `buyer_ref`          | string                                                                                           | Yes      | Your reference identifier for this media buy                                                                                                                                         |
| `buyer_campaign_ref` | string                                                                                           | No       | Buyer's campaign label for CRM and ad server correlation (e.g., `"NovaDrink_Meals_Q2"`). Groups related discovery and buy operations.                                                |
| `account`            | [account-ref](/dist/docs/3.0.0-rc.2/building/integration/accounts-and-agents#account-references) | Yes      | Account reference. Pass `{ "account_id": "..." }` or `{ "brand": {...}, "operator": "..." }` if the seller supports implicit resolution. Required for billing and policy evaluation. |
| `proposal_id`        | string                                                                                           | No\*     | ID of a proposal from `get_products` to execute. Alternative to providing packages.                                                                                                  |
| `total_budget`       | TotalBudget                                                                                      | No\*     | Total budget when executing a proposal. Publisher applies allocation percentages.                                                                                                    |
| `packages`           | Package\[]                                                                                       | No\*     | Array of package configurations (see below). Required when not using proposal\_id.                                                                                                   |
| `brand`              | BrandRef                                                                                         | Yes      | Brand reference — resolved to full identity at execution time. See [brand.json](/dist/docs/3.0.0-rc.2/brand-protocol/brand-json)                                                     |
| `start_time`         | string                                                                                           | Yes      | `"asap"` or ISO 8601 date-time                                                                                                                                                       |
| `end_time`           | string                                                                                           | Yes      | ISO 8601 date-time (UTC unless timezone specified)                                                                                                                                   |
| `po_number`          | string                                                                                           | No       | Purchase order number                                                                                                                                                                |
| `reporting_webhook`  | ReportingWebhook                                                                                 | No       | Automated reporting delivery configuration                                                                                                                                           |

\* Either `packages` OR (`proposal_id` + `total_budget`) must be provided.

### TotalBudget Object

| Parameter  | Type   | Required | Description            |
| ---------- | ------ | -------- | ---------------------- |
| `amount`   | number | Yes      | Total budget amount    |
| `currency` | string | Yes      | ISO 4217 currency code |

### Package Object

| Parameter              | Type                                                                                            | Required | Description                                                                                                                                                                                                                                                                                                                                                                                                                          |
| ---------------------- | ----------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `buyer_ref`            | string                                                                                          | Yes      | Your reference for this package                                                                                                                                                                                                                                                                                                                                                                                                      |
| `product_id`           | string                                                                                          | Yes      | Product ID from `get_products`                                                                                                                                                                                                                                                                                                                                                                                                       |
| `pricing_option_id`    | string                                                                                          | Yes      | Pricing option ID from product's `pricing_options` array                                                                                                                                                                                                                                                                                                                                                                             |
| `format_ids`           | FormatID\[]                                                                                     | Yes      | Format IDs that will be used - must be supported by product                                                                                                                                                                                                                                                                                                                                                                          |
| `budget`               | number                                                                                          | Yes      | Budget in currency specified by pricing option                                                                                                                                                                                                                                                                                                                                                                                       |
| `impressions`          | number                                                                                          | No       | Impression goal for this package                                                                                                                                                                                                                                                                                                                                                                                                     |
| `paused`               | boolean                                                                                         | No       | Create package in paused state (default: `false`)                                                                                                                                                                                                                                                                                                                                                                                    |
| `pacing`               | string                                                                                          | No       | `"even"` (default), `"asap"`, or `"front_loaded"`                                                                                                                                                                                                                                                                                                                                                                                    |
| `bid_price`            | number                                                                                          | No       | Bid price for auction pricing. This is the exact bid/price to honor unless the selected pricing option has `max_bid: true`, in which case it is treated as the buyer's maximum willingness to pay (ceiling).                                                                                                                                                                                                                         |
| `optimization_goals`   | [OptimizationGoal\[\]](/dist/docs/3.0.0-rc.2/media-buy/conversion-tracking/#optimization-goals) | No       | Optimization targets for this package. Each goal is either `kind: "event"` (conversion events with `event_sources` array, optional `cost_per`, `per_ad_spend`, or `maximize_value` target) or `kind: "metric"` (seller-native metric with optional `cost_per` or `threshold_rate` target). Event goals require `conversion_tracking.supported_targets` on the product; metric goals require `metric_optimization.supported_metrics`. |
| `targeting_overlay`    | TargetingOverlay                                                                                | No       | Additional targeting criteria (see [Targeting](/dist/docs/3.0.0-rc.2/media-buy/advanced-topics/targeting))                                                                                                                                                                                                                                                                                                                           |
| `start_time`           | string                                                                                          | No       | ISO 8601 date-time for this package's flight start. When omitted, inherits the media buy's `start_time`. Must fall within the media buy's date range. Does not support `"asap"`.                                                                                                                                                                                                                                                     |
| `end_time`             | string                                                                                          | No       | ISO 8601 date-time for this package's flight end. When omitted, inherits the media buy's `end_time`. Must fall within the media buy's date range.                                                                                                                                                                                                                                                                                    |
| `creative_assignments` | CreativeAssignment\[]                                                                           | No       | Assign existing library creatives with optional weights and placement targeting                                                                                                                                                                                                                                                                                                                                                      |
| `creatives`            | CreativeAsset\[]                                                                                | No       | Upload new creative assets and assign (`creative_id` must not already exist in library)                                                                                                                                                                                                                                                                                                                                              |

## Response

### Success Response

| Field                | Description                                     |
| -------------------- | ----------------------------------------------- |
| `media_buy_id`       | Publisher's unique identifier                   |
| `buyer_ref`          | Your reference identifier                       |
| `buyer_campaign_ref` | Buyer's campaign label (echoed from request)    |
| `creative_deadline`  | ISO 8601 timestamp for creative upload deadline |
| `packages`           | Array of created packages with complete state   |

### Error Response

| Field    | Description                               |
| -------- | ----------------------------------------- |
| `errors` | Array of error objects explaining failure |

**Note**: Responses use discriminated unions - you get either success fields OR errors, never both. Always check for `errors` before accessing success fields.

## Common Scenarios

### Campaign with Targeting

Add geographic restrictions and frequency capping:

<CodeGroup>
  ```javascript JavaScript test=false theme={null}
  import { testAgent } from '@adcp/client/testing';
  import { CreateMediaBuyResponseSchema } from '@adcp/client';

  // Calculate end date dynamically - 90 days from now
  const endDate = new Date();
  endDate.setDate(endDate.getDate() + 90);

  const result = await testAgent.createMediaBuy({
    buyer_ref: 'regional_campaign_' + Date.now(),
    brand: {
      domain: 'acmecorp.com'
    },
    packages: [{
      buyer_ref: 'ctv_regional',
      product_id: 'prod_d979b543',
      pricing_option_id: 'cpm_usd_auction',
      format_ids: [{
        agent_url: 'https://creative.adcontextprotocol.org',
        id: 'display_300x250_image'
      }],
      budget: 2500,
      bid_price: 5.00,
      targeting_overlay: {
        geo_countries: ['US'],
        geo_regions: ['US-CA', 'US-NY'],
        frequency_cap: {
          suppress: { interval: 60, unit: 'minutes' }
        }
      }
    }],
    start_time: 'asap',
    end_time: endDate.toISOString()
  });

  if (!result.success) {
    throw new Error(`Request failed: ${result.error}`);
  }

  const validated = CreateMediaBuyResponseSchema.parse(result.data);
  if ('errors' in validated && validated.errors) {
    throw new Error(`Creation failed: ${JSON.stringify(validated.errors)}`);
  }

  if ('media_buy_id' in validated) {
    console.log(`Campaign ${validated.media_buy_id} created with targeting`);
  }
  ```

  ```python Python test=false theme={null}
  import asyncio
  import time
  from datetime import datetime, timedelta, timezone
  from adcp.testing import test_agent

  async def create_targeted_campaign():
      # Calculate end date dynamically - 90 days from now
      end_date = datetime.now(timezone.utc) + timedelta(days=90)

      result = await test_agent.simple.create_media_buy(
          buyer_ref=f'regional_campaign_{int(time.time() * 1000)}',
          brand={
              'domain': 'acmecorp.com'
          },
          packages=[{
              'buyer_ref': 'ctv_regional',
              'product_id': 'prod_d979b543',
              'pricing_option_id': 'cpm_usd_auction',
              'format_ids': [{
                  'agent_url': 'https://creative.adcontextprotocol.org',
                  'id': 'display_300x250_image'
              }],
              'budget': 2500,
              'bid_price': 5.00,
              'targeting_overlay': {
                  'geo_countries': ['US'],
                  'geo_regions': ['US-CA', 'US-NY'],
                  'frequency_cap': {
                      'suppress': {'interval': 60, 'unit': 'minutes'}
                  }
              }
          }],
          start_time='asap',
          end_time=end_date.isoformat().replace('+00:00', 'Z')
      )

      if hasattr(result, 'errors') and result.errors:
          raise Exception(f"Creation failed: {result.errors}")

      print(f"Campaign {result.media_buy_id} created with targeting")

  asyncio.run(create_targeted_campaign())
  ```
</CodeGroup>

### Campaign with Conversion Optimization

Set a per\_ad\_spend target for conversion-optimized delivery. The product must declare support in `conversion_tracking.supported_targets`, and you must have an event source configured via `sync_event_sources`:

<CodeGroup>
  ```javascript JavaScript test=false theme={null}
  import { testAgent } from '@adcp/client/testing';
  import { CreateMediaBuyResponseSchema } from '@adcp/client';

  const endDate = new Date();
  endDate.setDate(endDate.getDate() + 90);

  const result = await testAgent.createMediaBuy({
    buyer_ref: 'performance_campaign_' + Date.now(),
    brand: {
      domain: 'acmecorp.com'
    },
    packages: [{
      buyer_ref: 'roas_package',
      product_id: 'prod_retail_sp',
      pricing_option_id: 'cpc_usd_auction',
      budget: 10000,
      bid_price: 1.20,
      optimization_goals: [{
        kind: 'event',
        event_sources: [
          { event_source_id: 'retailer_sales', event_type: 'purchase', value_field: 'value' }
        ],
        target: { kind: 'per_ad_spend', value: 4.0 },
        priority: 1
      }]
    }],
    start_time: 'asap',
    end_time: endDate.toISOString()
  });

  if (!result.success) {
    throw new Error(`Request failed: ${result.error}`);
  }

  const validated = CreateMediaBuyResponseSchema.parse(result.data);
  if ('errors' in validated && validated.errors) {
    throw new Error(`Creation failed: ${JSON.stringify(validated.errors)}`);
  }

  if ('media_buy_id' in validated) {
    console.log(`Campaign ${validated.media_buy_id} created with per_ad_spend target`);
  }
  ```

  ```python Python test=false theme={null}
  import asyncio
  import time
  from datetime import datetime, timedelta, timezone
  from adcp.testing import test_agent

  async def create_optimized_campaign():
      end_date = datetime.now(timezone.utc) + timedelta(days=90)

      result = await test_agent.simple.create_media_buy(
          buyer_ref=f'performance_campaign_{int(time.time() * 1000)}',
          brand={
              'domain': 'acmecorp.com'
          },
          packages=[{
              'buyer_ref': 'roas_package',
              'product_id': 'prod_retail_sp',
              'pricing_option_id': 'cpc_usd_auction',
              'budget': 10000,
              'bid_price': 1.20,
              'optimization_goals': [{
                  'kind': 'event',
                  'event_sources': [
                      { 'event_source_id': 'retailer_sales', 'event_type': 'purchase', 'value_field': 'value' }
                  ],
                  'target': { 'kind': 'per_ad_spend', 'value': 4.0 },
                  'priority': 1
              }]
          }],
          start_time='asap',
          end_time=end_date.isoformat().replace('+00:00', 'Z')
      )

      if hasattr(result, 'errors') and result.errors:
          raise Exception(f"Creation failed: {result.errors}")

      print(f"Campaign {result.media_buy_id} created with per_ad_spend target")

  asyncio.run(create_optimized_campaign())
  ```
</CodeGroup>

### Catalog-driven packages

A catalog-driven package allocates a single budget envelope to an entire catalog of items. Instead of creating separate packages per item, the platform optimizes delivery across all catalog items based on performance. This is the AdCP equivalent of catalog-based campaign types such as Google Performance Max or Meta Dynamic Product Ads.

Include the `catalogs` field in a package to make it catalog-driven. Each catalog should have a distinct type (e.g., one product catalog, one store catalog). The referenced catalogs must already be synced via [`sync_catalogs`](/dist/docs/3.0.0-rc.2/media-buy/task-reference/sync_catalogs).

**Job campaign with synced job catalog:**

```json test=false theme={null}
{
  "buyer_ref": "recruitment_q1",
  "brand": { "domain": "acme-restaurants.com" },
  "packages": [{
    "buyer_ref": "chef_vacancies",
    "product_id": "prod_job_board",
    "pricing_option_id": "cpc_eur_auction",
    "budget": 5000,
    "bid_price": 2.50,
    "catalogs": [{
      "catalog_id": "chef-vacancies",
      "type": "job"
    }]
  }],
  "start_time": "asap",
  "end_time": "2026-06-30T23:59:59Z"
}
```

**Retail media with product catalog and store catchment targeting:**

```json test=false theme={null}
{
  "buyer_ref": "summer_promo",
  "brand": { "domain": "acmecorp.com" },
  "packages": [{
    "buyer_ref": "sponsored_products",
    "product_id": "prod_retail_sp",
    "pricing_option_id": "cpc_usd_auction",
    "budget": 10000,
    "bid_price": 1.20,
    "catalogs": [{
      "catalog_id": "gmc-primary",
      "type": "product",
      "tags": ["summer"]
    }],
    "targeting_overlay": {
      "store_catchments": [{
        "catalog_id": "retail-locations",
        "catchment_ids": ["drive"]
      }]
    }
  }],
  "start_time": "asap",
  "end_time": "2026-09-30T23:59:59Z"
}
```

The platform distributes budget across catalog items based on performance. For per-item reporting, use [`get_media_buy_delivery`](/dist/docs/3.0.0-rc.2/media-buy/task-reference/get_media_buy_delivery) which returns `by_catalog_item` breakdowns. Creative variants for catalog-driven packages represent individual catalog items rendered as ads.

### Campaign with Inline Creatives

Upload creatives at the same time as creating the campaign:

<CodeGroup>
  ```javascript JavaScript test=false theme={null}
  import { testAgent } from '@adcp/client/testing';
  import { CreateMediaBuyResponseSchema } from '@adcp/client';

  // Calculate end date dynamically - 90 days from now
  const endDate = new Date();
  endDate.setDate(endDate.getDate() + 90);

  const result = await testAgent.createMediaBuy({
    buyer_ref: 'campaign_with_creatives_' + Date.now(),
    brand: {
      domain: 'acmecorp.com'
    },
    packages: [{
      buyer_ref: 'ctv_package',
      product_id: 'prod_d979b543',
      pricing_option_id: 'cpm_usd_auction',
      format_ids: [{
        agent_url: 'https://creative.adcontextprotocol.org',
        id: 'display_300x250_image'
      }],
      budget: 2500,
      bid_price: 5.00,
      creatives: [{
        creative_id: 'hero_video_30s',
        name: 'Hero Video',
        format_id: {
          agent_url: 'https://creative.adcontextprotocol.org',
          id: 'display_300x250_image'
        },
        assets: {
          image: {
            url: 'https://cdn.example.com/hero-banner.jpg',
            width: 300,
            height: 250
          }
        }
      }]
    }],
    start_time: 'asap',
    end_time: endDate.toISOString()
  });

  if (!result.success) {
    throw new Error(`Request failed: ${result.error}`);
  }

  const validated = CreateMediaBuyResponseSchema.parse(result.data);
  if ('errors' in validated && validated.errors) {
    throw new Error(`Creation failed: ${JSON.stringify(validated.errors)}`);
  }

  if ('packages' in validated) {
    console.log(`Campaign created with ${validated.packages[0].creative_assignments.length} creatives`);
  }
  ```

  ```python Python test=false theme={null}
  import asyncio
  import time
  from datetime import datetime, timedelta, timezone
  from adcp.testing import test_agent

  async def create_with_creatives():
      # Calculate end date dynamically - 90 days from now
      end_date = datetime.now(timezone.utc) + timedelta(days=90)

      result = await test_agent.simple.create_media_buy(
          buyer_ref=f'campaign_with_creatives_{int(time.time() * 1000)}',
          brand={
              'domain': 'acmecorp.com'
          },
          packages=[{
              'buyer_ref': 'ctv_package',
              'product_id': 'prod_d979b543',
              'pricing_option_id': 'cpm_usd_auction',
              'format_ids': [{
                  'agent_url': 'https://creative.adcontextprotocol.org',
                  'id': 'display_300x250_image'
              }],
              'budget': 2500,
              'bid_price': 5.00,
              'creatives': [{
                  'creative_id': 'hero_video_30s',
                  'name': 'Hero Video',
                  'format_id': {
                      'agent_url': 'https://creative.adcontextprotocol.org',
                      'id': 'display_300x250_image'
                  },
                  'assets': {
                      'image': {
                          'url': 'https://cdn.example.com/hero-banner.jpg',
                          'width': 300,
                          'height': 250
                      }
                  }
              }]
          }],
          start_time='asap',
          end_time=end_date.isoformat().replace('+00:00', 'Z')
      )

      if hasattr(result, 'errors') and result.errors:
          raise Exception(f"Creation failed: {result.errors}")

      print(f"Campaign created with {len(result.packages[0].creative_assignments)} creatives")

  asyncio.run(create_with_creatives())
  ```
</CodeGroup>

### Campaign with Reporting Webhook

Receive automated reporting notifications:

<CodeGroup>
  ```javascript JavaScript test=false theme={null}
  import { testAgent } from '@adcp/client/testing';
  import { CreateMediaBuyResponseSchema } from '@adcp/client';

  // Calculate end date dynamically - 90 days from now
  const endDate = new Date();
  endDate.setDate(endDate.getDate() + 90);

  const result = await testAgent.createMediaBuy({
    buyer_ref: 'campaign_with_reporting_' + Date.now(),
    brand: {
      domain: 'acmecorp.com'
    },
    packages: [{
      buyer_ref: 'ctv_package',
      product_id: 'prod_d979b543',
      pricing_option_id: 'cpm_usd_auction',
      format_ids: [{
        agent_url: 'https://creative.adcontextprotocol.org',
        id: 'display_300x250_image'
      }],
      budget: 2500,
      bid_price: 5.00
    }],
    start_time: 'asap',
    end_time: endDate.toISOString(),
    reporting_webhook: {
      url: 'https://buyer.example.com/webhooks/reporting',
      authentication: {
        schemes: ['Bearer'],
        credentials: 'secret_token_xyz_minimum_32_chars'
      },
      reporting_frequency: 'daily',
      requested_metrics: ['impressions', 'spend', 'video_completions']
    }
  });

  if (!result.success) {
    throw new Error(`Request failed: ${result.error}`);
  }

  const validated = CreateMediaBuyResponseSchema.parse(result.data);
  if ('errors' in validated && validated.errors) {
    throw new Error(`Creation failed: ${JSON.stringify(validated.errors)}`);
  }

  if ('media_buy_id' in validated) {
    console.log(`Campaign created - daily reports will be sent to webhook`);
  }
  ```

  ```python Python test=false theme={null}
  import asyncio
  import time
  from datetime import datetime, timedelta, timezone
  from adcp.testing import test_agent

  async def create_with_reporting():
      # Calculate end date dynamically - 90 days from now
      end_date = datetime.now(timezone.utc) + timedelta(days=90)

      result = await test_agent.simple.create_media_buy(
          buyer_ref=f'campaign_with_reporting_{int(time.time() * 1000)}',
          brand={
              'domain': 'acmecorp.com'
          },
          packages=[{
              'buyer_ref': 'ctv_package',
              'product_id': 'prod_d979b543',
              'pricing_option_id': 'cpm_usd_auction',
              'format_ids': [{
                  'agent_url': 'https://creative.adcontextprotocol.org',
                  'id': 'display_300x250_image'
              }],
              'budget': 2500,
              'bid_price': 5.00
          }],
          start_time='asap',
          end_time=end_date.isoformat().replace('+00:00', 'Z'),
          reporting_webhook={
              'url': 'https://buyer.example.com/webhooks/reporting',
              'authentication': {
                  'schemes': ['Bearer'],
                  'credentials': 'secret_token_xyz_minimum_32_chars'
              },
              'reporting_frequency': 'daily',
              'requested_metrics': ['impressions', 'spend', 'video_completions']
          }
      )

      if hasattr(result, 'errors') and result.errors:
          raise Exception(f"Creation failed: {result.errors}")

      print('Campaign created - daily reports will be sent to webhook')

  asyncio.run(create_with_reporting())
  ```
</CodeGroup>

### Executing a Proposal

Execute a proposal from `get_products` without manually constructing packages:

<CodeGroup>
  ```javascript JavaScript test=false theme={null}
  import { testAgent } from '@adcp/client/testing';
  import { CreateMediaBuyResponseSchema } from '@adcp/client';

  // Calculate end date dynamically - 90 days from now
  const endDate = new Date();
  endDate.setDate(endDate.getDate() + 90);

  const result = await testAgent.createMediaBuy({
    buyer_ref: 'swiss_campaign_' + Date.now(),
    proposal_id: 'swiss_balanced_v1',  // From get_products response
    total_budget: {
      amount: 50000,
      currency: 'USD'
    },
    brand: {
      domain: 'acmecorp.com'
    },
    start_time: 'asap',
    end_time: endDate.toISOString()
  });

  if (!result.success) {
    throw new Error(`Request failed: ${result.error}`);
  }

  const validated = CreateMediaBuyResponseSchema.parse(result.data);
  if ('errors' in validated && validated.errors) {
    throw new Error(`Creation failed: ${JSON.stringify(validated.errors)}`);
  }

  if ('media_buy_id' in validated) {
    // Publisher converted proposal allocations to packages
    console.log(`Created media buy ${validated.media_buy_id}`);
    console.log(`Packages created: ${validated.packages.length}`);
  }
  ```

  ```python Python test=false theme={null}
  import asyncio
  import time
  from datetime import datetime, timedelta, timezone
  from adcp.testing import test_agent

  async def execute_proposal():
      # Calculate end date dynamically - 90 days from now
      end_date = datetime.now(timezone.utc) + timedelta(days=90)

      result = await test_agent.simple.create_media_buy(
          buyer_ref=f'swiss_campaign_{int(time.time() * 1000)}',
          proposal_id='swiss_balanced_v1',  # From get_products response
          total_budget={
              'amount': 50000,
              'currency': 'USD'
          },
          brand={
              'domain': 'acmecorp.com'
          },
          start_time='asap',
          end_time=end_date.isoformat().replace('+00:00', 'Z')
      )

      if hasattr(result, 'errors') and result.errors:
          raise Exception(f"Creation failed: {result.errors}")

      # Publisher converted proposal allocations to packages
      print(f"Created media buy {result.media_buy_id}")
      print(f"Packages created: {len(result.packages)}")

  asyncio.run(execute_proposal())
  ```
</CodeGroup>

When executing a proposal:

* The publisher converts allocation percentages to actual budgets using `total_budget`
* Packages are created automatically based on the proposal's allocations
* All other fields (brand, start\_time, end\_time, etc.) work the same as manual mode

See [Proposals](/dist/docs/3.0.0-rc.2/media-buy/product-discovery/media-products#proposals) for the complete workflow.

## Error Handling

Common errors and resolutions:

| Error Code               | Description                           | Resolution                                                                                                          |
| ------------------------ | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `PRODUCT_NOT_FOUND`      | Invalid product\_id                   | Verify product exists via `get_products`                                                                            |
| `FORMAT_INCOMPATIBLE`    | Format not supported by product       | Check product's `format_ids` field                                                                                  |
| `BUDGET_INSUFFICIENT`    | Budget below product minimum          | Increase budget or choose different product                                                                         |
| `TARGETING_TOO_NARROW`   | Targeting yields zero inventory       | Broaden geographic or audience criteria                                                                             |
| `POLICY_VIOLATION`       | Brand/product violates policy         | Review publisher's content policies                                                                                 |
| `INVALID_PRICING_OPTION` | pricing\_option\_id not found         | Use ID from product's `pricing_options`                                                                             |
| `CREATIVE_ID_EXISTS`     | Creative ID already exists in library | Use a different `creative_id`, assign existing creatives via `creative_assignments`, or update via `sync_creatives` |

Example error response:

```json theme={null}
{
  "errors": [{
    "code": "FORMAT_INCOMPATIBLE",
    "message": "Product 'prod_d979b543' does not support format 'display_728x90'",
    "field": "packages[0].format_ids",
    "suggestion": "Use display_300x250_image, display_300x250_html, or display_300x250_generative formats"
  }]
}
```

## Key Concepts

### Format Specification

Format IDs are **required** for each package because:

* Publishers create placeholder creatives in ad servers
* Both parties know exactly what creative assets are needed
* Validation ensures products support requested formats
* Progress tracking shows which assets are missing

See [Format Workflow](#format-workflow) below for complete details.

### Brand reference

The `brand` field identifies the advertiser for policy compliance and business purposes.

```json theme={null}
{
  "brand": {
    "domain": "acmecorp.com"
  }
}
```

Full brand identity data (colors, fonts, product catalog) is resolved from brand.json at execution time. See [brand.json](/dist/docs/3.0.0-rc.2/brand-protocol/brand-json).

### Pricing & Currency

Each package specifies its `pricing_option_id`, which determines:

* Currency (USD, EUR, etc.)
* Pricing model (CPM, CPCV, CPP, etc.)
* Rate and whether it's fixed or auction-based

Packages can use different currencies when sellers support it. See [Pricing Models](/dist/docs/3.0.0-rc.2/media-buy/advanced-topics/pricing-models).

### Targeting Overlays

**Use sparingly** - most targeting should be in your brief and handled through product selection.

Use overlays only for:

* Geographic restrictions (RCT testing, regulatory compliance)
* Frequency capping
* AXE segment inclusion/exclusion

See [Targeting](/dist/docs/3.0.0-rc.2/media-buy/advanced-topics/targeting) for details.

## Format Workflow

### Why Format Specification Matters

When creating a media buy, format specification enables:

1. **Placeholder Creation** - Publisher creates placeholders in ad server with correct specs
2. **Validation** - System validates products support requested formats
3. **Clear Expectations** - Both parties know exactly what's needed
4. **Progress Tracking** - Track which assets are missing vs. required
5. **Technical Setup** - Ad server configured before creatives arrive

### Complete Workflow

```
1. list_creative_formats → Get available format specifications
2. get_products → Find products (returns format_ids they support)
3. Validate compatibility → Ensure products support desired formats
4. create_media_buy → Specify formats (REQUIRED)
   └── Publisher creates placeholders
   └── Clear creative requirements established
5. sync_creatives → Upload actual files matching formats
6. Campaign activation → Replace placeholders with real creatives
```

### Format Validation

Publishers MUST validate:

* All formats are supported by the product
* Format specifications match `list_creative_formats` output
* Creative requirements can be fulfilled within timeline

Invalid format example:

```json theme={null}
{
  "errors": [{
    "code": "FORMAT_INCOMPATIBLE",
    "message": "Product 'ctv_sports_premium' does not support format 'audio_standard_30s'",
    "field": "packages[0].format_ids",
    "supported_formats": [
      { "agent_url": "https://creative.adcontextprotocol.org", "id": "video_standard_30s" },
      { "agent_url": "https://creative.adcontextprotocol.org", "id": "video_standard_15s" }
    ]
  }]
}
```

### Flight date validation

When a package specifies `start_time` or `end_time`, sellers SHOULD validate that:

* Both dates fall within the media buy's date range
* `start_time` is before `end_time`

Out-of-range or inverted dates SHOULD return an `INVALID_REQUEST` error:

```json theme={null}
{
  "errors": [{
    "code": "INVALID_REQUEST",
    "message": "Package 'week_5' end_time 2026-04-05T23:59:59Z is after media buy end_time 2026-03-31T23:59:59Z",
    "field": "packages[3].end_time"
  }]
}
```

## Asynchronous Operations

This task can complete instantly or take days depending on complexity and approval requirements. The response includes a `status` field that tells you what happened and what to do next.

| Status           | Meaning                   | Your Action                         |
| ---------------- | ------------------------- | ----------------------------------- |
| `completed`      | Done immediately          | Process the result                  |
| `working`        | Processing (\~2 min)      | Poll frequently or wait for webhook |
| `submitted`      | Long-running (hours/days) | Use webhooks or poll infrequently   |
| `input-required` | Needs your input          | Read message, respond with info     |
| `failed`         | Error occurred            | Handle the error                    |

**Note:** For the complete status list see [Task Lifecycle](/dist/docs/3.0.0-rc.2/building/implementation/task-lifecycle).

<Tabs>
  <Tab title="MCP">
    ### Immediate Success (`completed`)

    The task completed synchronously. No async handling needed.

    **Request:**

    ```javascript test=false theme={null}
    const response = await session.call('create_media_buy', {
      buyer_ref: 'summer_campaign_2025',
      brand: { domain: 'acmecorp.com' },
      packages: [
        {
          buyer_ref: 'ctv_package',
          product_id: 'prod_ctv_sports',
          pricing_option_id: 'cpm_fixed',
          budget: 50000
        }
      ]
    });
    ```

    **Response:**

    ```json theme={null}
    {
      "status": "completed",
      "media_buy_id": "mb_12345",
      "buyer_ref": "summer_campaign_2025",
      "creative_deadline": "2025-06-15T23:59:59Z",
      "packages": [
        {
          "package_id": "pkg_001",
          "buyer_ref": "ctv_package",
          "product_id": "prod_ctv_sports"
        }
      ]
    }
    ```

    ### Long-Running (`submitted`)

    The task is queued for manual approval. Configure a webhook to receive updates.

    **Request with webhook:**

    ```javascript test=false theme={null}
    const response = await session.call('create_media_buy',
      {
        buyer_ref: 'enterprise_campaign',
        brand: { domain: 'acmecorp.com' },
        packages: [
          {
            buyer_ref: 'premium_package',
            product_id: 'prod_premium_ctv',
            pricing_option_id: 'cpm_fixed',
            budget: 500000  // Large budget triggers approval
          }
        ]
      },
      {
        pushNotificationConfig: {
          url: 'https://your-app.com/webhooks/adcp',
          authentication: {
            schemes: ['bearer'],
            credentials: 'your_webhook_secret'
          }
        }
      }
    );
    ```

    **Initial response:**

    ```json theme={null}
    {
      "status": "submitted",
      "task_id": "task_abc123",
      "message": "Budget exceeds auto-approval limit. Sales review required (2-4 hours)."
    }
    ```

    **Webhook POST when approved:**

    ```json theme={null}
    {
      "task_id": "task_abc123",
      "task_type": "create_media_buy",
      "status": "completed",
      "timestamp": "2025-01-22T14:30:00Z",
      "message": "Media buy approved and created",
      "result": {
        "media_buy_id": "mb_67890",
        "buyer_ref": "enterprise_campaign",
        "creative_deadline": "2025-06-20T23:59:59Z",
        "packages": [
          {
            "package_id": "pkg_002",
            "buyer_ref": "premium_package"
          }
        ]
      }
    }
    ```

    ### Error (`failed`)

    **Response:**

    ```json theme={null}
    {
      "status": "failed",
      "errors": [
        {
          "code": "INSUFFICIENT_INVENTORY",
          "message": "Requested targeting yields no available impressions",
          "field": "packages[0].targeting",
          "suggestion": "Expand geographic targeting or increase CPM bid"
        }
      ]
    }
    ```
  </Tab>

  <Tab title="A2A">
    ### Immediate Success (`completed`)

    **Request:**

    ```javascript test=false theme={null}
    const response = await a2a.send({
      message: {
        parts: [{
          kind: 'data',
          data: {
            skill: 'create_media_buy',
            parameters: {
              buyer_ref: 'summer_campaign_2025',
              brand: { domain: 'acmecorp.com' },
              packages: [
                {
                  buyer_ref: 'ctv_package',
                  product_id: 'prod_ctv_sports',
                  pricing_option_id: 'cpm_fixed',
                  budget: 50000
                }
              ]
            }
          }
        }]
      }
    });
    ```

    **Response:**

    ```json theme={null}
    {
      "status": "completed",
      "taskId": "task_123",
      "contextId": "ctx_456",
      "artifacts": [{
        "parts": [
          { "text": "Media buy created successfully" },
          {
            "data": {
              "media_buy_id": "mb_12345",
              "buyer_ref": "summer_campaign_2025",
              "creative_deadline": "2025-06-15T23:59:59Z",
              "packages": [
                {
                  "package_id": "pkg_001",
                  "buyer_ref": "ctv_package"
                }
              ]
            }
          }
        ]
      }]
    }
    ```

    ### Processing (`working`)

    Task is actively processing. Use SSE streaming or poll for updates.

    **Initial response:**

    ```json theme={null}
    {
      "status": "working",
      "taskId": "task_789",
      "contextId": "ctx_456"
    }
    ```

    **SSE status update:**

    ```json theme={null}
    {
      "taskId": "task_789",
      "status": {
        "state": "working",
        "message": {
          "parts": [
            { "text": "Validating inventory availability..." },
            {
              "data": {
                "percentage": 50,
                "current_step": "inventory_check"
              }
            }
          ]
        }
      }
    }
    ```

    ### Long-Running (`submitted`)

    **Request with push notification:**

    ```javascript test=false theme={null}
    const response = await a2a.send({
      message: {
        parts: [{
          kind: 'data',
          data: {
            skill: 'create_media_buy',
            parameters: {
              buyer_ref: 'enterprise_campaign',
              packages: [{ budget: 500000 }]  // Triggers approval
            }
          }
        }]
      },
      pushNotificationConfig: {
        url: 'https://your-app.com/webhooks/a2a',
        authentication: {
          schemes: ['bearer'],
          credentials: 'your_webhook_secret'
        }
      }
    });
    ```

    **Initial response:**

    ```json theme={null}
    {
      "status": "submitted",
      "taskId": "task_abc",
      "contextId": "ctx_456"
    }
    ```

    **Webhook POST (Task) when completed:**

    ```json theme={null}
    {
      "id": "task_abc",
      "contextId": "ctx_456",
      "status": {
        "state": "completed",
        "message": {
          "parts": [
            { "text": "Media buy approved and created" },
            {
              "data": {
                "media_buy_id": "mb_67890",
                "buyer_ref": "enterprise_campaign",
                "packages": [{ "package_id": "pkg_002" }]
              }
            }
          ]
        },
        "timestamp": "2025-01-22T14:30:00Z"
      }
    }
    ```

    ### Input Required (`input-required`)

    Task is paused waiting for clarification or approval.

    **Response:**

    ```json theme={null}
    {
      "status": "input-required",
      "taskId": "task_def",
      "contextId": "ctx_456",
      "artifacts": [{
        "parts": [
          { "text": "The requested budget exceeds your pre-approved limit. Please confirm you want to proceed with $500K spend." },
          {
            "data": {
              "reason": "APPROVAL_REQUIRED",
              "errors": [
                {
                  "code": "BUDGET_EXCEEDS_LIMIT",
                  "message": "Requested budget exceeds pre-approved limit",
                  "field": "total_budget"
                }
              ]
            }
          }
        ]
      }]
    }
    ```

    **Follow-up to approve:**

    ```javascript test=false theme={null}
    await a2a.send({
      contextId: 'ctx_456',  // Continue the conversation
      message: {
        parts: [{ kind: 'text', text: 'Yes, I confirm the $500K budget' }]
      }
    });
    ```

    ### Error (`failed`)

    **Response:**

    ```json theme={null}
    {
      "status": "failed",
      "taskId": "task_xyz",
      "artifacts": [{
        "parts": [
          { "text": "Failed to create media buy" },
          {
            "data": {
              "errors": [
                {
                  "code": "INSUFFICIENT_INVENTORY",
                  "message": "Requested targeting yields no available impressions",
                  "suggestion": "Expand geographic targeting"
                }
              ]
            }
          }
        ]
      }]
    }
    ```
  </Tab>
</Tabs>

For complete async handling patterns, see [Async Operations](/dist/docs/3.0.0-rc.2/building/implementation/async-operations).

## Usage Notes

* Total budget is distributed across packages based on individual `budget` values
* Both media buys and packages have `buyer_ref` fields for tracking
* Creative assets must be uploaded before deadline for campaign activation
* AXE segments enable advanced audience targeting
* Pending states (`working`, `submitted`) are normal, not errors
* Orchestrators MUST handle pending states as part of normal workflow
* **Inline creatives**: The `creatives` array creates NEW creatives only. To update existing creatives, use [`sync_creatives`](/dist/docs/3.0.0-rc.2/creative/task-reference/sync_creatives). To assign existing library creatives, use `creative_assignments` instead.

## Policy Compliance

Brand and products are validated during creation. Policy violations return errors:

```json theme={null}
{
  "errors": [{
    "code": "POLICY_VIOLATION",
    "message": "Brand or product category not permitted on this publisher",
    "field": "brand",
    "suggestion": "Contact publisher for category approval process"
  }]
}
```

Publishers should ensure:

* Brand/products align with selected packages
* Creatives match declared brand/products
* Campaign complies with all advertising policies

## Next Steps

After creating a media buy:

1. **Upload Creatives**: Use [`sync_creatives`](/dist/docs/3.0.0-rc.2/creative/task-reference/sync_creatives) before deadline
2. **Monitor Status**: Use [`get_media_buy_delivery`](/dist/docs/3.0.0-rc.2/media-buy/task-reference/get_media_buy_delivery)
3. **Optimize**: Use [`provide_performance_feedback`](/dist/docs/3.0.0-rc.2/media-buy/task-reference/provide_performance_feedback)
4. **Update**: Use [`update_media_buy`](/dist/docs/3.0.0-rc.2/media-buy/task-reference/update_media_buy) to modify campaign

## Learn More

* [Media Buy Lifecycle](/dist/docs/3.0.0-rc.2/media-buy/media-buys/) - Complete campaign workflow
* [get\_products](/dist/docs/3.0.0-rc.2/media-buy/task-reference/get_products) - Discover inventory
* [Targeting](/dist/docs/3.0.0-rc.2/media-buy/advanced-topics/targeting) - Targeting strategies
* [Pricing Models](/dist/docs/3.0.0-rc.2/media-buy/advanced-topics/pricing-models) - Currency and pricing
