Skip to main content
To send images, video, audio, or documents, you first upload the file and get a file_uuid, which you then pass to a send action.
1

Request an upload URL

With { filename, mime_type }. It returns 201 with file_uuid, upload_url, upload_token, storage_path, and expires_at.
2

Upload the binary

Upload the file to the upload_url you received, before it expires (expires_at).
3

Confirm

With { file_uuid }. It returns 200 with file_uuid, mime_type, size_bytes, requires_transcription, download_url, and expires_at. From here the file_uuid is sendable.
You no longer declare size_bytes when requesting the URL: the server measures the real size on confirm. In the confirm response, requires_transcription signals whether this file_uuid will require a transcription when used in a send — because the file type isn’t auto-transcribable or it exceeds 20 MB. It’s a hint so you can prepare it ahead of time.Validate the file size locally before uploading: the per-type limit and the storage quota are enforced at confirm, with the binary already transferred. A business with its storage disabled (quota_bytes = 0) or already over its cap is rejected at request-upload with 413 before the transfer.

Size limits

Concurrent pending uploads: a business can have at most 100 unconfirmed uploads at a time. Beyond that, request-upload returns 429 TOO_MANY_PENDING_UPLOADS with Retry-After — confirm or let your pending uploads expire before requesting more. The normal flow (request → PUT → confirm) never hits it; it only affects integrations that request many request-upload up front without confirming.

Send the file

Once confirmed, pass the file_uuid to a send action — as direct media in send_message, or as a template header:
The header.typeimage | video | document carries file_uuid (document accepts an optional filename) and a transcription that is required when vision can’t transcribe the file (the requires_transcription above anticipates it). In send_message media mode, the file_uuid goes at the action level with body as an optional caption (audio doesn’t accept a caption) and the same transcription rule.
If the preflight rejects the group, the response is a synchronous 4xx and no action runs. See Errors.
Uploaded media is ephemeral. It is not permanent storage: under storage quota pressure, the system may automatically delete files by last-use age (the ones longest without being sent go first; reusing a file_uuid in any conversation refreshes its “last use” and keeps it alive for all of them). Keep your own copy if you need a durable record. The API does not offer a stable download by file_uuid; the only download is the signed download_url returned by /files/confirm, valid for a limited time. Resending an already-deleted file_uuid responds FILE_NOT_FOUND.Reuse across conversations. The same file_uuid can be sent to several conversations without re-uploading, but it is a single file: if quota cleanup deletes it, it is lost in all conversations that reference it. If you need independence between conversations, upload one file per conversation.

Executable example

The full flow in three calls. The file_uuid confirmed in step 3 is the one you then pass to a send action. 1 · Request an upload URL
2 · Upload the binary to the upload_url before it expires
3 · Confirm

With required transcription

If the file isn’t auto-transcribable by vision (an unsupported type such as .docx/.xlsx, or larger than 20 MB), the /confirm response anticipates it with requires_transcription: true:
200 Response (transcription required)
In that case, when you send it you must include transcription — as the primary media in send_message or as template.header.transcription — or the action fails with TRANSCRIPTION_REQUIRED / TEMPLATE_HEADER_TRANSCRIPTION_MISSING:
Send with transcription