# Create a file upload

`POST /asset.create`

## Description

Create a file asset and get back a self-describing instruction for
uploading its bytes — the JSON-friendly way to attach a file (image, PDF,
document, …) to a message, supply media for a story, or host an avatar
image. Unlike [`/item.upload`](/docs/api/item-upload), which takes raw
bytes in the request body, every caller-visible step here is JSON in /
JSON out (so it can be driven from MCP and other tool-calling clients),
and the file bytes never pass through this API.

**Flow:**
1. `POST /asset.create` with the file `name` (include the extension, e.g.
   `photo.png`) and, if known, its `size` in bytes. For stories, also pass
   `purpose: "story"`. For avatars, pass `purpose: "avatar"` and `size`
   (max 10 MiB). The response
   is an upload instruction: `assetId`, `uploadUrl`, `uploadMethod`, and
   `uploadHeaders`. Avatar responses also include `imageUrl`.
2. Upload the raw bytes in a **single request**: use `uploadMethod` (a
   `POST`) against `uploadUrl`, send every header from `uploadHeaders`
   verbatim, and put the file in the request body. Send the headers exactly
   as given — they authorize the upload and select the single-request
   upload protocol; omitting any will cause the upload to fail.
3. Processing (thumbnails, previews, 512×512 WebP for avatars) happens
   automatically once the bytes land. There is no separate "complete" call.
4. Once the asset is ready, use it:
   - `purpose: "file"` (default) — attach via `assetIds` on
     [`/chat.post`](/docs/api/chat-post) or
     [`/chat.update`](/docs/api/chat-update)
   - `purpose: "story"` — post via [`/story.post`](/docs/api/story-post)
   - `purpose: "avatar"` — pass `imageUrl` as `sender.imageUrl` on
     [`/chat.post`](/docs/api/chat-post) (and related send endpoints), or
     as `hosts[].imageUrl` on
     [`/onair.event.create`](/docs/onair-api/onair-event-create) /
     [`/onair.event.update`](/docs/onair-api/onair-event-update)

A freshly-uploaded asset may take a few seconds to process (videos take
longer). Chat and story endpoints that consume the asset return a 400 with
a "still processing" message until processing completes. Avatar `imageUrl`
404s until the image is ready — wait a moment after the upload returns
before posting it.

The `uploadUrl` is short-lived; if it expires, call `asset.create` again for
a fresh instruction. Maximum file size is 5 GiB for `file` / `story`, and
10 MiB for `avatar`.

## Purposes

| Purpose | Use | Access |
|---------|-----|--------|
| `file` (default) | Chat message attachments | Organization and Personal |
| `story` | Story media (photo or video) | Personal only |
| `avatar` | `sender.imageUrl` and On-Air `hosts.imageUrl` | Organization and Personal |

Story assets are owned by the authenticated user (stories are posted as you,
not as a bot) and expire about 48 hours after creation. Because the media
must outlive the story's 24-hour lifetime, call
[`/story.post`](/docs/api/story-post) within about 23 hours of creating the
asset; after that the asset is rejected and a new one must be created.

Avatar assets are public 512×512 WebP images. They do not expire. From
API version `2026-08-25`, `sender.imageUrl` and On-Air `hosts.imageUrl`
must be a Roam-hosted avatar URL (this `imageUrl`, or a legacy
`/card-images/` or `/photos/people/` URL). Third-party image URLs return
400. See [API Versioning](/docs/guides/api-versioning) and
[Sender Profiles](/docs/guides/sender-profiles).

**Access:** Organization and Personal. `purpose: "story"` is Personal only.

**Required scope:** `item:write` for `purpose: "file"`; `chat:send_message`
or `chat:write` for `purpose: "story"`; any of `item:write`,
`chat:send_message`, `chat:write`, or `onair:write` for `purpose: "avatar"`.

---

**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)

## Authentication

```
Authorization: Bearer YOUR_API_KEY
```

## Request Body

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `name` | string | Yes | File name, including its extension (e.g. `report.pdf`). Processing determines the media type from the extension. |
| `size` | integer | No | File size in bytes, if known. The true size is enforced server-side during the upload. Maximum 5 GiB. Required for `purpose: "avatar"` (maximum 10 MiB).  |
| `purpose` | "file" | "story" | "avatar" | No | What the asset will be used for. `file` (default) for chat message attachments; `story` for story media (Personal tokens only); `avatar` for `sender.imageUrl` and On-Air host photos.  |

## Responses

### 200 - Upload instruction created.

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `assetId` | string | Yes | ID of the created asset. Pass it to chat.post / chat.update via `assetIds`, or to story.post, once the upload completes.  |
| `uploadUrl` | string | Yes | URL to upload the file bytes to. |
| `uploadMethod` | string | Yes | HTTP method to use for the upload request (always `POST`). |
| `uploadHeaders` | object | Yes | Headers to send verbatim on the upload request. They authorize the upload and select the single-request upload protocol.  |
| `imageUrl` | string | No | Present for `purpose: "avatar"`. Canonical public URL to pass as `sender.imageUrl` or On-Air `hosts[].imageUrl` after the upload completes. 404s until image processing finishes.  |

### 400 - Bad request. Common causes:
- `name` missing or too long
- `size` negative or larger than 5 GiB, or missing / larger than
  10 MiB for `avatar`
- `purpose: "story"` used with an organization token
- Malformed JSON body


### 401 - Presented invalid authentication credentials.

### 403 - The token lacks the required scope (`item:write` for file assets;
`chat:send_message` / `chat:write` for story assets; `item:write` /
`chat:send_message` / `chat:write` / `onair:write` for avatar assets).


### 405 - An unsupported method was requested.

### 500 - An internal error occurred.

---

*Machine-readable API documentation.*
*Full documentation: https://developer.ro.am/docs/api/asset-create*
