Skip to main content

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

The 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.
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.

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:
cURL
The response carries the file_* id and a signed url:
Result
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:
Template component
Create the template over the Templates API (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:
cURL
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:
Play verb
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 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 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