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

The bytes of a file never go through the API. You reserve the file, send the bytes to a storage URL, and then finalize the file. Assets and Documents use the same three steps.

- **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 an Asset

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 "Content-Type: video/mp4" \
      --upload-file spring-sale.mp4
    ```

    **Send the `Content-Type` of the file.** Storage keeps that header with the file, and finalize reads the type from it. A `PUT` with no `Content-Type` fails at finalize with `unsupported_type`.

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

3. **Finalize the Asset**

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

    The answer is the Asset, with the status `ready`. To register the Asset in the same call, send `{ "advertiserId": "acc_1203456789012345" }`.

## Upload a Document

The steps are the same, 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 |
| Finalize before the bytes arrive | `409` `not_uploaded` | `409` `no_object` |

## Finalize is the only check

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

|                | 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 answers `413` `too_large` or `415` `unsupported_type`, and AdCrunch deletes it. 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`.

## 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.

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`.
