Private assets in AdCP use presigned URLs to grant temporary access to files stored in DAMs, S3 buckets, or authenticated sources.
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.
The recommended pattern is presigned URLs. Most cloud storage providers support generating time-limited URLs that grant temporary read access without requiring authentication headers.
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.
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.
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 fileconst 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 fetchconst url = await getSignedUrl( client, new GetObjectCommand({ Bucket: "my-brand-assets", Key: "logos/primary.png", }), { expiresIn: 3600 });
import { Storage } from "@google-cloud/storage";const storage = new Storage();const bucket = storage.bucket("my-brand-assets");// Upload the local fileawait bucket.upload("./assets/logo.png", { destination: "logos/primary.png", contentType: "image/png",});// Generate a presigned URL for the creative agent to fetchconst [url] = await bucket .file("logos/primary.png") .getSignedUrl({ action: "read", expires: Date.now() + 3600 * 1000, });
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: