Skip to main content
Upload and manage creative assets in a creative library. Supports bulk uploads, upsert semantics, and generative creatives. Implemented by any agent that hosts a creative library — creative agents (ad servers, creative management platforms) and sales agents that manage creatives. Response time: Instant to days (returns completed, or submitted for review that takes hours/days) Request Schema: creative/sync-creatives-request.json Response Schema: creative/sync-creatives-response.json

Quick start

Upload creative assets:
import { testAgent } from "@adcp/client/testing";
import { SyncCreativesResponseSchema } from "@adcp/client";

const result = await testAgent.syncCreatives({
  creatives: [
    {
      creative_id: "creative_video_001",
      name: "Summer Sale 30s",
      format_id: {
        agent_url: "https://creative.adcontextprotocol.org",
        id: "video_standard_30s",
      },
      assets: {
        video: {
          url: "https://cdn.example.com/summer-sale-30s.mp4",
          width: 1920,
          height: 1080,
          duration_ms: 30000,
        },
      },
    },
  ],
});

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

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

// Check for operation-level errors first (discriminated union)
if ("errors" in validated && validated.errors) {
  throw new Error(`Operation failed: ${JSON.stringify(validated.errors)}`);
}

if ("creatives" in validated) {
  console.log(`Synced ${validated.creatives.length} creatives`);
}
Note: When creatives require approval, the response returns status: "submitted" with a task_id. See Async approval workflow for handling these cases.

Request parameters

ParameterTypeRequiredDescription
accountobjectYesAccount reference identifying the advertiser/workspace for this sync (account-ref)
creativesCreative[]YesCreative assets to upload/update (max 100)
creative_idsstring[]NoOptional filter to limit sync scope to specific creative IDs. Only these creatives are affected, others remain untouched. Useful for partial updates and error recovery.
assignmentsarrayNoArray of {creative_id, package_id} objects for bulk assignment. Optional weight and placement_ids per assignment.
dry_runbooleanNoWhen true, preview changes without applying them (default: false)
validation_modestringNoValidation strictness: "strict" (default) or "lenient"
delete_missingbooleanNoWhen true, creatives not in this sync are archived (default: false). Cannot be combined with creative_ids. Cannot delete creatives assigned to active, non-paused packages.

Creative object

FieldTypeRequiredDescription
creative_idstringYesUnique identifier for this creative
namestringYesHuman-readable name
format_idFormatIdYesFormat specification (structured object with agent_url and id)
assetsobjectYesAssets keyed by role (e.g., {video: {...}, thumbnail: {...}}). Catalogs are included as assets with asset_type: "catalog". See Catalogs.
tagsstring[]NoSearchable tags for creative organization

Asset structure

Assets are keyed by role name. Each role contains the asset details:
test=false
{
  "assets": {
    "video": {
      "url": "https://cdn.example.com/video.mp4",
      "width": 1920,
      "height": 1080,
      "duration_ms": 30000
    },
    "thumbnail": {
      "url": "https://cdn.example.com/thumb.jpg",
      "width": 300,
      "height": 250
    }
  }
}

Assignments structure

Assignments are at the request level, mapping creative IDs to package IDs. Standalone creative agents that do not manage media buys ignore this field.
test=false
{
  "assignments": [
    { "creative_id": "creative_video_001", "package_id": "pkg_premium" },
    { "creative_id": "creative_video_001", "package_id": "pkg_standard" },
    { "creative_id": "creative_display_002", "package_id": "pkg_standard" }
  ]
}

Response

Success Response:
  • creatives - Results for each creative processed (includes both successful and failed items)
  • dry_run - Boolean indicating if this was a dry run (optional)
Error Response:
  • errors - Array of operation-level errors (auth failure, service unavailable)
Note: Responses use discriminated unions - you get either success fields OR errors, never both. Per-creative errors appear in the errors array of individual creative objects when action: "failed". Each creative in success response includes:
  • All request fields
  • platform_id - Platform’s internal ID (when action is not failed)
  • action - What happened: created, updated, unchanged, failed, deleted
  • errors - Array of error messages (only when action: "failed")
  • warnings - Array of non-fatal warnings (optional)
