---
title: asset_create_upload
description: Reserve an Asset and get a short-lived URL to upload the file to. Step one of bringing your own creative in.
---

Reserves an **Asset** and returns a short-lived, presigned URL to upload the file to. This is step one of the upload flow; see [Bring your own creative](/mcp/assets) for the whole sequence.

## Input

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `filename` | `string` | yes | The original filename, including extension. |

## Output

`{ assetId, uploadUrl, expiresAt }`:

| Field | Type | Description |
| --- | --- | --- |
| `assetId` | `ast_*` | The reserved Asset id — pass it to `asset_finalize`. |
| `uploadUrl` | `string` | A presigned `PUT` URL. Send the file bytes here. |
| `expiresAt` | `number` | Epoch ms when `uploadUrl` stops working. |

## The agent uploads the bytes itself

Send an HTTP `PUT` to `uploadUrl` with the file as the request body and its `Content-Type` set. The bytes must **never pass through the conversation** — the agent reads the file and PUTs it from its own runtime.

```
PUT <uploadUrl>
Content-Type: image/png

<the file bytes>
```

Once the `PUT` returns `200`, call [`asset_finalize`](/mcp/tools/asset-finalize).

:::warning[Requires outbound network access]

The `PUT` targets `*.r2.cloudflarestorage.com`. If the agent's runtime restricts network access, that host must be allowed or the upload can't complete.

:::

## Example

> _"Add this logo.png to our creative library."_

Claude calls `asset_create_upload({ filename: 'logo.png' })`, PUTs the file to the returned `uploadUrl`, then finalizes with the returned `assetId`.

## Errors

- The file's type and size aren't checked here — nothing is known until the bytes arrive, so rejection happens at [`asset_finalize`](/mcp/tools/asset-finalize).
- `401` or `403` — see [Errors](/mcp/errors), which every tool shares. This one needs `asset:write`.
