Skip to main content
Create a media buy from selected packages. Handles validation, approval if needed, and campaign creation. 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 Response Schema: /schemas/v2/media-buy/create-media-buy-response.json

Quick Start

Create a simple media buy with two packages:
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_manifest: {
    name: 'Nike',
    url: 'https://nike.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: 5000,
      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: 5000,
      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}`);
}

Request Parameters

ParameterTypeRequiredDescription
buyer_refstringYesYour reference identifier for this media buy
packagesPackage[]YesArray of package configurations (see below)
brand_manifestBrandManifestRefYesBrand identity - minimal (name/URL) acceptable for sales agents. See Brand Manifest
start_timestringYes"asap" or ISO 8601 date-time
end_timestringYesISO 8601 date-time (UTC unless timezone specified)
po_numberstringNoPurchase order number
reporting_webhookReportingWebhookNoAutomated reporting delivery configuration

Package Object

ParameterTypeRequiredDescription
buyer_refstringYesYour reference for this package
product_idstringYesProduct ID from get_products
pricing_option_idstringYesPricing option ID from product’s pricing_options array
format_idsFormatID[]YesFormat IDs that will be used - must be supported by product
budgetnumberYesBudget in currency specified by pricing option
pacingstringNo"even" (default), "asap", or "front_loaded"
bid_pricenumberNoBid price for auction pricing (required when is_fixed is false)
targeting_overlayTargetingOverlayNoAdditional targeting criteria (see Targeting)
creative_idsstring[]NoExisting library creative IDs to assign
creativesCreativeAsset[]NoFull creative objects to upload and assign

Response

Success Response

FieldDescription
media_buy_idPublisher’s unique identifier
buyer_refYour reference identifier
creative_deadlineISO 8601 timestamp for creative upload deadline
packagesArray of created packages with complete state

Error Response

FieldDescription
errorsArray 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:
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_manifest: {
    name: 'Regional Retailer',
    url: 'https://example-retailer.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: 5000,
    bid_price: 5.00,
    targeting_overlay: {
      geo_country_any_of: ['US'],
      geo_region_any_of: ['CA', 'NY'],
      frequency_cap: {
        suppress_minutes: 60
      }
    }
  }],
  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`);
}

Campaign with Inline Creatives

Upload creatives at the same time as creating the campaign:
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_manifest: {
    name: 'Nike',
    url: 'https://nike.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: 5000,
    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`);
}

Campaign with Reporting Webhook

Receive automated reporting notifications:
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_manifest: {
    name: 'Nike',
    url: 'https://nike.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: 5000,
    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`);
}

Error Handling

Common errors and resolutions:
Error CodeDescriptionResolution
PRODUCT_NOT_FOUNDInvalid product_idVerify product exists via get_products
FORMAT_INCOMPATIBLEFormat not supported by productCheck product’s format_ids field
BUDGET_INSUFFICIENTBudget below product minimumIncrease budget or choose different product
TARGETING_TOO_NARROWTargeting yields zero inventoryBroaden geographic or audience criteria
POLICY_VIOLATIONBrand/product violates policyReview publisher’s content policies
INVALID_PRICING_OPTIONpricing_option_id not foundUse ID from product’s pricing_options
Example error response:
{
  "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 below for complete details.

Brand Manifest

The brand_manifest field identifies the advertiser for policy compliance and business purposes. Minimal manifests are acceptable for sales agents:
{
  "brand_manifest": {
    "name": "ACME Corporation",
    "url": "https://acmecorp.com"
  }
}
Full manifests with colors, fonts, and product catalogs are only needed for creative generation. See Brand Manifest.

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.

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 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:
{
  "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" }
    ]
  }]
}

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.
StatusMeaningYour Action
completedDone immediatelyProcess the result
workingProcessing (~2 min)Poll frequently or wait for webhook
submittedLong-running (hours/days)Use webhooks or poll infrequently
input-requiredNeeds your inputRead message, respond with info
failedError occurredHandle the error
Note: For the complete status list see Core Concepts - Task Status System.

Immediate Success (completed)

The task completed synchronously. No async handling needed.Request:
const response = await session.call('create_media_buy', {
  buyer_ref: 'summer_campaign_2025',
  brand_manifest: { name: 'Nike', url: 'https://nike.com' },
  packages: [
    {
      buyer_ref: 'ctv_package',
      product_id: 'prod_ctv_sports',
      pricing_option_id: 'cpm_fixed',
      budget: 50000
    }
  ]
});
Response:
{
  "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:
const response = await session.call('create_media_buy',
  {
    buyer_ref: 'enterprise_campaign',
    brand_manifest: { name: 'Nike', url: 'https://nike.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:
{
  "status": "submitted",
  "task_id": "task_abc123",
  "message": "Budget exceeds auto-approval limit. Sales review required (2-4 hours)."
}
Webhook POST when approved:
{
  "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:
{
  "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"
    }
  ]
}
For complete async handling patterns, see Task Management.

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

Policy Compliance

Brand and products are validated during creation. Policy violations return errors:
{
  "errors": [{
    "code": "POLICY_VIOLATION",
    "message": "Brand or product category not permitted on this publisher",
    "field": "brand_manifest",
    "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 before deadline
  2. Monitor Status: Use get_media_buy_delivery
  3. Optimize: Use provide_performance_feedback
  4. Update: Use update_media_buy to modify campaign

Learn More