---
title: Upload a file
description: Upload an image or a video as an Asset, or a file as a Brand Document. Then register an Asset in the library of an advertiser.
---

The bytes of a large file never go through the API. You reserve the file and send the bytes to a storage URL. A Document then needs a finalize call. An Asset needs none: a read of the Asset completes it.

- **An Asset** is an image or a video that goes into an ad. You register it in the library of an advertiser.
- **A Document** is reference material on a Brand, such as a logo or a guidelines PDF. An agent reads it. It never leaves AdCrunch.

## Upload a small Asset in one call

Send the file as the raw body of `POST /assets`. The answer is the Asset, with the status `ready`.

```bash
curl --data-binary @ad.png \
  -H "Authorization: Bearer $ADCRUNCH_API_KEY" \
  "https://api.adcrunch.dev/assets?filename=ad.png"
```

- `filename` is required. `name` is optional and replaces the filename in the display.
- Do not use `multipart/form-data`. The body is the file.
- The request needs a `Content-Length`. Without one, the answer is `411` `length_required`.
- The route takes a body up to 95 MB. Over that, the answer is `413` `use_upload_url`: use the two steps below. A body over the limit of the Cloudflare plan of the domain, 100 MB at the lowest, gets a `413` from Cloudflare with no `error` code.
- AdCrunch identifies the type from the bytes. A refused file leaves no object and no Asset. The answer is `413` `too_large` or `415` `unsupported_type` or `unreadable`.

## Import an Asset from a URL

Send a JSON body to `POST /assets`. AdCrunch fetches the file from the URL. The answer is `202` with the Asset, with the status `pending`.

```bash
curl -X POST https://api.adcrunch.dev/assets \
  -H "Authorization: Bearer $ADCRUNCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "sourceUrl": "https://example.com/ad.png" }'
```

- `sourceUrl` is required, and it must be an `https` URL. Another scheme answers `400` `invalid_url`, and AdCrunch creates nothing. `name` is optional.
- The host must be a public name. An IP address, a local or private host, or an AdCrunch domain (`adcrunch.dev` and its subdomains) answers `400` `source_not_allowed`. To store a file that is on an AdCrunch domain, send its bytes instead.
- The source must send a `Content-Length`. A source that declares a type AdCrunch does not accept, such as `image/svg+xml` or `text/html`, fails at once with `unsupported_type`. AdCrunch follows a redirect only to an `https` URL.
- Read the Asset with `GET /assets/{id}` until its status is `ready` or `failed`.
- A failure is on the Asset, not in the answer. `failureCode` is `source_unreachable` (the source did not answer, answered with an error, or sent the file in part), `source_length_unknown` (the source sent no `Content-Length`), `too_large`, `unsupported_type` or `unreadable`.
- The file takes its name from the `Content-Disposition` of the source, or else from the last segment of the URL.

## Upload an Asset in two steps

1. **Reserve the Asset**

    ```bash
    curl -X POST https://api.adcrunch.dev/assets/uploads \
      -H "Authorization: Bearer $ADCRUNCH_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "filename": "spring-sale.mp4" }'
    ```

    The answer holds the id of the Asset, the storage URL, and the time when the URL stops working:

    ```json
    {
      "assetId": "ast_7c1e9a",
      "uploadUrl": "https://…",
      "expiresAt": 1790000000000
    }
    ```

2. **Send the bytes to the storage URL**

    ```bash
    curl -X PUT "$UPLOAD_URL" \
      -H "If-None-Match: *" \
      --upload-file spring-sale.mp4
    ```

    The `PUT` of an Asset needs the header `If-None-Match: *`. A `PUT` without it answers `403`. The URL can create the file once, and it can never replace it:

    - The first `PUT` answers `200`.
    - A second `PUT` answers `412`. A `412` means that the file is already stored, for example after a retry when the first answer was lost. Continue to the next step.

    The bytes of an Asset never change. To use a different file, reserve a new Asset.

    The `PUT` of an Asset needs no `Content-Type`. AdCrunch identifies the type of an Asset from its bytes, and a `Content-Type` is only a hint. A Document still needs the `Content-Type` of its file.

    Do not send your API key to this URL. The URL itself gives write access: each person who holds it can create that one file until the URL expires.

3. **Read the Asset**

    ```bash
    curl https://api.adcrunch.dev/assets/ast_7c1e9a \
      -H "Authorization: Bearer $ADCRUNCH_API_KEY"
    ```

    The first read after the `PUT` inspects the stored file and completes the Asset. The answer is the Asset, with the status `ready` or `failed`. A read before the `PUT` answers `pending`. Read again until the status is `ready` or `failed`.

    AdCrunch also completes a reservation that nobody reads. The daily sweep inspects each `pending` Asset that is one day old. It completes the Asset when the file exists, and it deletes the reservation when no file exists.

## Upload a Document

A Document has three steps, on the Brand:

1. `POST /context/brands/{slug}/documents` with `filename` and `content_type`. The answer holds `documentId`, `uploadUrl`, and `expiresInSeconds`.
2. `PUT` the bytes to `uploadUrl`, with the `Content-Type` of the file.
3. `POST /context/brands/{slug}/documents/{documentId}/finalize`.

The two flows use different names for some fields:

