> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# MMS media rich-content cookbook

> Practical MMS recipes — media sizing for reliable carrier acceptance, group sends, contact-card handoffs, SMS fallback with tracked short links, per-recipient personalization, and the error classes media traffic raises.

# MMS media rich-content cookbook

Every recipe here runs against the MMS surfaces end to end — single sends through `POST /api/v1/messages/sms`, group sends through `POST /api/v1/messages/group` — so you can copy the exact request and response shapes your integration handles. The [MMS send & receive guide](/guides/mms-send-and-receive) walks the dashboard and API from first principles; the [MMS channel reference](/channels/mms) lists every request field and cap. This page is the cookbook: sizing, patterns, fallbacks, and failure handling for media traffic.

Authenticate every request with `X-API-Key`. Run against the sandbox with a `dv_test_sk_…` key, then swap in your live key.

## Task index

| # | Recipe                                                                                                                 | Endpoints used                                 |
| - | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| 1 | [Quick send — one MMS, one image](#1-quick-send--one-mms-one-image)                                                    | `POST /messages/sms`                           |
| 2 | [Size media for carrier acceptance](#2-size-media-for-carrier-acceptance)                                              | —                                              |
| 3 | [Group send — one payload, up to 20 recipients](#3-group-send--one-payload-up-to-20-recipients)                        | `POST /messages/group`                         |
| 4 | [Hand off a contact card](#4-hand-off-a-contact-card)                                                                  | link-in-body or [WhatsApp](/channels/whatsapp) |
| 5 | [SMS fallback with a tracked short link](#5-sms-fallback-with-a-tracked-short-link)                                    | `POST /messages/sms`, `metadata.shorten_urls`  |
| 6 | [Personalize body text per recipient, share one media set](#6-personalize-body-text-per-recipient-share-one-media-set) | campaigns, per-recipient `to` loop             |
| 7 | [Handle the media error surface](#7-handle-the-media-error-surface)                                                    | `422 VALIDATION_ERROR`, webhooks               |
| 8 | [Budget for MMS pricing](#8-budget-for-mms-pricing)                                                                    | `GET /pricing/messaging`                       |

## 1. Quick send — one MMS, one image

Send through the SMS endpoint with a `media_urls` array — the send auto-upgrades to MMS as soon as one attachment is present:

```bash cURL theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/sms \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: launch-hero-98421" \
  -d '{
    "to": "+14155552671",
    "from": "+18005551234",
    "body": "New menu drop — tonight only",
    "media_urls": ["https://storage.googleapis.com/your-bucket/menu-hero.jpg"]
  }'
```

```typescript Node.js theme={null}
import { Orbit } from '@devotel-orbit/node';

const orbit = new Orbit(process.env.ORBIT_API_KEY);

const response = await orbit.messages.sendSms({
  to: '+14155552671',
  from: '+18005551234',
  body: 'New menu drop — tonight only',
  media_urls: ['https://storage.googleapis.com/your-bucket/menu-hero.jpg'],
});
```

The response confirms the upgrade with `"channel": "mms"` on the returned message. Always send an `Idempotency-Key` on single sends — a retried request returns the original message instead of dispatching a duplicate.

The `media_urls` array is additive with the singular `media_url` field — combine them or use either; together they carry up to 10 attachment URLs.

## 2. Size media for carrier acceptance

The platform enforces a **5 MB** ceiling per attachment and rejects anything larger with `422 VALIDATION_ERROR` before dispatch. Carriers enforce their own, much tighter, transcode budgets downstream — size your assets for the carriers, not the ceiling.

| Class               | Hard gate (422) | Carrier acceptance guidance                                                                                                                                                                                                                                                              |
| ------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Size per attachment | 5 MB            | **Under 300 KB** survives carrier transcode untouched. Between 300 KB and 1 MB most carriers deliver after down-rendering. Above 1 MB individual gateways frequently drop or heavily compress the media even though the platform accepted it.                                            |
| Content types       | Table below     | JPEG and PNG have the broadest carrier acceptance. GIF animates on most handsets but transcodes differently per carrier; send a still-frame JPEG if the message must look identical. WebP passes the gate but a few carriers reject or strip it — prefer JPEG/PNG on production traffic. |
| Count per message   | 10 attachments  | One attachment renders most predictably; multi-attachment order varies by handset.                                                                                                                                                                                                       |

Accepted content types (anything outside this table is rejected with `422`):

| Category | Accepted types                                       |
| -------- | ---------------------------------------------------- |
| Image    | `image/jpeg`, `image/png`, `image/gif`, `image/webp` |
| Video    | `video/mp4`, `video/3gpp`                            |
| Audio    | `audio/mpeg`, `audio/ogg`, `audio/aac`               |

Media URLs must be **HTTPS** and resolve through the send-time URL allowlist — the platform issues a `HEAD` request per URL to check type and size, and rejects URLs that fail allowlist or DNS-rebind checks before dispatch. Documents (PDF), vCards, and formats outside the table are not deliverable attachments; recipe 4 covers the contact-card path and recipe 5 the link-based fallback.

## 3. Group send — one payload, up to 20 recipients

`POST /api/v1/messages/group` fans one `body` + `media_urls` payload out to up to 20 recipients, with a per-recipient status breakdown in the response:

```bash cURL theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/group \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "+18005551234",
    "to": ["+14155552671", "+14155552672"],
    "body": "Weekend sale — 20% off everything!",
    "media_urls": ["https://storage.googleapis.com/your-bucket/sale.jpg"]
  }'
```

Semantics to know before you scale a group send:

* **Delivered-to semantics.** Each recipient gets an independent MMS — this is one-to-many fan-out, not a shared group thread. Recipients cannot see or reply to each other.
* **Partial failure.** The endpoint returns `200 OK` when every recipient succeeded and `207 Multi-Status` on any failure. Read the per-recipient `status` / `error_code` in the body, then retry only the failed recipients (see recipe 7).
* **Pre-filter non-NANP recipients.** MMS delivers to US/Canada `+1` numbers only; a non-`+1` recipient comes back failed with `MMS_NANP_ONLY`. Filter before dispatch with the partition pattern on the [channel reference](/channels/mms#error-recovery-pre-filter-nanp-recipients) to avoid paying for known non-deliverables.
* **Cap and shape.** 20 recipients per call, `body` 1–1600 characters, up to 10 attachments, shared `metadata` merged onto every recipient row. For larger broadcasts use the [batch SMS endpoint](/guides/messages-batch-sms) (with the same media upgrade) or the campaigns module.
* **Opt-out caveats.** Orbit is the conduit — consent and opt-out handling are your tenant-side controls. Suppression lists and quiet hours are tenant-configurable under **Settings → Compliance**; group sends honor the suppression list, so a suppressed recipient lands as a visible `failed` row rather than a silent drop. Keep your opt-out keywords and suppression source synced before a campaign.

## 4. Hand off a contact card

MMS does not accept vCard attachments — a `text/vcard` URL is outside the content-type allowlist and is rejected with `422 VALIDATION_ERROR` before dispatch, the same as PDFs and other documents. Two shipped paths deliver contact details to a handset:

1. **Host the vCard, send the URL.** Write the `.vcf` file to HTTPS storage and put the link in the MMS body. Most handsets render the URL tappable; the recipient downloads and saves the card. Make it a tracked short link (recipe 5) and each save shows up as a click.
2. **Send a native contacts message on WhatsApp.** WhatsApp carries first-class contact messages (full vCard payload rendered as an in-app card). If your recipients are reachable on WhatsApp, route the contact card through the [WhatsApp channel](/channels/whatsapp) instead — no tap-through required.

The same logic applies to any document MMS refuses: host it, link it, and let recipe 5 give you click analytics on the handoff.

## 5. SMS fallback with a tracked short link

Rich content over MMS stops at three boundaries: non-NANP recipients, media above what carriers accept, and content types outside the media table. In each case the fallback is a plain SMS carrying a tracked short link to the hosted image — recipients tap through, and you keep click analytics on the handoff. Because shortening is per-URL fire-and-forget, a failed mint never blocks the send — the original URL ships untouched.

```bash cURL theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/sms \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "+18005551234",
    "to": "+442071234567",
    "body": "New menu drop: https://cdn.example.com/menu-hero.jpg — tonight only",
    "metadata": { "shorten_urls": true }
  }'
```

The pipeline rewrites the URL into a tracked short link before dispatch, stamping the message id onto the mint so clicks attribute back to this send. When an MMS comes back `MMS_NANP_ONLY` on some recipients, re-send those recipients on this pattern — switch the surface, do not retry MMS. For destinations reachable on WhatsApp or RCS, inline media there beats a tap-through link.

The full pattern — inline shortening, net-shortening guard, per-campaign rollups, per-contact clicks, and the `short_link.click` webhook — is in [Short links with click tracking](/guides/short-links-and-click-tracking) and the [short-links cookbook](/guides/short-links-cookbook).

## 6. Personalize body text per recipient, share one media set

One media URL set (or one store-to-uploads upload) serves the whole audience; vary only the `body` per recipient.

**Per-recipient sends in a loop.** Render the body from a template in your application, then send one request per recipient:

```typescript Node.js theme={null}
import { Orbit } from '@devotel-orbit/node';

const orbit = new Orbit(process.env.ORBIT_API_KEY);
const media = ['https://storage.googleapis.com/your-bucket/sale.jpg'];

for (const contact of contacts) {
  const body = renderTemplate(
    'Hi {{first_name}}, 20% off this weekend at {{store}}!',
    contact
  );
  await orbit.messages.sendSms({
    to: contact.phone,
    from: '+18005551234',
    body,
    media_urls: media,
  }); // one send per recipient, shared media set
}
```

One send per recipient keeps each as its own idempotent unit — set a unique `Idempotency-Key` per recipient so a retried loop never double-sends.

**Campaign-style sends.** On a campaign, the merge-tag layer renders the body per recipient at send time against the campaign's `message_template`. The five contact fields (`{{first_name}}`, `{{last_name}}`, `{{phone}}`, `{{email}}`, `{{company}}`), plus `{{coupon_code}}` and every key on `campaign.variables`, resolve per recipient — anything unresolved renders blank with no send-time error, so run the dry-run and clear every unresolved tag before launch. The contract and pre-launch validation flow are in [Validate campaign personalization merge-tags before launch](/guides/campaign-personalization-preview).

## 7. Handle the media error surface

Media sends fail in four classes, each with a different playbook:

**Pre-send rejection (`422 VALIDATION_ERROR`).** The media `HEAD` check failed — wrong content type, over 5 MB, or a URL outside the HTTPS allowlist. Read `details.issues` on the response, fix the attachment, and retry with a fresh `Idempotency-Key`. Nothing was dispatched.

**Destination rejection (`MMS_NANP_ONLY`).** Non-`+1` recipient, rejected per message at send time. Not retryable — switch the recipient to WhatsApp/RCS or the SMS-with-link pattern (recipe 5).

**Post-dispatch carrier rejection (`undelivered` / `failed`).** Dispatch succeeded but the downstream carrier or handset refused the media — most often an oversize attachment or a carrier filter. One `message.failed` webhook event fires per recipient carrying your `media_urls`; re-send on SMS-with-link (recipe 5) and shrink the asset under the 300 KB carrier-acceptance band before trying MMS again. The carrier classes and their playbooks are in the [undelivered / failed message guide](/troubleshooting/message-undelivered-failed).

**Mixed fan-out (`207 Multi-Status`).** On a group send some recipients failed while others dispatched. Read the per-recipient `error_code` in the response body, split the failures into the classes above, and re-drive only retryable ones — never blind-retry the whole group.

Codes, HTTP statuses, and remediation for every failure surface above are catalogued in the [error code reference](/reference/error-codes).

## 8. Budget for MMS pricing

MMS bills **per message, not per attachment** — a message with three attachments costs one MMS message, and MMS is a richer (higher-priced) surface than SMS. On a group send each recipient bills as its own message; the dashboard composer shows a cost estimate before you confirm.

Rates vary by destination. Check the [pricing page](https://orbit.devotel.io/pricing) or query real-time rates:

```bash cURL theme={null}
curl "https://api.orbit.devotel.io/api/v1/pricing/messaging?country=US" \
  -H "X-API-Key: dv_test_sk_YOUR_KEY"
```

The response's `channels` array includes `mms` alongside `sms`. Fall back to SMS-with-link (recipe 5) when the media does not justify the richer per-message rate.

## Next steps

* [MMS channel reference](/channels/mms) — every request field, cap, and response shape
* [MMS send & receive](/guides/mms-send-and-receive) — dashboard + API quickstart on the same surfaces
* [Short links with click tracking](/guides/short-links-and-click-tracking) — the fallback pattern's full mechanics
* [Error code reference](/reference/error-codes) — every code this page names, with remediation