See schema for complete field list: sync-creatives-response.json

Common scenarios

Bulk upload

Upload multiple creatives in one call:
import { testAgent } from "@adcp/client/testing";
import { SyncCreativesResponseSchema } from "@adcp/client";

const result = await testAgent.syncCreatives({
  creatives: [
    {
      creative_id: "creative_display_001",
      name: "Summer Sale Banner 300x250",
      format_id: {
        agent_url: "https://creative.adcontextprotocol.org",
        id: "display_300x250",
      },
      assets: {
        image: {
          url: "https://cdn.example.com/banner-300x250.jpg",
          width: 300,
          height: 250,
        },
      },
    },
    {
      creative_id: "creative_video_002",
      name: "Product Demo 15s",
      format_id: {
        agent_url: "https://creative.adcontextprotocol.org",
        id: "video_standard_15s",
      },
      assets: {
        video: {
          url: "https://cdn.example.com/demo-15s.mp4",
          width: 1920,
          height: 1080,
          duration_ms: 15000,
        },
      },
    },
    {
      creative_id: "creative_display_002",
      name: "Summer Sale Banner 728x90",
      format_id: {
        agent_url: "https://creative.adcontextprotocol.org",
        id: "display_728x90",
      },
      assets: {
        image: {
          url: "https://cdn.example.com/banner-728x90.jpg",
          width: 728,
          height: 90,
        },
      },
    },
  ],
});

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

const validated = SyncCreativesResponseSchema.parse(result.data);

if ("errors" in validated && validated.errors) {
  throw new Error(`Operation failed: ${JSON.stringify(validated.errors)}`);
}

if ("creatives" in validated) {
  console.log(`Successfully synced ${validated.creatives.length} creatives`);
  validated.creatives.forEach((creative) => {
    console.log(`  ${creative.name}: ${creative.platform_id}`);
  });
}

Generative creatives

Use the creative agent to generate creatives from brand identity data. See the Generative Creatives guide for complete workflow details.
import { testAgent } from "@adcp/client/testing";
import { SyncCreativesResponseSchema } from "@adcp/client";

const result = await testAgent.syncCreatives({
  creatives: [
    {
      creative_id: "creative_gen_001",
      name: "AI-Generated Summer Banner",
      format_id: {
        agent_url: "https://creative.adcontextprotocol.org",
        id: "display_300x250",
      },
      assets: {
        manifest: {
          url: "https://cdn.example.com/brand.json",
        },
      },
    },
  ],
});

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

const validated = SyncCreativesResponseSchema.parse(result.data);

if ("errors" in validated && validated.errors) {
  throw new Error(`Operation failed: ${JSON.stringify(validated.errors)}`);
}

if ("creatives" in validated) {
  console.log(
    "Generative creative synced:",
    validated.creatives[0].creative_id
  );
}

Dry run validation

Validate creative configuration without uploading:
import { testAgent } from "@adcp/client/testing";
import { SyncCreativesResponseSchema } from "@adcp/client";

const result = await testAgent.syncCreatives({
  dry_run: true,
  creatives: [
    {
      creative_id: "creative_test_001",
      name: "Test Creative",
      format_id: {
        agent_url: "https://creative.adcontextprotocol.org",
        id: "video_standard_30s",
      },
      assets: {
        video: {
          url: "https://cdn.example.com/test-video.mp4",
          width: 1920,
          height: 1080,
          duration_ms: 30000,
        },
      },
    },
  ],
});

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

const validated = SyncCreativesResponseSchema.parse(result.data);

if ("errors" in validated && validated.errors && validated.errors.length > 0) {
  console.log("Validation errors found:");
  validated.errors.forEach((error) => console.log(`  - ${error.message}`));
} else {
  console.log("Validation passed! Ready to sync.");
}

Scoped update with creative_ids filter

Update only specific creatives from a large library without affecting others:
import { testAgent } from "@adcp/client/testing";
import { SyncCreativesResponseSchema } from "@adcp/client";

