# Send to One Person
URL: /docs/sending/send-one
LLM index: /llms.txt
Description: Send SMS and MMS to a single recipient: required fields, attachments, scheduling, personalization, and common errors.
Related: /docs/sending/handle-failures, /docs/sending/track-delivery, /docs/sending/conversations, /docs/receiving/events

# Send to One Person

All outbound messages — SMS and MMS — go through the same endpoint:

```
POST /accounts/{account_id}/messages
```

## Send an SMS

The minimum required fields are `to` (the recipient's phone number in <Tooltip tip="International phone number format: starts with +, country code, then subscriber number, no spaces or punctuation. Example: +18015551234.">E.164</Tooltip> format) and `body` (the message text). If you've configured a default phone number on the account, you don't need to specify `from` — otherwise you'll get a [`no_phone_number` error](./handle-failures#no_phone_number).

<CodeGroup items={["Python","TypeScript","Ruby","Elixir","cURL"]}>

```python Python
from surge import Surge

surge = Surge()

message = surge.messages.create(
    account_id="{account_id}",
    to="+18015551234",
    body="Your appointment is confirmed for Friday at 2pm.",
)
```

```typescript TypeScript
import Surge from "@surgeapi/node";

const surge = new Surge();

const message = await surge.messages.create("{account_id}", {
  to: "+18015551234",
  body: "Your appointment is confirmed for Friday at 2pm.",
});
```

```ruby Ruby
require "surge_api"

surge = SurgeAPI::Client.new

message = surge.messages.create(
  "{account_id}",
  to: "+18015551234",
  body: "Your appointment is confirmed for Friday at 2pm."
)
```

```elixir Elixir
client = Surge.Client.new(System.get_env("SURGE_API_KEY"))

{:ok, message} =
  Surge.Messages.create(client, "{account_id}", %{
    to: "+18015551234",
    body: "Your appointment is confirmed for Friday at 2pm."
  })
```

```bash cURL
curl -X POST https://api.surge.app/accounts/{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."
  }'
```

</CodeGroup>

To send from a specific number, include its ID or E.164 number as `from`. (Python and Ruby use `from_` to avoid the reserved keyword; the wire field is still `from`.)

<CodeGroup items={["Python","TypeScript","Ruby","Elixir","cURL"]}>

```python Python
message = surge.messages.create(
    account_id="{account_id}",
    to="+18015551234",
    from_="pn_01jrzhe8d9enptypyx360pcmxm",
    body="Your appointment is confirmed for Friday at 2pm.",
)
```

```typescript TypeScript
const message = await surge.messages.create("{account_id}", {
  to: "+18015551234",
  from: "pn_01jrzhe8d9enptypyx360pcmxm",
  body: "Your appointment is confirmed for Friday at 2pm.",
});
```

```ruby Ruby
message = surge.messages.create(
  "{account_id}",
  to: "+18015551234",
  from_: "pn_01jrzhe8d9enptypyx360pcmxm",
  body: "Your appointment is confirmed for Friday at 2pm."
)
```

```elixir Elixir
{:ok, message} =
  Surge.Messages.create(client, "{account_id}", %{
    to: "+18015551234",
    from: "pn_01jrzhe8d9enptypyx360pcmxm",
    body: "Your appointment is confirmed for Friday at 2pm."
  })
```

```bash cURL
curl -X POST https://api.surge.app/accounts/{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."
  }'
```

</CodeGroup>

The response includes the message ID, body, and the conversation thread the message belongs to:

```json
{
  "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 event types:

| Event | Meaning |
|---|---|
| `message.sent` | Carrier accepted the message for transmission |
| `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](../receiving/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.

<CodeGroup items={["Python","TypeScript","Ruby","Elixir","cURL"]}>

```python Python
message = surge.messages.create(
    account_id="{account_id}",
    to="+18015551234",
    body="Here is your invoice.",
    attachments=[{"url": "https://acme.com/invoices/inv-1234.pdf"}],
)
```

```typescript TypeScript
const message = await surge.messages.create("{account_id}", {
  to: "+18015551234",
  body: "Here is your invoice.",
  attachments: [{ url: "https://acme.com/invoices/inv-1234.pdf" }],
});
```

```ruby Ruby
message = surge.messages.create(
  "{account_id}",
  to: "+18015551234",
  body: "Here is your invoice.",
  attachments: [{ url: "https://acme.com/invoices/inv-1234.pdf" }]
)
```

```elixir Elixir
{:ok, message} =
  Surge.Messages.create(client, "{account_id}", %{
    to: "+18015551234",
    body: "Here is your invoice.",
    attachments: [%{url: "https://acme.com/invoices/inv-1234.pdf"}]
  })
```

```bash cURL
curl -X POST https://api.surge.app/accounts/{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" }
    ]
  }'
```

</CodeGroup>

The response includes the attachment IDs. To retrieve an attachment file later, use `GET /attachments/{attachment_id}/file`.

### MMS gotchas

- **Content type**: some carriers reject attachment types they don't support. Common safe types are `image/jpeg`, `image/png`, and `image/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 `body` text.

<Warning>
**Attachment URL restrictions.** Surge fetches the attachment URL to build the MMS, so the URL must be publicly accessible over `https://` or `http://` (no other schemes). The following are blocked:

- Private IP addresses: `10.x.x.x`, `172.16.x.x`, `192.168.x.x`, `127.x.x.x`, etc.
- Reserved TLDs: `.local`, `.internal`, `.localhost`, `.test`

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.
</Warning>

### Automatic SMS-to-MMS conversion

Surge can automatically upgrade a plain SMS to MMS in two situations:

1. **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.
2. **Message is 3 or more segments and `auto_mms_enabled` is 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.

<CodeGroup items={["Python","TypeScript","Ruby","Elixir","cURL"]}>

```python Python
message = surge.messages.create(
    account_id="{account_id}",
    to="+18015551234",
    body="Your trial ends tomorrow, upgrade to keep your data.",
    send_at="2026-06-01T09:00:00Z",
)
```

```typescript TypeScript
const message = await surge.messages.create("{account_id}", {
  to: "+18015551234",
  body: "Your trial ends tomorrow, upgrade to keep your data.",
  send_at: "2026-06-01T09:00:00Z",
});
```

```ruby Ruby
message = surge.messages.create(
  "{account_id}",
  to: "+18015551234",
  body: "Your trial ends tomorrow, upgrade to keep your data.",
  send_at: "2026-06-01T09:00:00Z"
)
```

```elixir Elixir
{:ok, message} =
  Surge.Messages.create(client, "{account_id}", %{
    to: "+18015551234",
    body: "Your trial ends tomorrow, upgrade to keep your data.",
    send_at: "2026-06-01T09:00:00Z"
  })
```

```bash cURL
curl -X POST https://api.surge.app/accounts/{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"
  }'
```

</CodeGroup>

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 |

<CodeGroup items={["Python","TypeScript","Ruby","Elixir","cURL"]}>

```python Python
message = surge.messages.create(
    account_id="{account_id}",
    to="+18015551234",
    body="Hi {first_name}, your order has shipped!",
)
```

```typescript TypeScript
const message = await surge.messages.create("{account_id}", {
  to: "+18015551234",
  body: "Hi {first_name}, your order has shipped!",
});
```

```ruby Ruby
message = surge.messages.create(
  "{account_id}",
  to: "+18015551234",
  body: "Hi {first_name}, your order has shipped!"
)
```

```elixir Elixir
{:ok, message} =
  Surge.Messages.create(client, "{account_id}", %{
    to: "+18015551234",
    body: "Hi {first_name}, your order has shipped!"
  })
```

```bash cURL
curl -X POST https://api.surge.app/accounts/{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!"
  }'
```

</CodeGroup>

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, so 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.

```json
{
  "to": "+18015551234",
  "body": "Your order has shipped.",
  "metadata": {
    "order_id": "ord_9887",
    "customer_tier": "premium"
  }
}
```

<Note>
**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.
</Note>

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](../registration/index) |
| `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](./handle-failures) and the [Error Reference](../api-reference/errors).
