The media asset store
Every piece of rich content a customer or your organization uploads — a campaign image, an email logo, a brand favicon, a document attached to a message — travels through one server-to-server surface: the files API. Upload a file once, receive a signed read URL for it, and reference that asset from email templates, WhatsApp media, RCS catalogs, branding settings, or any other surface that renders it. Manage assets in the dashboard under Developer > Media, or directly against the endpoints covered below.This page is the concept for the files API. For the step-by-step
operator walkthrough — browsing, re-minting links, and deleting from the
dashboard — read Manage uploaded files with the Media
Library. For the raw endpoint contract and
multi-language SDK samples, see the Files API reference.
Section 1 — What the files API is for
The files API stores a single uploaded object per call in a private, tenant-scoped media bucket. It exists so that any image, audio clip, video, PDF, or spreadsheet your messages and templates cite is hosted by you, under your own access scope, rather than on a third-party CDN link that can expire or be repurposed. Upload paths across Orbit funnel into this store:
Upload an asset with
POST /api/v1/files/upload — one multipart form
with a single file field — and receive an UploadResult envelope
holding the asset’s id, a signed read URL, the sanitized filename, the
stored content type, and its size:
cURL
url is what you store in a template block or brand
setting. Read it back with GET /api/v1/files (cursor-paginated list),
GET /api/v1/files/:id (single metadata), and remove it with
DELETE /api/v1/files/:id.
Section 2 — Object keys and the tenant boundary
Every object lands under<tenant>/<fileId>_<filename>, so a file you
upload stays inside your tenant’s namespace. For a tenant with schema
name tenant_8f9e2d1c-4b3a-4f8e-9c7b-1234567890ab, an uploaded
logo.png becomes:
..
path-traversal sequences are stripped, and any character outside letters,
digits, dash, underscore, and dot is removed.
The tenant boundary is enforced server-side in apps/api/src/routes/files/files.service.ts:
uploadFilewrites to${tenantSchema}/${fileId}_${safeName}.getFile,presignFileUrl, anddeleteFileall scan with the prefix${tenantSchema}/${fileId}_. The trailing underscore is a lookup anchor: without it, a lookup forfile_abccould match a sibling objectfile_abcd_xxx.pngand return or remove the wrong file.refreshSignedMediaUrlre-mints a stored URL only when the parsed object key starts with${ownerTenantSchema}/. A caller-controllable URL pointing at another tenant’s object is left untouched.
Section 3 — Content-type gates and reject envelopes
The upload pipeline rejects a file before its bytes are stored when the declared type, magic bytes, or size fail the gate.Accepted types
The allow-list infiles.service.ts covers:
- Images:
image/jpeg,image/png,image/gif,image/webp - Audio:
audio/mpeg,audio/ogg,audio/wav,audio/aac,audio/amr - Video:
video/mp4,video/3gpp - Documents:
application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet - Text / data:
text/plain,text/csv - Archives:
application/zip(served only from private signed URLs, never inline)
files.controller.ts) is kept in lock-step
with the service list; either rejects a MIME outside this set.
Reject envelopes
Three distinct gate failures return three distinct envelopes: Unsupported MIME — 422MAX_FILE_UPLOAD_BYTES = 25 * 1024 * 1024 in
@devotel/shared/constants (25 MB), enforced by the multipart parser
stream limit, the post-buffer guard, and the service check.
Section 4 — Signed URLs are the only egress
Objects are stored privately; the only way to read them is a signed URL minted at upload or on demand. Default lifetime is one hour. Callers that pin a URL into a long-lived row — a WhatsApp media field, an email image block, a brand logo — must either request a longer lifetime at upload (up to seven days, the V4-signing maximum) or mint a fresh URL on demand withGET /api/v1/files/:id/presign. Pass ttl_ms (in
milliseconds, clamped to 60 s–7 d) on upload and presign.
This is the envelope your content has to fit:
Re-mint state diagram
cURL
Section 5 — Size, quota, and billing
Upload size is capped per file at 25 MB. Storage is bounded per organization; the cap shows in your billing surface as usage rather than a rejected request. A413 PAYLOAD_TOO_LARGE means the object
exceeded the 25 MB ceiling — split the asset, compress it, or host your
own CDN link and reference it instead.
Section 6 — Audit events
Every upload and every delete emits an audit event (file.uploaded / file.deleted) with the user id, organization id,
filename, and content type, so a run of removals or unexpected uploads
is visible from Compliance > Audit the same way template or API-key
changes are.
The controller writes the audit row via safeAuditLog in
apps/api/src/routes/files/files.controller.ts:
tenantId from organizationId so the row is
scoped correctly in per-tenant audit views.
Sample webhook/audit envelope emitted for an upload:
Section 7 — Worked example: upload to audit trail
This example ties the sections together end to end. 1. Upload a campaign image with a 7-day TTLid and url. Because ttl_ms is set to the 7-day
ceiling, the URL is safe to pin into an email template that may not send
immediately.
2. The TTL expires after 7 days
A recipient opens the email two weeks later. The platform sees the stored
URL, extracts the file id, verifies the object lives under your tenant
prefix, and re-mints a fresh short-lived URL for the render.
3. Manually re-mint after expiry
If your integration stores the URL outside Orbit, call presign directly:
file.uploaded or
file.deleted. Each row shows who performed the action, which file id
was touched, and the original filename and MIME type.
Section 8 — When the URL terminus is custom-domain-terminating
Egress for an asset is the signed URL, not a platform hostname. If you front customer-facing previews or download links behind your own custom domain, a signed URL still feeds them — the custom domain terminates the dashboard/sign-in surface (see Custom domains and managed SSL), while media stays on the signed URL path. Pair the two surfaces cleanly: use custom domains for your white-label address, and the files API for the asset itself.Cross-references
- Files API reference — full endpoint envelope and error shapes.
- Manage uploaded files with the Media Library — the dashboard operator walkthrough for browsing, re-minting, and deleting.
- Media file upload and the presign lifecycle — the shared object-storage plane underneath the files API.
- Custom domains and managed SSL — white-label hostname termination.
- Tenant isolation — the per-tenant namespace these object keys live under.
- Compliance > Audit — where upload/delete events land in the event log.