// Update just 2 creatives out of 100+ in the library
const result = await testAgent.syncCreatives({
  creative_ids: ["creative_video_001", "creative_display_001"],
  creatives: [
    {
      creative_id: "creative_video_001",
      name: "Summer Sale 30s - Updated",
      format_id: {
        agent_url: "https://creative.adcontextprotocol.org",
        id: "video_standard_30s",
      },
      assets: {
        video: {
          url: "https://cdn.example.com/updated-video.mp4",
          width: 1920,
          height: 1080,
          duration_ms: 30000,
        },
      },
    },
    {
      creative_id: "creative_display_001",
      name: "Summer Sale Banner - Updated",
      format_id: {
        agent_url: "https://creative.adcontextprotocol.org",
        id: "display_300x250",
      },
      assets: {
        image: {
          url: "https://cdn.example.com/updated-banner.jpg",
          width: 300,
          height: 250,
        },
      },
    },
  ],
});

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

const validated = SyncCreativesResponseSchema.parse(result.data);

if ("errors" in validated && validated.errors) {
  throw new Error(`Update failed: ${JSON.stringify(validated.errors)}`);
}

if ("creatives" in validated) {
  console.log(
    `Updated ${validated.creatives.length} creatives, others untouched`
  );
}
Why use creative_ids filter:
  • Scoped updates: Only specified creatives modified, even with 100+ in library
  • Error recovery: Retry only failed creatives after bulk sync validation failures
  • Performance: Publisher can optimize processing when scope is known upfront
  • Safety: Explicit targeting reduces risk of unintended changes

Async approval workflow

When creatives require review (brand safety, policy compliance), the initial response is status: "submitted". Use webhooks or polling to get the outcome. Final response has status: "completed" with per-creative results:
  • Approved creatives: action: "created" with platform_id
  • Rejected creatives: action: "failed" with error details in errors array
The operation status (completed) means the review process finished. Individual creative outcomes are in the action field. Operation-level failures (auth error, service unavailable) return status: "failed" with no creatives array. See: Webhooks for webhook configuration.

Sync modes

Upsert (default)

  • Creates new creatives or updates existing by creative_id
  • Merges package assignments (additive)
  • Updates provided fields, leaves others unchanged
  • Use creative_ids filter to limit scope to specific creatives

Dry run

  • Validates request without making changes
  • Returns errors and warnings
  • Does not process assets or create creatives
  • Use for pre-flight validation checks

Error handling

Error CodeDescriptionResolution
INVALID_FORMATFormat not supported by productCheck product’s supported formats via list_creative_formats
ASSET_PROCESSING_FAILEDAsset file corrupt or invalidVerify asset meets format requirements (codec, dimensions, duration)
PACKAGE_NOT_FOUNDPackage ID doesn’t exist in media buyVerify package_id from create_media_buy response
BRAND_SAFETY_VIOLATIONCreative failed brand safety scanReview content against publisher’s brand safety guidelines
FORMAT_MISMATCHAssets don’t match format requirementsVerify asset types and specifications match format definition
CREATIVE_IN_ACTIVE_DELIVERYCreative is assigned to an active, non-paused package (blocks updates and delete_missing deletions)Pause the package first, or create a new creative version

Best practices

  1. Use upsert semantics - Same creative_id updates existing creative rather than creating duplicates. This allows iterative creative development. Note: updates are blocked for creatives in active delivery (see #7).
  2. Validate first - Use dry_run: true to catch errors before actual upload. This saves bandwidth and processing time.
  3. Batch assignments - Include all package assignments in single sync call to avoid race conditions between updates.
  4. CDN-hosted assets - Use publicly accessible CDN URLs for faster processing. Platforms can fetch assets directly without proxy delays.
  5. Brand identity - For generative creatives, validate brand identity schema before syncing to avoid processing failures.
  6. Check format support - Use list_creative_formats to verify product supports your creative formats before uploading.
  7. Active delivery protection - Creatives assigned to active, non-paused packages cannot be updated or deleted via delete_missing. Pause the package first, unassign the creative via update_media_buy, or create a new creative with a different creative_id.