Skip to main content

The media attach pipeline: upload, scan, and per-channel delivery

Every image, audio clip, or document you send through Orbit travels one pipeline: upload, virus scan, per-channel size and type checks, provider push, delivery receipt. This page connects those stages to the exact API calls and dashboard surfaces that drive them, so when a send fails on a size cap or a scan verdict you know which stage refused it and what to do next. The inbound direction runs the same gauntlet in reverse: media a contact sends you (MMS attachments, email attachments) is byte-scanned before it reaches an agent inbox or the message store. The inbound model is covered in Inbound media threat scan and quarantine model and Inbound attachment threat scan; this page covers the outbound path you compose and the surfaces both directions share.

1. What the attach pipeline connects

The outbound path is five stages. Each one can refuse the file, and each refusal carries its own code:
  1. Upload. POST /api/v1/files/upload accepts a multipart body and stores the bytes in your tenant’s media bucket. The intake checks run before buffering: a MIME type outside the allow-list fails with a 422, and anything over the 25 MB ceiling fails with a 413.
  2. Virus scan. The uploaded file enters the content scanner and sits in pending until a verdict lands. Any download, attachment fetch, or agent read on a pending file returns 402 FILE_SCAN_QUARANTINED with a scan_id and a scan_eta_seconds estimate instead of the bytes.
  3. Attach to a send. POST /api/v1/messages carries the file as media_url (or media_urls for multiple attachments) referencing the signed URL the upload returned. The message envelope never embeds your bytes; it references the stored object.
  4. Channel size check. Before the provider is called, the send is checked against the per-channel allow-list and caps in the next section. A 4 MB video is a valid upload and an invalid MMS; the rejection names the channel cap it tripped, not the upload cap.
  5. Provider push and delivery receipt. Cleared media is pushed to the channel and the delivery receipt flows back through the normal message status pipeline — see Message send request lifecycle.
A quarantined or oversized file never reaches stages 4 and 5: the hold is a fault-line decision at the earliest stage that can make it, so suspect content is never passed to your recipient.

2. Per-channel file-class allow-list

Intake and channel checks are separate gates with separate ceilings. The upload allow-list covers the file classes the channels actually send: JPEG, PNG, GIF, and WebP images; MP3, OGG, WAV, and M4A audio; MP4 and 3GPP video; PDF, Word, and Excel documents; plain text and CSV. SVG and executable types are deliberately absent — an SVG can embed script content, so it is refused at intake rather than stored and served back. Per channel, the caps shrink from the 25 MB intake ceiling to what the provider and carrier gateways accept: Read the error message before retrying: it states the exact ceiling in bytes that tripped, and it tells you whether the refusal came from intake (the 25 MB upload cap) or from the channel (the per-channel cap). The two are fixed on different sides — shrink or re-encode for a channel cap, split or re-host for the intake cap. The per-surface table and fixes are in Troubleshooting: attachment size caps and MMS media and rich content.

3. The quarantine decision tree

The scanner grades every file it inspects and routes the verdict:
  • clean — no threat signal. Downloads, attachment fetches, and sends proceed against the stored object from here on.
  • pending — no verdict yet. The file is held fail-closed: any read returns 402 FILE_SCAN_QUARANTINED until the scanner answers, so bytes are never served to a recipient or an AI agent before the verdict.
  • quarantined — the scanner flagged the content. The file is dropped before it can reach a delivery, an attachment fetch, or an agent context, and replaying the identical bytes never heals it.
Inbound media adds a banded verdict on top of the same states: the byte scan scores each attachment from 0 to 100 and bands it clean, suspicious, or malicious, mapping to allow, review, and quarantine. An external antivirus verdict, where one is wired, is the dominant signal: a malicious external verdict quarantines outright; a suspicious one lands in the review band rather than going straight to delivery. Byte-level signatures — executable headers, macro payloads in Office documents and PDFs, script shebangs — raise the score even without an external feed. Suspicious and malicious results route to quarantine or human review; nothing that fails to reach a clean verdict is served. Work a verdict from the dashboard the same way you work it over the API: Settings → Media shows each flagged asset with its quarantine state, and lets you re-enable a false positive or remove the file. The console loop is in Manage media quarantines and upload policy; the 402 envelope mechanics, the poll-to-clean retry loop, and the escalation path with the scan_id are in Troubleshooting: FILE_SCAN_QUARANTINED. Where the hold is about what the content says rather than what it can do — sensitive data patterns, policy matches — that is the data-loss prevention surface, covered in DLP scanner.

4. The Files and media API surface

The upload path is driven by five endpoints, guarded by the files:read and files:write API-key scopes: Attach to a send by referencing the returned URL: POST /api/v1/messages takes media_url (or media_urls for up to ten files) alongside the channel and body fields. Email additionally accepts inline base64 content per attachment. There is no separate “quarantine list” endpoint with its own shape — the verdict lives on the file row, so the same GET /api/v1/files list and GET /api/v1/files/{id} get you the quarantine state, and Settings → Media in the dashboard renders the same rows. The full request/response contracts are in the Files API reference.

Worked example: end-to-end media send over the Files API

Upload a product image, poll it to clean, and send it on MMS:
If step 2 returns the 402 quarantine envelope instead, branch on the state as in the troubleshooting loop: pending polls again after scan_eta_seconds; quarantined stops the loop and replaces the artifact.

5. Presigned URL vs direct-body upload

Uploads are always direct-body: one multipart POST /api/v1/files/upload with the file in a file field. The API stores the object and hands back a signed read URL; bytes are never proxied through the API on the read side either — GET /api/v1/files/{id}/presign mints a fresh signed URL instead. The choice you actually make is on the attach side, and on the signed URL lifetime:
  • Reference by URL (media_url). The default for every channel. The message carries a pointer; the provider fetches the object at send time. Use the upload’s returned url for sends that happen soon after the upload.
  • Inline base64 content (email only). Hand the bytes in the request itself. Reserved for small assets you do not want to store — it counts against the same 10 MB per-attachment and 25 MB per-email caps, and it skips the reuse and audit trail a stored file gives you.
  • Pin a longer TTL when the URL outlives an hour. Signed read URLs default to a 1-hour lifetime. If you store the URL in a long-lived row — an email-builder image block, media archived for redelivery — pass ttl_ms on upload or on presign (up to 7 days, the signing maximum) so the link is still live when the row re-renders. An expired link is not a scanner verdict: mint a fresh one with presign rather than re-uploading.

6. Tenant-visible audit surface

Each upload and each delete writes an audit row scoped to your organization, visible in the dashboard’s audit trail and exportable like any other audit event: the action (file.uploaded, file.deleted), the file id, and the filename, content type, and size at intake. Scan runs are carried on the file row rather than as a separate audit stream — the scan_id in a 402 envelope names the individual scan run, so you and support read the same verdict record when you escalate. Who sees what stays tenant-scoped: your organization’s members see your files, your audit rows, and your quarantine states; another tenant’s rows live in a separate namespace and return 404 on direct access. Escalating a verdict shares exactly three things — the file id, the scan_id, and why the content type is expected in your flow — nothing else leaves your tenant.

7. Troubleshooting

The wider intake-rejection cluster (INVALID_FILE_TYPE, MAGIC_BYTE_MISMATCH, FILE_TOO_LARGE, INVALID_AUDIO) and its retry semantics are in Troubleshooting: fan-out and upload errors; 502-class cases where the platform rather than your payload is at fault are in Troubleshooting: MEDIA_UPLOAD_FAILED.

See also