Create a file upload
POST/asset.create
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, 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:
POST /asset.createwith the filename(include the extension, e.g.photo.png) and, if known, itssizein bytes. For stories, also passpurpose: "story". For avatars, passpurpose: "avatar"andsize(max 10 MiB). The response is an upload instruction:assetId,uploadUrl,uploadMethod, anduploadHeaders. Avatar responses also includeimageUrl.- Upload the raw bytes in a single request: use
uploadMethod(aPOST) againstuploadUrl, send every header fromuploadHeadersverbatim, 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. - Processing (thumbnails, previews, 512×512 WebP for avatars) happens automatically once the bytes land. There is no separate "complete" call.
- Once the asset is ready, use it:
purpose: "file"(default) — attach viaassetIdson/chat.postor/chat.updatepurpose: "story"— post via/story.postpurpose: "avatar"— passimageUrlassender.imageUrlon/chat.post(and related send endpoints), or ashosts[].imageUrlon/onair.event.create//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 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 and
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
Request
Responses
- 200
- 400
- 401
- 403
- 405
- 500
Upload instruction created.
Bad request. Common causes:
namemissing or too longsizenegative or larger than 5 GiB, or missing / larger than 10 MiB foravatarpurpose: "story"used with an organization token- Malformed JSON body
Presented invalid authentication credentials.
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).
An unsupported method was requested.
An internal error occurred.