---
title: Bring your own creative
description: Upload images and video you own, then register them into an advertiser's provider-side library — from your AI agent, over MCP.
---

Until now the MCP tools let an agent **read** your accounts and performance. The `asset_*` tools add the first creative-side capability: bring the source media _you_ own — images and video — into AdCrunch, and place it in a specific ad account's library so ads there can use it. All of it from the same AI client you already connected.

## Two ideas

Everything here is built on two words. They mean different things, and keeping them apart is what makes the workflow legible.

- An **Asset** is a source media file your organization owns — an image or a video. It lives in AdCrunch, belongs to your organization (not to any one ad account), and exists _before_ and independently of any ad. An Asset always has bytes we hold: a video hosted somewhere else is not an Asset.
- A **Registration** is the act — and the record — of placing one Asset's bytes into a **single advertiser's** provider-side library, in exchange for a provider identifier. One Asset can be registered to many advertisers, one per account. Registering does not make an Asset _live_; it makes it available for an ad in that account to reference.

:::info[Register targets an advertiser, not a provider]

An organization usually has several ad accounts on the same provider. You register an Asset into **one advertiser** at a time — use [`list_advertisers`](/mcp/tools/list-advertisers) to find the right `acc_…`.

:::

## The workflow

1. **Start an upload**

    The agent calls [`asset_create_upload`](/mcp/tools/asset-create-upload) with
    the filename. It gets back an `assetId` (`ast_…`) and a short-lived
    `uploadUrl` — a presigned URL that accepts the bytes directly.

2. **Upload the bytes — the agent does this itself**

    The agent sends an HTTP `PUT` to `uploadUrl` with the file as the body. The
    bytes go **straight to storage from the agent's runtime**; they never pass
    through the conversation. A Worker can't receive a large upload at all —
    Cloudflare rejects an over-limit body before any code runs — which is why
    the upload is client-to-storage, not through us.

3. **Finalize**

    The agent calls [`asset_finalize`](/mcp/tools/asset-finalize) with the
    `assetId`. Nothing is known about the file until it lands, so this is where
    the type and size are checked and a wrong file is rejected. On success the
    Asset joins your library. You can pass an `advertiser_id` here to register
    in the same call.

4. **Register**

    [`asset_register`](/mcp/tools/asset-register) places the Asset in an
    advertiser's library. It returns immediately with a `running` Registration —
    the provider processes the file in the background.

5. **Poll until ready**

    The agent calls [`asset_get`](/mcp/tools/asset-get) to watch the
    Registration go from `running` to `ready`. Video can stay `running` for
    several minutes while the provider transcodes it — that's normal, not a
    failure.

:::warning[The agent needs network access to storage]

The `PUT` in step 2 goes to `*.r2.cloudflarestorage.com`. If the agent's runtime restricts outbound network access, that host has to be allowed, or the upload can't complete.

:::

## What you can upload

Finalizing rejects anything ads can't use. Accepted types today:

- **Images** — JPEG, PNG, WebP, GIF.
- **Video** — MP4 (Meta accepts files up to 4 GB).

Size can't be enforced before the file arrives, so an oversized file is caught at finalize and discarded — upload a smaller version and try again.

## Statuses you'll see

| On | Value | Meaning |
| --- | --- | --- |
| **Asset** | `pending` | Reserved; the bytes haven't landed or been finalized yet. |
| **Asset** | `ready` | Finalized and usable. |
| **Registration** | `running` | Sent to the provider; still processing. Poll `asset_get`. |
| **Registration** | `ready` | Live in the advertiser's library; carries the `providerIdentifier`. |
| **Registration** | `failed` | The placement failed; `failureReason` says why. Retry `asset_register`. |

:::tip[A failed registration is not a failed upload]

If an inline registration fails during `asset_finalize`, the Asset is still stored — only the placement needs retrying. Call `asset_register` again; **do not re-upload the file**. Registering the same Asset to the same advertiser twice is refused, not duplicated, so retries are always safe.

:::

## Scopes

The read tools need `asset:read`; the write tools — upload **and** register — need `asset:write`. Registration isn't folded into `mutation:write`: an Asset landing in a library changes no delivery and spends no money, so it stays a separate, lower-stakes grant. See [Auth & scopes](/mcp/auth).

## Provider support

Meta and TikTok are supported today, and support is per-provider by design: Meta streams the bytes, while TikTok registers media **by URL** — it fetches the file itself, so a video that Meta accepts up to 4 GB may be refused by TikTok's own lower limits, reported plainly on the Registration. [What each provider supports](/use/connect/providers#register-creative) carries the rest, including why Google Ads is recorded as `unsupported` rather than failed. As more providers come online they slot in behind the same five tools.

## Next

**[asset_create_upload](/mcp/tools/asset-create-upload)**

Reserve an Asset and get an upload URL.

**[asset_register](/mcp/tools/asset-register)**

Place an Asset in an advertiser's library.

**[Upload and register a creative](/recipes/upload-and-register-a-creative)**

The full flow as one prompt.
