> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Media assets end to end: upload, tag, and use in a template

> A runnable cookbook for the Files API and Media Library — upload an asset over the API or console, keep a durable reference, and attach it to a WhatsApp template header, an MMS payload, or a voice prompt.

# Media assets end to end: upload, tag, and use in a template

The [Media Library](/guides/media-library) page lists what your workspace
has; this cookbook is the end-to-end loop operators actually run: upload
the asset, hold onto a reference that survives, and point a template or
send at it. Worked examples cover the three places media reaches a
customer — a WhatsApp template header, an MMS payload, and a played-audio
voice prompt.

<Note>
  Upload and delete require the `files:write` scope on your API key; list
  and presign read under `files:read`. Console access is gated to owner,
  admin, and developer roles — the same gate the server applies.
</Note>

## 1. What lives in the Media Library

Every upload surface writes to one tenant-owned storage namespace: images
dropped into the email Builder, message-composer attachments, recorded
greetings, and porting documents. The file table shows the `file_*` id,
filename, MIME type, size, and upload timestamp, and the Files API lists
the same rows.

Channel-safe MIME classes:

* **Images** — JPEG, PNG, GIF, WebP (WhatsApp headers, MMS image, email
  blocks)
* **Audio** — MP3, OGG, WAV, MP4-audio (voice prompts played to callers)
* **Video** — MP4, 3GPP (WhatsApp video headers, MMS video)
* **Documents and text** — PDF, DOC/DOCX, XLS/XLSX, plain text, CSV

Uploads are capped at 25 MB; oversized uploads return **413** and an MIME
type outside the allow-list returns **422**. The server also sniffs the
magic bytes and rejects a file whose actual content disagrees with the
declared `Content-Type` with a **422** — so a renamed executable cannot
reach your library dressed as an image.

## 2. Uploading assets

Choose the API for anything you reference from code, and the console
(**Settings → Media**) when a human is picking the file.

### In the console

Open **Settings → Media** and use the upload action to drop the file into
the tenant bucket. The row it creates carries the file id and a fresh
signed link — everything you need for step 3.

### Programmatically

`POST /api/v1/files/upload` accepts one `multipart/form-data` field named
`file`:

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/files/upload?ttl_ms=604800000" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -F "file=@product-photo.png"
```

The response carries the `file_*` id and a signed `url`:

```json Result theme={null}
{
  "data": {
    "id": "file_x3k9",
    "url": "https://storage.googleapis.com/…/signed…",
    "filename": "product-photo.png",
    "content_type": "image/png",
    "size": 184223,
    "uploaded_at": "2026-09-14T09:30:11Z"
  },
  "meta": {}
}
```

**The presigned-URL flow.** Every read of the object goes through a
time-limited signed URL. The upload response defaults to a
**1-hour lifetime**; `ttl_ms` stretches it up to the **7-day signing
ceiling** (604800000 ms). The rule for templates: if the URL will be
stored — a saved email Builder block, an MMS send request, a played-audio
verb — pass `ttl_ms` on upload, and prefer passing the **`file_*` id**
wherever the channel accepts it, since a stored signed URL always has a
deadline either way. The errors to expect: **422** for an unsupported
MIME class or a content sniff mismatch, **413** when the file exceeds
25 MB, and **404** when a presign requests a file id outside your tenant
namespace.

## 3. Attaching media to templates

Three attach shapes, one asset. Tag point: label the file consistently at
upload (`product-photo.png`, `welcome-greeting.wav`) — filename and MIME
type are the two identifiers the console filter searches, so naming is
your tagging scheme.

### WhatsApp template header image

A WhatsApp `HEADER` component with `format: "IMAGE"` (or `"VIDEO"`) puts
your uploaded asset at the top of every message rendered from the
template:

```json Template component theme={null}
{
  "type": "HEADER",
  "format": "IMAGE",
  "example": { "header_handle": ["file_x3k9"] }
}
```

Create the template over the
[Templates API](/api-reference/templates) (`components` array), then send
it by `template_name`. Meta reviews the header media alongside the body
during approval, so upload clean — carrier-safe classes from section 1 —
and the handle you pin is the `file_*` id the upload response returned,
which survives far longer than any signed URL would.

### MMS attachment

Pass the media URL on the plain message send:

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/messages" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552671",
    "body": "Your receipt is attached.",
    "media_url": "https://storage.googleapis.com/…/file_x3k9…"
  }'
```

At least one of `body`, `template_name`, or `media_url` is required. MMS
accepts one attachment on this surface; the media-conversion endpoint
(`POST /messages/media/convert`) re-encodes HEIC/BMP/TIFF or oversized
images down to a carrier-native format before you point the send at them.

### Voice prompt

Reference the uploaded audio in a `play` verb — either in `staticVerbs`
on the create call or served from your `answer_url` when the call picks
up:

```json Play verb theme={null}
[
  { "verb": "play", "url": "https://storage.googleapis.com/…/file_greeting…" }
]
```

Upload MP3/WAV under the audio classes above, include the full signed URL
at the longest practical `ttl_ms`, or re-sign via the presign endpoint
before placing calls in bulk. The
[programmable voice guide](/guides/programmable-voice-call) walks both
control modes.

## 4. Deletion, retention, and deduplication

`DELETE /api/v1/files/{id}` is permanent → returns **204**, and there is
no usage check: the platform does not stop you deleting a file a template
still references, because draft-template references are yours to manage.
Search templates for the file id or signed URL before confirming.

Retention is tenant-owned: the
[Retention console](/guides/settings-data-retention) applies per-channel
media windows (voice recordings, SMS/MMS, email, WhatsApp, RCS, Viber)
with a 7–3650-day clamp, and a lifecycle safety net hard-purges media at
365 days regardless. Deduplication is naming discipline: there is no
automatic content-hash dedupe — the same bytes uploaded twice land twice
— so pin one canonical file id per asset and reuse it across channels.

## 5. Efficient patterns

* **Store file ids, not signed URLs.** A `file_*` id is durable; a signed
  URL never lasts past its signing window. Re-mint with
  `GET /api/v1/files/{id}/presign?ttl_ms=…` (optional TTL clamped between
  1 minute and 7 days; omit it for the 1-hour default).
* **Set `ttl_ms` at upload for long-lived rows.** Email Builder image
  blocks and archived media pinned without an explicit TTL breaking
  silently at the 1-hour default is the most common failure this library
  sees.
* **Respect per-channel limits at the source.** MMS carriers cap media
  size below the 25 MB upload ceiling, and WhatsApp image headers have
  their own Meta limits — the carrier, not the library, rejects an
  oversized attachment at send time.
* **CDN and caching.** Signed URLs ride the Google Cloud Storage edge;
  you cannot cache the URL pair, but you can cache the file id and re-sign
  on demand, which is cheaper than re-uploading.

## 6. Webhooks and moderation callbacks

The upload path validates synchronously — MIME allow-list, magic-byte
sniff, size cap — so there are no asynchronous moderation callbacks to
subscribe to and no file-upload webhook events. A rejected upload answers
**422** inline with the failing class (`Unsupported file type`, `content
does not match declared type`), which keeps your template-attach loop
atomic: either the asset is safe to reference, or the response tells you
why not before any template points at it.

## See also

* [Media Library console](/guides/media-library) — browse, re-mint, and
  delete from Settings → Media.
* [Files API reference](/api-reference/files) — full upload, list,
  presign, and delete schemas.
* [Templates API](/api-reference/templates) — attach a header component
  and reference it by `template_name`.
* [Retention console](/guides/settings-data-retention) — the per-channel
  media windows that govern stored files.
