# Direct Upload

> Upload a brand image, font, video or audio file in three steps, without sending the bytes through a JSON body

- **URL**: https://orshot.com/docs/api-reference/brand-assets-direct-upload

---

## Overview

Use direct upload when the file is large or when your client cannot build a base64 string. The flow has three steps:

1. Ask for an upload URL. It is valid for 5 minutes.
2. PUT the raw file bytes to that URL. This request takes no API key; the URL itself is the credential.
3. Confirm the upload. The file is checked and stored in your brand library, the same way the `/add` endpoints store it.

The same three endpoints work for every asset kind. The image-only paths `/images/upload-url`, `/images/upload` and `/images/confirm` still work for existing clients and behave the same with `kind` fixed to `image`.

```markdown tab="Endpoints"
POST https://api.orshot.com/v1/brand-assets/upload-url
PUT  <uploadUrl from step 1>
POST https://api.orshot.com/v1/brand-assets/confirm
```

## Request

**1. Upload URL**
```js
const { data } = await fetch("https://api.orshot.com/v1/brand-assets/upload-url", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer <ORSHOT_API_KEY>",
  },
  body: JSON.stringify({
    kind: "video",
    contentType: "video/mp4",
    fileName: "launch-teaser.mp4",
  }),
}).then((r) => r.json());

// data.uploadUrl, data.key, data.maxBytes, data.expiresAt
```

**2. PUT the file**
```bash
curl -X PUT "<data.uploadUrl>" \
  -H "Content-Type: video/mp4" \
  --data-binary @launch-teaser.mp4
```

The response is `{ "data": { "key", "kind", "bytes" } }`. No API key is sent on this step.

**3. Confirm**
```js
await fetch("https://api.orshot.com/v1/brand-assets/confirm", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer <ORSHOT_API_KEY>",
  },
  body: JSON.stringify({
    key: data.key,
    name: "Launch teaser",
    tags: ["launch", "video"],
  }),
});
```

**Response**
```json
{
  "data": {
    "kind": "video",
    "asset": {
      "id": 418,
      "name": "Launch teaser",
      "direct_url": "https://storage.orshot.com/cloud/w-50/renders/brand_assets/x9Kq2mVt7Lp.mp4",
      "file_size": 18329044,
      "tags": ["launch", "video"],
      "workspace_id": 50
    },
    "url": "https://storage.orshot.com/cloud/w-50/renders/brand_assets/x9Kq2mVt7Lp.mp4"
  }
}
```

The `asset` object has the same fields as the matching `/add` endpoint for that kind.

## Step 1: Upload URL parameters

| Parameter     | Type   | Required | Description                                                                                           |
| ------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------- |
| `kind`        | String | Yes      | `image`, `font`, `video` or `audio`. Not needed on the `/images/upload-url` alias                      |
| `contentType` | String | Yes      | MIME type of the file, for example `image/png`. Must be one of the kind's supported formats            |
| `fileName`    | String | No       | Used to work out the file extension when `contentType` is ambiguous                                   |

Response fields:

| Field               | Description                                                                                             |
| ------------------- | ------------------------------------------------------------------------------------------------------- |
| `uploadUrl`         | PUT the file here. Valid for 5 minutes                                                                  |
| `directUploadUrl`   | A second URL for the same upload that goes straight to storage. Use it only if your client cannot PUT to `uploadUrl`. For SVG, send the headers in `directUploadHeaders` with it |
| `key`               | Pass this to `/confirm`                                                                                 |
| `kind`, `contentType` | Echoed back                                                                                           |
| `maxBytes`          | The size limit for this kind                                                                            |
| `expiresAt`         | When the upload URL stops working (ISO 8601)                                                            |

## Step 3: Confirm parameters

| Parameter  | Type     | Required | Description                                                                      |
| ---------- | -------- | -------- | -------------------------------------------------------------------------------- |
| `key`      | String   | Yes      | The `key` from step 1                                                            |
| `name`     | String   | No       | Display name in the library. Falls back to `fileName`                            |
| `fileName` | String   | No       | Original file name                                                               |
| `tags`     | String[] | No       | Tags, stored lowercase                                                           |
| `metadata` | Object   | No       | Arbitrary metadata. For video and audio, pass `width`, `height` or `duration` here if you want them stored; the API does not read them from the file. `meta` is accepted as an alias for images |

## Size limits and formats

| Kind    | Limit | Formats                                                   |
| ------- | ----- | --------------------------------------------------------- |
| `image` | 10MB  | PNG, JPEG, WebP, SVG, GIF                                 |
| `font`  | 10MB  | TTF, OTF, WOFF, WOFF2                                     |
| `video` | 60MB  | MP4, WebM, MOV, AVI, MKV, FLV, WMV, M4V, GIF              |
| `audio` | 50MB  | MP3, WAV, M4A, AAC, OGG, Opus, FLAC, WebM, AIFF, WMA      |

Processing after confirm is the same as for the `/add` endpoint of each kind: images are optimised and SVGs cleaned, videos may be converted, fonts are parsed for their family name. See [Upload Brand Image](https://orshot.com/docs/api-reference/brand-assets-post), [Upload Font](https://orshot.com/docs/api-reference/custom-fonts-post), [Upload Video](https://orshot.com/docs/api-reference/brand-videos-post) and [Upload Audio](https://orshot.com/docs/api-reference/brand-audio-post) for the rules of each kind.

## Notes

- The file type is read from the bytes, not from `contentType` or the file extension. A file that is not a real image, font, video or audio file is refused at confirm.
- Unconfirmed uploads are deleted automatically. Confirm within a few minutes of the PUT.
- A `key` can only be confirmed by the workspace that requested it.

## Error responses

| Step    | Status | When                                                                                          |
| ------- | ------ | --------------------------------------------------------------------------------------------- |
| 1       | `400`  | `kind` missing or unknown, or `contentType` missing or not supported for that kind            |
| 2       | `400`  | Empty body. Send the raw bytes, for example `curl -X PUT --data-binary @file`                 |
| 2       | `403`  | The upload URL was changed or is not valid                                                    |
| 2       | `410`  | The upload URL has expired. Request a new one                                                 |
| 2       | `413`  | The file is larger than the kind's limit                                                      |
| 2       | `429`  | More than 60 PUTs in a minute for one workspace                                               |
| 3       | `400`  | `key` missing, the file is over the limit, or the bytes are not a valid file of that kind. Image problems carry the same `code` values as [Upload Brand Image](https://orshot.com/docs/api-reference/brand-assets-post#error-responses) |
| 3       | `403`  | The `key` belongs to another workspace or was not issued by this API                          |
| 3       | `404`  | Nothing was uploaded for this `key` yet, or the unconfirmed file was already cleaned up        |

## Query Parameters

| Parameter     | Type   | Required | Description                                                                    |
| ------------- | ------ | -------- | ------------------------------------------------------------------------------ |
| `embedId`     | String | No       | Embed instance ID. Pair with `embedUserId` to scope the asset to one embed user |
| `embedUserId` | String | No       | The same `userId` your app passes to the embed URL. Requires `embedId`         |

Pass them on the `/confirm` call, which is where the asset is created. See [Per-User Brand Assets](https://orshot.com/docs/orshot-embed/per-user-brand-assets).

## Rate Limits

- Steps 1 and 3: 30 requests per minute per endpoint
- Step 2: 60 PUTs per minute per workspace