Skip to Content
SessionCloud Portal

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:

OperationRequest
Discover MMS supportGET /inbox/capabilities
Upload one attachmentPOST /inbox/attachments
Resolve a private attachment downloadGET /inbox/attachments/ATTACHMENT_UUID
Submit an MMSPOST /inbox
Fetch messagesExisting 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.

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.

Last updated on