Sending MMS
MMS is a carrier messaging channel that delivers images, video, vCard contacts, and plain text over the same number and carrier infrastructure as SMS. This guide covers how to send MMS using the Sent API.
Availability
Whether a message can go out over MMS depends on your routes: the recipient's market, the sending number, and whether that number carries MMS.
MMS is priced per message at a higher rate than SMS. A message goes out over MMS only when it carries an attachment, so a text-only send is never billed at MMS rates.
Sending an MMS Message
MMS uses the same POST /v3/messages endpoint as every other channel. Set channel: ["mms"] to pin the send to MMS. A pinned MMS send needs an attachment, which can come from two places:
curl -X POST "https://api.sent.dm/v3/messages" \
-H "x-api-key: $SENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": ["+1234567890"],
"channel": ["mms"],
"template": {
"id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
"parameters": {
"customerName": "Jane"
}
}
}'import SentDm from '@sentdm/sentdm';
const client = new SentDm();
const response = await client.messages.send({
to: ['+1234567890'],
channel: ['mms'],
template: {
id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
parameters: { customerName: 'Jane' }
}
});
console.log(`Message ID: ${response.data.recipients[0].message_id}`);from sent_dm import SentDm
client = SentDm()
response = client.messages.send(
to=["+1234567890"],
channel=["mms"],
template={
"id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
"parameters": {"customerName": "Jane"}
}
)
print(f"Message ID: {response.data.recipients[0].message_id}")import (
"context"
"github.com/sentdm/sent-dm-go"
)
client := sentdm.NewClient()
response, err := client.Messages.Send(context.Background(), sentdm.MessageSendParams{
To: []string{"+1234567890"},
Channel: []string{"mms"},
Template: sentdm.MessageSendParamsTemplate{
ID: sentdm.String("7ba7b820-9dad-11d1-80b4-00c04fd430c8"),
Parameters: map[string]interface{}{
"customerName": "Jane",
},
},
})import dm.sent.client.SentDmClient;
import dm.sent.client.okhttp.SentDmOkHttpClient;
import dm.sent.core.JsonValue;
import dm.sent.models.messages.MessageSendParams;
SentDmClient client = SentDmOkHttpClient.fromEnv();
MessageSendParams params = MessageSendParams.builder()
.addTo("+1234567890")
.addChannel("mms")
.template(MessageSendParams.Template.builder()
.id("7ba7b820-9dad-11d1-80b4-00c04fd430c8")
.parameters(MessageSendParams.Template.Parameters.builder()
.putAdditionalProperty("customerName", JsonValue.from("Jane"))
.build())
.build())
.build();
var response = client.messages().send(params);using Sentdm;
using Sentdm.Models.Messages;
SentDmClient client = new();
MessageSendParams parameters = new()
{
To = new List<string> { "+1234567890" },
Channel = new List<string> { "mms" },
Template = new MessageSendParamsTemplate
{
Id = "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
Parameters = new Dictionary<string, string>
{
{ "customerName", "Jane" }
}
}
};
var response = await client.Messages.Send(parameters);Two optional fields on POST /v3/messages carry MMS content:
| Field | Description |
|---|---|
media_urls | Array holding one https URL to attach. MMS carries one attachment, so a second is rejected with 400. Replaces the template's own media rather than adding to it. |
subject | Subject line shown above the body on most handsets. Maximum 80 characters. |
When neither field is supplied, the template's own MMS body provides the media. The text comes from the template's mms body, or its shared multiChannel body if it has no mms body, or its sms body if it has neither. The subject and attachments come only from an mms body, so a template without one needs media_urls to send as MMS. When media_urls is supplied, it overrides whatever the template carries, so a template can hold a default creative while a caller sends something recipient-specific. A template saved with more than one attachment before the one-attachment limit still sends, carrying only its first.
Each media URL must be an absolute https URL on a hostname. A literal IP address is refused with 400.
Supported Media Types
These are the formats Sent supports on an outbound MMS:
| Format | mediaType to declare |
|---|---|
| JPEG, PNG, GIF | image |
| MP4, 3GP | video |
| vCard contact cards | contact |
| Plain text | Omit the field |
The carrier fetches the media from the URL at delivery time and reads the content type off the object it fetched. Keep media files publicly accessible and hosted on a stable URL.
mediaType is a label, not a guarantee. The API accepts four values (image, video, document, contact) and rejects audio when a template is saved. It does not check the label against the file: Sent reads only the size your server declares for it (see Media Size Limits), never its content. Declaring document on a PDF is accepted at send time, but PDF is not in the preceding set, and the failure surfaces at the carrier long after Sent answered 202. Send only the formats listed.
The field is optional, so omit it when none of the four describes your file. It exists so an authoring UI can preview the attachment, not to select a delivery path.
Media Size Limits
An attachment must be 600 KB or smaller. That is the size the carrier network delivers end to end. Over it, a carrier can accept and charge for a message and then discard the attachment before it reaches the handset.
Sent checks the size on the send itself, after your 202. It sends one HEAD request to the media URL and reads the Content-Length your server declares. It does not download the file.
| Result of the check | Auto-detect | channel: ["mms"] |
|---|---|---|
| 600 KB or smaller | Sent as MMS | Sent as MMS |
| Over 600 KB | Falls to the SMS route on the same number, with the attachment as a link when it fits (see Auto-Detect Routing and MMS) | Fails |
Size unknown: no Content-Length, a non-2xx response, or an unreachable server | Sent as MMS | Sent as MMS |
An unknown size never blocks a send, because a check that learned nothing about the file is no reason to fail it. The carrier still applies its own limits to that message after acceptance. Host media on a server that answers HEAD with a Content-Length, as most CDNs and object stores do, so the check can protect the send.
An oversized attachment produces no validation error on the request. The send is accepted, and the outcome arrives later as a delivery status, or on auto-detect as an SMS.
The ceiling a carrier delivers intact depends on its tier:
| Carriers | Delivered intact up to |
|---|---|
| Tier 2 carriers | 600 KB |
| Tier 3 carriers | 300 KB |
Images and video over a carrier's ceiling are resized automatically on the way out. Resizing is lossy, so treat it as a safety net rather than a workflow, and design for 300 KB if you need tier 3 carriers to deliver the attachment intact.
Auto-Detect Routing and MMS
When you omit channel, auto-detect selects the best-matching route per recipient based on your routing rules. MMS routes sit above SMS routes in priority for the same sender number, so a message with media prefers MMS over SMS when both are available. RCS and WhatsApp routes typically outrank MMS when the recipient is reachable there. The exact channel always depends on which routes are configured for your account and the recipient.
MMS only enters auto-detect consideration when the send carries an attachment. A text-only send never produces an MMS candidate, regardless of routing rules: the pipeline drops MMS candidates without media before routing resolves. This prevents a plain-text message from being billed at MMS rates.
Attachments reach the send from either the template's mms body (see Template Definition) or the per-send media_urls field. If neither provides media, auto-detect skips MMS and falls to SMS.
When a message with an attachment falls to SMS, including when its MMS candidate fails after routing chose it (for example, an attachment over 600 KB), the attachment URL is appended to the text as a link. The link is added only when it fits without adding a billed segment. Otherwise the SMS carries the text alone.
Pinned vs. Auto-Detect
channel: ["mms"] (pinned) | channel omitted (auto-detect) | |
|---|---|---|
| Falls back if no MMS route | No (fails with ERR_NO_ROUTE_MATCHED) | Yes (falls to SMS) |
| Requires media | Yes (refused with 400 without one) | MMS only fires when message has media |
| Attachment over 600 KB | Fails | Falls to SMS, with the attachment as a link when it fits |
Pinning channel: ["mms"] without an attachment is refused with 400 (VALIDATION_001) before anything is sent: add media_urls, or use a template with media in its mms body. Send text-only messages on sms. If you pin MMS and the recipient has no MMS-capable route, the message fails rather than falling back.
Template Approval
When Meta approves a template, the approval covers its MMS channel too, the same way it covers SMS and RCS. Meta reviews the template's text; it never sees the MMS media.
Quiet Hours
MMS follows the same quiet-hours rules as SMS. Sends scheduled inside a recipient's legally protected quiet-hours window are deferred rather than failed.
Delivery Reports
MMS delivery reports arrive on the same webhook you configure for other channels. The message moves through the normal lifecycle (QUEUED → ROUTED → SENT → DELIVERED / FAILED) and fires the same message.delivered and message.failed events.
Inbound MMS
Sent identifies an inbound MMS as one: the message is recorded with channel: "mms" and relayed to your webhook as an ordinary message.received event, including when it carries no caption.
Attachments arrive as links to the carrier's copy. message.received carries a media array with one entry per attachment. Sent records where the carrier holds each file; it does not download or store the file itself.
{
"field": "message",
"event": "message.received",
"timestamp": "2025-10-31T10:10:42Z",
"payload": {
"message_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"updated_at": "2025-10-31T10:10:40Z",
"account_id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
"inbound_number": "+1234567890",
"outbound_number": "+1987654321",
"text": null,
"channel": "mms",
"media": [
{
"url": "https://media.example-carrier.com/7f3c2a9e/photo.jpg",
"mime_type": "image/jpeg",
"size_bytes": 245760,
"hash_sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
],
"received_at": "2025-10-31T10:10:40Z"
}
}| Field | Description |
|---|---|
url | Where the carrier hosts the file. Always present |
mime_type | The content type the carrier reported, for example image/jpeg. Omitted when the carrier declared none |
size_bytes | The size the carrier declared. Omitted when it declared none |
hash_sha256 | The SHA-256 the carrier declared for the file. Omitted when it declared none |
Only url is guaranteed. Some carriers declare the type, size, and hash; others send only the address. Sent never guesses a missing value, so read the type from the response when you fetch the file. Only https links are relayed.
Download attachments when they arrive. The url is the carrier's own link. It needs no authentication, so anyone holding it can fetch the file, and it expires on the carrier's schedule, which varies: assume days, not months. The webhook is the only place Sent gives you the link, and Sent can't fetch the file once the link lapses. Copy anything you need to keep into your own storage, and verify it against hash_sha256 when that's present.
media is omitted when a message carries no attachments, so an SMS payload keeps exactly the shape it has always had. A media-only MMS arrives with text: null; treat that as a normal value rather than an empty message.