Customer-hosted MMS
Add this extension to your existing customer-hosted SMS service to send and receive attachments directly between your server and the app. It applies to portal-provisioned SessionCloud and standalone SessionTalk Pro/Free accounts. Your server handles carrier delivery and stores the media; SessionCloud does not proxy or store it.
App update required. This extension is implemented for integration testing; the compatible app update has not yet been released to the stores. Existing customer-hosted SMS builds do not enable its MMS attachment controls. Keep SMS working while preparing and testing the server extension.
No new portal fields or provisioning QR are needed when the existing service URLs and account credentials remain the same. An updated app discovers support from the service. Text SMS continues to use the existing outgoing form endpoint.
Endpoints and authentication
All paths below are relative to the configured incoming message fetch URL.
For example, if it is https://sms.example.com/inbox:
| Operation | Request |
|---|---|
| Discover MMS support | GET /inbox/capabilities |
| Upload one attachment | POST /inbox/attachments |
| Resolve a private attachment download | GET /inbox/attachments/ATTACHMENT_UUID |
| Submit an MMS | POST /inbox |
| Fetch messages | Existing GET /inbox?after=cursor and GET /inbox/MESSAGE_UUID |
These requests use the same HTTP Basic or Bearer authorization as inbox fetching. Authenticate every request and select the user’s inbox, sending number and permitted attachments from those credentials. A client-supplied UUID is not authorization. The service credentials must also permit MMS submission for that account. Never expose carrier-wide credentials to the app.
Use HTTPS without redirects. Return JSON for successful API requests. A download URL returned by the metadata endpoint must also use HTTPS; the app does not send service credentials to that URL and does not follow redirects.
Advertise support
Return this only after the authenticated account and carrier can send MMS and your server implements the durable deduplication contract below:
{"mms":true,"mmsVersion":1,"maxBytes":600000,"maxAttachments":1}Return {"mms":false} or HTTP 404 for a service without MMS. Unknown versions,
authentication failures and unavailable endpoints do not enable the attachment
control. The app refreshes discovery while open, with a cache of up to one minute.
SMS sending and inbox access continue independently.
The app sends one attachment at a time. maxBytes is the carrier’s total byte
limit for the file and UTF-8 caption, capped at 600,000 bytes by the app. Large
camera photos can be resized; oversized videos and files are rejected. The
caption limit is 1,600 characters. Your server must enforce these limits too,
along with permitted file types and the number’s carrier capabilities.
Upload and download media
POST /inbox/attachments accepts JSON:
{"name":"photo.png","contentType":"image/png","data":"BASE64_FILE_BYTES"}After storing the file privately, return HTTP 201:
{
"attachment": {
"id": "00000000-0000-4000-8000-000000000002",
"name": "photo.png",
"contentType": "image/png",
"byteSize": 12345
}
}Use lowercase UUIDs for attachment IDs. Check decoded size, filename and file type before storing; reject disallowed types with 415 and oversized files with 413. An upload is not a send. Bind each upload to its authenticated inbox, and retain it long enough for a queued submission or document its expiry policy.
GET /inbox/attachments/ATTACHMENT_UUID returns the same metadata with an
additional url containing a short-lived, signed HTTPS download URL. Check inbox
ownership before issuing it. The app downloads and caches the bytes using that
URL; keep service credentials out of it. Return 404 for an unavailable or
unauthorized attachment. Return 503 while incoming media is still being stored.
The app accepts incoming attachments up to 5 MiB each.
Submit an MMS without duplicate sends
The app persists the message locally before posting JSON to /inbox:
{
"clientId": "00000000-0000-4000-8000-000000000003",
"to": "+12025550124",
"text": "Photo caption",
"attachments": ["00000000-0000-4000-8000-000000000002"]
}An empty text is valid when an attachment is present. The sending number comes
from the authenticated account. Check that each attachment is an authorized
outgoing upload for that account before contacting the carrier.
mmsVersion: 1 promises durable idempotency. Atomically claim
(authenticated inbox, clientId) before a billable carrier request. Persist the
recipient, caption and attachment IDs with that claim. Repeating the same request,
including after a restart or concurrently, must return the same message without
another carrier send. Reject a reused clientId with changed content using 409.
The app may repeat requests after lost responses or temporary failures, keeping
the same clientId. If carrier acceptance is uncertain, keep the original claim
and return state: "unknown"; do not blindly send again. Resolve that outcome
with the carrier. A service must not advertise version 1 if retries can produce
duplicate carrier submissions.
Return HTTP 200 or 201 with the existing message envelope:
{
"message": {
"id": "00000000-0000-4000-8000-000000000004",
"client_id": "00000000-0000-4000-8000-000000000003",
"peer": "+12025550124",
"body": "Photo caption",
"direction": "outbound",
"state": "sent",
"created_at": "2026-01-01T12:00:00Z",
"media": [{
"id": "00000000-0000-4000-8000-000000000002",
"name": "photo.png",
"contentType": "image/png",
"byteSize": 12345
}]
}
}Echo the exact clientId as client_id so the app replaces its queued entry.
sent means accepted for sending, not delivered to the recipient. Use failed
for a confirmed rejection and unknown for uncertain acceptance. Preserve the
same message UUID and response when handling retries.
Receive an MMS
Verify the carrier’s callback and number ownership, download its media safely,
and store private attachment records before exposing the message. Extend the
existing inbox message with the media array above; use direction: "inbound"
and state: "received". The caption may be empty. Incoming messages can contain
up to ten attachments. Keep UUIDs stable across duplicate carrier callbacks.
Use the existing cursor and single-message fetch endpoints. Send the usual
notification containing the message UUID, sender and a short caption, or
"Picture message" when there is no caption. Do not put media bytes, download
URLs or service credentials in the push. The app fetches the message and resolves
its attachments from the authenticated service.
Group MMS and carrier delivery-receipt callbacks are outside this extension.
Reference bridge and verification
The downloadable bridge includes this extension for
the Custom adapter. Set MMS_ENABLED=true only when its downstream adapter
accepts {id,from,to,text,media} JSON with base64 file bytes and can send MMS.
Set MMS_MAX_BYTES to the carrier’s limit. The Custom inbound signed webhook
accepts media entries containing name, contentType and base64 data.
Its A&A, Twilio and Telnyx example adapters continue to support text SMS only.
Test with an updated app: a photo in each direction, an image without a caption, caption and file size limits, unauthorized attachment access, duplicate callbacks, simultaneous send retries, a lost submission response, restart recovery, and a locked-phone notification that opens the correct conversation. Confirm actual carrier delivery separately from API acceptance.