Skip to main content
AdCP does not include an asset upload task. Creative agents are not expected to accept file uploads or manage storage on behalf of buyers. Instead, buyer agents are responsible for hosting their own assets and providing accessible URLs in creative manifests. When assets live in private storage — an internal DAM, a private S3 bucket, or behind authentication — the buyer agent must make them accessible before passing URLs in a manifest.

Presigned URLs

The recommended pattern is presigned URLs. Most cloud storage providers support generating time-limited URLs that grant temporary read access without requiring authentication headers.

How it works

  1. Buyer agent receives or locates a private asset (e.g., a brand logo in S3)
  2. Buyer agent generates a presigned URL with a short expiration
  3. Buyer agent passes the presigned URL in the creative manifest
  4. Creative agent fetches the asset like any other public URL
{
  "format_id": {
    "agent_url": "https://creatives.example.com",
    "id": "display_static",
    "width": 300,
    "height": 250
  },
  "assets": {
    "banner_image": {
      "url": "https://my-bucket.s3.amazonaws.com/brand/logo.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=3600&X-Amz-Signature=...",
      "width": 300,
      "height": 250
    },
    "headline": {
      "content": "Spring collection"
    },
    "clickthrough_url": {
      "url": "https://shop.example.com/spring"
    }
  }
}
The only difference from a standard manifest is the URL itself. The banner_image.url contains presigned query parameters (X-Amz-Algorithm, X-Amz-Expires, X-Amz-Signature). No other fields change.

Provider examples

import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const client = new S3Client({ region: "us-east-1" });

const url = await getSignedUrl(
  client,
  new GetObjectCommand({
    Bucket: "my-brand-assets",
    Key: "logos/primary.png",
  }),
  { expiresIn: 3600 } // 1 hour
);

Expiration guidelines

Set presigned URL expiration long enough to cover the full workflow, but no longer than necessary.
WorkflowSuggested expiration
build_creative only1 hour
build_creative + preview_creative2 hours
Full pipeline (build, preview, iterate, sync_creatives)4 hours
If a presigned URL expires mid-workflow, the creative agent will receive an HTTP error when fetching the asset. The buyer agent must generate a new presigned URL and resubmit the request.
For assets that will be served at scale (e.g., a logo used across many impressions), use a CDN with long-lived public URLs instead of presigned URLs. Presigned query parameters defeat CDN caching since each generated URL is unique.

Uploading local files

For assets that don’t already live in cloud storage — local files, Slack attachments, email attachments — the buyer agent should upload them to its own storage first, then generate a presigned URL.
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { readFile } from "fs/promises";

const client = new S3Client({ region: "us-east-1" });

// Upload the local file
const fileBuffer = await readFile("./assets/logo.png");
await client.send(new PutObjectCommand({
  Bucket: "my-brand-assets",
  Key: "logos/primary.png",
  Body: fileBuffer,
  ContentType: "image/png",
}));

// Generate a presigned URL for the creative agent to fetch
const url = await getSignedUrl(
  client,
  new GetObjectCommand({
    Bucket: "my-brand-assets",
    Key: "logos/primary.png",
  }),
  { expiresIn: 3600 }
);

Why not auth headers?

AdCP manifests are declarative data passed between agents as JSON. Adding per-URL authentication headers would mean sharing storage credentials across trust boundaries — every system that touches the manifest (creative agent, preview service, ad server, logging infrastructure) would need to handle those credentials securely. Presigned URLs avoid this by encoding authorization into the URL itself:
  • Scoped to a single object with read-only access
  • Time-limited with built-in expiration
  • No credential forwarding required
  • Revocation is automatic (the URL expires)