Send to One Person
All outbound messages — SMS and MMS — go through the same endpoint:
POST /accounts/:account_id/messagesSend an SMS
The minimum required fields are to (the recipient's phone number in E.164 format) and body (the message text). If you've configured a default phone number on the account, you don't need to specify from.
curl -X POST https://api.surge.app/accounts/YOUR_ACCOUNT_ID/messages \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+18015551234",
"body": "Your appointment is confirmed for Friday at 2pm."
}'To send from a specific number, include its ID or E.164 number as from:
curl -X POST https://api.surge.app/accounts/YOUR_ACCOUNT_ID/messages \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+18015551234",
"from": "pn_01jrzhe8d9enptypyx360pcmxm",
"body": "Your appointment is confirmed for Friday at 2pm."
}'The response includes the message ID, body, and the conversation thread the message belongs to:
{
"id": "msg_01jrzhe8d9enptypyx360pcmxj",
"blast_id": null,
"body": "Your appointment is confirmed for Friday at 2pm.",
"attachments": [],
"conversation": {
"id": "cnv_01jrzhe8d9enptypyx360pcmxk"
},
"metadata": {}
}Tracking delivery
Message delivery status isn't a field on the REST response — it comes through webhook events. Subscribe to these two event types:
| Event | Meaning |
|---|---|
message.delivered | Carrier confirmed the message reached the handset (or was handed off to the carrier for local numbers) |
message.failed | The message could not be delivered; the reason field in the event explains why |
Not all carriers send delivery receipts, so message.delivered isn't guaranteed for every carrier. See Webhook Events for the full payload shapes.
Send an MMS
Include an attachments array to send MMS. Each item needs a url pointing to a publicly accessible file.
curl -X POST https://api.surge.app/accounts/YOUR_ACCOUNT_ID/messages \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+18015551234",
"body": "Here is your invoice.",
"attachments": [
{ "url": "https://acme.com/invoices/inv-1234.pdf" }
]
}'The response includes the attachment IDs. To retrieve an attachment file later, use GET /attachments/:id/file.
MMS gotchas
- Content type — some carriers reject attachment types they don't support. Common safe types are
image/jpeg,image/png, andimage/gif. PDFs deliver reliably to most handsets but not all. - File size — very large files may be silently truncated or fail delivery on certain networks. Keep attachments under 1 MB where possible.
- Body is optional — you can send an MMS with attachments only, no
bodytext. - Attachment URL restrictions — Surge fetches the attachment URL to build the MMS. The URL must be publicly accessible over
https://orhttp://(no other schemes). URLs that resolve to private IP addresses (10.x.x.x, 172.16.x.x, 192.168.x.x, 127.x.x.x, etc.) or use reserved TLDs (.local,.internal,.localhost,.test) are blocked. This means you cannot use localhost or internal network URLs for attachments, even in development. Host test files on a public URL or use a tunneling service like ngrok.
Automatic SMS-to-MMS conversion
Surge can automatically upgrade a plain SMS to MMS in two situations:
- Message exceeds 10 segments — any message over 10 segments (around 1,530 GSM-7 characters) is automatically sent as MMS regardless of whether you included attachments.
- Message is 3 or more segments and
auto_mms_enabledis on for your platform — contact support to enable this platform-level setting if you want long messages to convert automatically.
When a message is upgraded to MMS, it sends as a single media message rather than a concatenated SMS sequence. If the recipient's carrier doesn't support MMS (mms_not_supported), Surge falls back to SMS.
You never need to detect this yourself — the message.sent and message.delivered events reflect the actual protocol used.
Schedule a message
Set send_at to an ISO 8601 timestamp to schedule a message for later delivery. The maximum scheduling window is 65 days from now.
curl -X POST https://api.surge.app/accounts/YOUR_ACCOUNT_ID/messages \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+18015551234",
"body": "Your trial ends tomorrow — upgrade to keep your data.",
"send_at": "2026-06-01T09:00:00Z"
}'A scheduled message is accepted and queued immediately. Delivery events (message.delivered, message.failed) fire when the message is actually sent at the scheduled time.
Personalise messages with contact variables
If the recipient has a contact record, Surge substitutes these tokens in the message body at send time:
| Token | Replaced with |
|---|---|
{first_name} | Contact's first name, or empty string if not set |
{last_name} | Contact's last name, or empty string if not set |
{full_name} | First and last name joined, or empty string if neither is set |
curl -X POST https://api.surge.app/accounts/YOUR_ACCOUNT_ID/messages \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+18015551234",
"body": "Hi {first_name}, your order has shipped!"
}'If the contact's first_name is Jordan, the recipient receives Hi Jordan, your order has shipped!. If the contact has no first_name, the {first_name} token is replaced with an empty string — the recipient gets Hi , your order has shipped!. Set names on your contacts before sending personalised messages to avoid awkward blanks. Tokens are replaced when you pass to (a phone number or contact ID); they are not replaced on blast sends that bypass contact resolution.
Add metadata
metadata is a free-form JSON object you can attach to any message. It's returned in webhook events and list responses, which makes it useful for correlating Surge messages with records in your own system.
{
"to": "+18015551234",
"body": "Your order has shipped.",
"metadata": {
"order_id": "ord_9887",
"customer_tier": "premium"
}
}Metadata is encrypted at rest. Message metadata is stored encrypted in Surge's database. Store any data here you'd normally want protected — internal IDs, tier markers, contextual details about the send.
Keep metadata values below a few KB. Very large metadata objects have no hard size limit enforced at the API layer, but they are embedded in every webhook payload that references the message, which can make your webhook handler payload processing heavier than necessary.
Common errors
| Error type | Cause | Fix |
|---|---|---|
opted_out | The recipient has replied STOP | Do not send to opted-out contacts |
demo_message_limit | You've hit the 25-message demo cap | Register and buy a number |
link_size_limit | The message body contains too many URLs | Reduce links or use link shortening |
invalid_content_type | Attachment type unsupported by the carrier | Use a supported image format |
Full error reference is in Handle Failures and the Error Reference.