Attachments
Sending media is two steps: upload the bytes to get an attachment id, then reference that id when sending a message. Oversized images and video are transcoded down automatically to fit the channel’s limits (SMS/MMS caps are far smaller than iMessage’s).
1. Upload
POST/v1/attachments
multipart/form-data with one or more file parts (up to 10 per request):
curl -X POST https://api.msgbubbles.com/v1/attachments \
-H "Authorization: Bearer sk_live_…" \
-F "file=@/path/to/photo.jpg"{
"data": [
{
"id": "3f8a2c41-…",
"filename": "photo.jpg",
"mime_type": "image/jpeg",
"size_bytes": 84213,
"url": "https://…signed…",
"message_id": null,
"created_at": "2026-06-11T18:21:04.000Z"
}
],
"request_id": "req_…"
}Large files: presigned direct upload
POST/v1/attachments/presign
For big media (long videos), skip proxying bytes through the API: request an upload target, then PUT the file straight to storage.
curl -X POST https://api.msgbubbles.com/v1/attachments/presign \
-H "Authorization: Bearer sk_live_…" \
-H "content-type: application/json" \
-d '{ "filename": "demo.mp4", "content_type": "video/mp4" }'{
"data": {
"id": "9b1d4e72-…",
"upload_url": "https://…signed-put…",
"method": "PUT",
"headers": { "content-type": "video/mp4" }
},
"request_id": "req_…"
}2. Send with the attachment
Pass the ids as attachment_ids on POST /v1/messages. An attachment id is single-use: once a message claims it, reusing it returns 400 invalid_attachments.
curl -X POST https://api.msgbubbles.com/v1/messages \
-H "Authorization: Bearer sk_live_…" \
-H "content-type: application/json" \
-d '{
"to": "+15555550123",
"from_handle": "+18005551111",
"text": "check this out",
"attachment_ids": ["3f8a2c41-…"]
}'Voice notes
Set "voice_memo": true on the send and audio attachments render as native voice-note bubbles (iMessage voice memo / WhatsApp voice message) with a play button and duration, instead of a generic audio file.
Downloading media
GET/v1/attachments/:id
Returns the attachment’s metadata plus a short-lived signed url (about an hour). URLs expire — re-fetch the attachment when you need a fresh one rather than storing the link. Inbound message media (photos people send you) arrives the same way: the message.received webhook flags has_attachments, and the message object lists attachment ids to fetch.