|  | Asset | Document |
| --- | --- | --- |
| Permission | `asset:write` | `brand:write` |
| The id in the answer | `assetId` | `documentId` |
| The end of the URL | `expiresAt`, a timestamp | `expiresInSeconds`, a duration |
| Header on the `PUT` | `If-None-Match: *` | `Content-Type` of the file |
| Read of the Asset before the bytes arrive | `pending` | not applicable |
| Finalize before the bytes arrive | not applicable | `409` `no_object` |

## The stored file is the only check

The storage URL applies no rule to the content of a file. It accepts each file that you send to it. So AdCrunch checks the file when it completes the Asset or the Document, from what storage holds: the size of the stored file, and, for a Document, the `Content-Type` that your `PUT` sent. What you declared at step 1 controls nothing.

For an Asset, AdCrunch also reads the header of the stored file. The bytes decide the type, and AdCrunch stores that type as `mimeType`. A PNG that your `PUT` sent as `image/jpeg` becomes a `ready` Asset of type `image/png`. The header gives `width` and `height` as the file is displayed, with its rotation applied, and `durationMs` for a video.

|                | Types                     | Largest file |
| -------------- | ------------------------- | ------------ |
| An image Asset | GIF, JPEG, PNG, WebP      | 30 MB        |
| A video Asset  | MP4, QuickTime            | 4 GB         |
| A Document     | PDF, GIF, JPEG, PNG, WebP | 25 MB        |

A file that fails the check is deleted. A Document finalize answers `413` `too_large` or `415` `unsupported_type`. For an Asset, the read answers the Asset with the status `failed` and a `failureCode`. The Asset stays. See [The status of an Asset](#the-status-of-an-asset). For an Asset, `unsupported_type` means that the bytes are no accepted type. An Asset can also have the `failureCode` `unreadable`, when the file ends before its header does. Reserve and upload again. When AdCrunch does not accept the declared `content_type` of a Document, step 1 also answers `415`. That answer names the accepted types in `allowed`.

## The status of an Asset

`GET /assets/{id}` answers an Asset in every status. `GET /assets` lists `ready` Assets only.

| `status` | Meaning |
| --- | --- |
| `pending` | AdCrunch has not inspected the bytes yet. |
| `ready` | AdCrunch inspected and accepted the bytes. You can register the Asset. |
| `failed` | AdCrunch refused the file and deleted it. `failureCode` holds the code and `failureReason` holds a sentence. Branch on `failureCode`, never on the sentence. |

`failureCode` is `unsupported_type`, `too_large` or `unreadable`. An import adds `source_unreachable` and `source_length_unknown`. It is `null` unless the status is `failed`. A `failed` Asset cannot recover. Create a new Asset.

## Register an Asset

A Registration puts an Asset in the library of one advertiser, at its provider. The creative that you create with the [Mutations API](/api/make-a-change) takes the Asset from that library.

```bash
curl -X POST https://api.adcrunch.dev/assets/ast_7c1e9a/registrations \
  -H "Authorization: Bearer $ADCRUNCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "advertiserId": "acc_1203456789012345" }'
```

The answer is the Registration, with the status `running`. The provider does the work after the answer. To follow it, read [Get one Asset](/api/assets/get-asset): `registrations` holds each Registration of the Asset. [Register an Asset to an advertiser](/api/assets/register-asset) shows each field of a Registration.

| `status` | Meaning |
| --- | --- |
| `running` | The provider has not finished. A video can stay `running` for several minutes, and that is not a failure. |
| `ready` | The file is in the library of the advertiser. `providerIdentifier` holds the id that the provider gave. |
| `failed` | The Registration did not occur. `failureReason` holds the reason. |

When a provider does not accept a Registration, the Registration ends `failed`, and `failureReason` says why. The Asset stays stored. [What each provider supports](/connect/providers) lists the providers that accept a Registration.

When AdCrunch refuses the advertiser before any provider call, `failureCode` holds one of three codes. Branch on `failureCode`, never on the sentence in `failureReason`:

| `failureCode` | Meaning | What to do |
| --- | --- | --- |
| `not_found` | Your organization owns no advertiser of this provider with this id. | Check the advertiser id. |
| `provider_not_connected` | The advertiser has no usable connection to the provider. | Reconnect the provider, then send the request again. |
| `missing_write_access` | The connection does not permit this kind of file. | Reconnect the provider to grant the permission, then send the request again. |

`failureCode` is `null` when the provider refused the file.

Only a `ready` Asset can be registered. A request for another Asset answers `409`:

| `error` | Meaning | What to do |
| --- | --- | --- |
| `asset_not_ready` | The Asset is `pending`. | Read the Asset until its status is `ready`, then send the request again. |
| `asset_failed` | The Asset is `failed`. | Read `failureCode` and `failureReason`, then create a new Asset. |

An Asset can go in the library of many advertisers, with one Registration for each. For one Asset and one advertiser:

- A Registration that is `failed` can start again. Send the same request. AdCrunch replaces the failed Registration with a new one.
- A Registration that is `running` or `ready` blocks a second one. The second request answers `409` `already_registered`.

## Delete an Asset

`DELETE /assets/{id}` deletes the Asset from AdCrunch. It does not remove the file from the library of an advertiser, because that file belongs to the advertiser now. The answer counts those copies in `remainingRegistrations`.
