URL: https://docs.sent.dm/llms/start/guides/sending-messages.txt How to send SMS, WhatsApp, and RCS messages with the Sent API: template sends, free-form text without a template, channel selection, and error handling. # Sending Messages This guide shows you how to send SMS, WhatsApp, and RCS messages with the Sent API, using a template or free-form text. ## Overview A **Message** in Sent represents a single communication sent to a contact through a specific channel (SMS, WhatsApp, or RCS). This guide covers the core aspects of message sending. ## The Message Lifecycle ## Sending Methods ### 1. Send to Phone Number The simplest approach - send directly to any phone number: ```bash curl -X POST "https://api.sent.dm/v3/messages" \ -H "x-api-key: $SENT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": ["+1234567890"], "template": { "id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8", "parameters": { "customerName": "John Doe", "orderNumber": "#12345" } } }' ``` ```typescript import SentDm from '@sentdm/sentdm'; const client = new SentDm(); const response = await client.messages.send({ to: ['+1234567890'], template: { id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8', parameters: { customerName: 'John Doe', orderNumber: '#12345' } } }); console.log(`Message ID: ${response.data.recipients[0].message_id}`); ``` ```python from sent_dm import SentDm client = SentDm() response = client.messages.send( to=["+1234567890"], template={ "id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8", "parameters": { "customerName": "John Doe", "orderNumber": "#12345" } } ) print(f"Message ID: {response.data.recipients[0].message_id}") ``` ```go import ( "context" "github.com/sentdm/sent-dm-go" "github.com/sentdm/sent-dm-go/option" ) client := sentdm.NewClient() response, err := client.Messages.Send(context.Background(), sentdm.MessageSendParams{ To: []string{"+1234567890"}, Template: sentdm.MessageSendParamsTemplate{ ID: sentdm.String("7ba7b820-9dad-11d1-80b4-00c04fd430c8"), Parameters: map[string]interface{}{ "customerName": "John Doe", "orderNumber": "#12345", }, }, }) ``` ```java 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") .template(MessageSendParams.Template.builder() .id("7ba7b820-9dad-11d1-80b4-00c04fd430c8") .parameters(MessageSendParams.Template.Parameters.builder() .putAdditionalProperty("customerName", JsonValue.from("John Doe")) .putAdditionalProperty("orderNumber", JsonValue.from("#12345")) .build()) .build()) .build(); var response = client.messages().send(params); System.out.println("Sent: " + response.data().recipients().get(0).messageId()); ``` ```csharp using Sentdm; using Sentdm.Models.Messages; using System.Collections.Generic; SentDmClient client = new(); MessageSendParams parameters = new() { To = new List { "+1234567890" }, Template = new MessageSendParamsTemplate { Id = "7ba7b820-9dad-11d1-80b4-00c04fd430c8", Parameters = new Dictionary { { "customerName", "John Doe" }, { "orderNumber", "#12345" } } } }; var response = await client.Messages.Send(parameters); Console.WriteLine($"Sent: {response.Data.Recipients[0].MessageId}"); ``` ```php messages->send( to: ['+1234567890'], template: [ 'id' => '7ba7b820-9dad-11d1-80b4-00c04fd430c8', 'parameters' => [ 'customerName' => 'John Doe', 'orderNumber' => '#12345' ] ] ); var_dump($result->data->recipients[0]->message_id); ``` ```ruby require "sentdm" sent_dm = Sentdm::Client.new result = sent_dm.messages.send( to: ["+1234567890"], template: { id: "7ba7b820-9dad-11d1-80b4-00c04fd430c8", parameters: { customerName: "John Doe", orderNumber: "#12345" } } ) puts result.data.recipients[0].message_id ``` ### 2. Send to Multiple Recipients Send the same message to multiple recipients in one request: ```bash curl -X POST "https://api.sent.dm/v3/messages" \ -H "x-api-key: $SENT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": ["+1234567890", "+1987654321", "+1555555555"], "template": { "id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8", "parameters": { "announcement": "Our store is now open!" } } }' ``` ```typescript const response = await client.messages.send({ to: ['+1234567890', '+1987654321', '+1555555555'], template: { id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8', parameters: { announcement: 'Our store is now open!' } } }); console.log(`Sent to ${response.data.recipients.length} recipients`); ``` ```python response = client.messages.send( to=["+1234567890", "+1987654321", "+1555555555"], template={ "id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8", "parameters": { "announcement": "Our store is now open!" } } ) print(f"Sent to {len(response.data.recipients)} recipients") ``` ```go response, err := client.Messages.Send(context.Background(), sentdm.MessageSendParams{ To: []string{"+1234567890", "+1987654321", "+1555555555"}, Template: sentdm.MessageSendParamsTemplate{ ID: sentdm.String("7ba7b820-9dad-11d1-80b4-00c04fd430c8"), Parameters: map[string]interface{}{ "announcement": "Our store is now open!", }, }, }) fmt.Printf("Sent to %d recipients\n", len(response.Data.Recipients)) ``` ```java import dm.sent.models.messages.MessageSendParams; import dm.sent.core.JsonValue; MessageSendParams params = MessageSendParams.builder() .addTo("+1234567890") .addTo("+1987654321") .addTo("+1555555555") .template(MessageSendParams.Template.builder() .id("7ba7b820-9dad-11d1-80b4-00c04fd430c8") .parameters(MessageSendParams.Template.Parameters.builder() .putAdditionalProperty("announcement", JsonValue.from("Our store is now open!")) .build()) .build()) .build(); var response = client.messages().send(params); System.out.println("Sent to " + response.data().recipients().size() + " recipients"); ``` ```csharp using Sentdm.Models.Messages; MessageSendParams parameters = new() { To = new List { "+1234567890", "+1987654321", "+1555555555" }, Template = new MessageSendParamsTemplate { Id = "7ba7b820-9dad-11d1-80b4-00c04fd430c8", Parameters = new Dictionary { { "announcement", "Our store is now open!" } } } }; var response = await client.Messages.Send(parameters); Console.WriteLine($"Sent to {response.Data.Recipients.Count} recipients"); ``` ```php $result = $client->messages->send( to: ['+1234567890', '+1987654321', '+1555555555'], template: [ 'id' => '7ba7b820-9dad-11d1-80b4-00c04fd430c8', 'parameters' => [ 'announcement' => 'Our store is now open!' ] ] ); echo "Sent to " . count($result->data->recipients) . " recipients\n"; ``` ```ruby result = sent_dm.messages.send( to: ["+1234567890", "+1987654321", "+1555555555"], template: { id: "7ba7b820-9dad-11d1-80b4-00c04fd430c8", parameters: { announcement: "Our store is now open!" } } ) puts "Sent to #{result.data.recipients.length} recipients" ``` **Recipient Limit:** A single request can include up to **1000 recipients**. For campaigns larger than 1000, split into multiple requests. Each API call counts as one request toward the 200 req/min rate limit. ## Sending Free-Form Text (Without a Template) To send an ad-hoc message body without creating a template, pass `text` instead of `template`. Provide exactly one of the two: the API rejects a request that includes both, or neither, with a `400` validation error. ```bash curl -X POST "https://api.sent.dm/v3/messages" \ -H "x-api-key: $SENT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": ["+1234567890"], "text": "Thanks for reaching out! Your order shipped this morning." }' ``` ```typescript const response = await client.messages.send({ to: ['+1234567890'], text: 'Thanks for reaching out! Your order shipped this morning.' }); console.log(`Message ID: ${response.data.recipients[0].message_id}`); ``` ```python response = client.messages.send( to=["+1234567890"], text="Thanks for reaching out! Your order shipped this morning." ) print(f"Message ID: {response.data.recipients[0].message_id}") ``` ```go response, err := client.Messages.Send(context.Background(), sentdm.MessageSendParams{ To: []string{"+1234567890"}, Text: sentdm.String("Thanks for reaching out! Your order shipped this morning."), }) ``` ```java MessageSendParams params = MessageSendParams.builder() .addTo("+1234567890") .text("Thanks for reaching out! Your order shipped this morning.") .build(); var response = client.messages().send(params); ``` ```csharp MessageSendParams parameters = new() { To = new List { "+1234567890" }, Text = "Thanks for reaching out! Your order shipped this morning." }; var response = await client.Messages.Send(parameters); ``` ```php $result = $client->messages->send( to: ['+1234567890'], text: 'Thanks for reaching out! Your order shipped this morning.' ); var_dump($result->data->recipients[0]->message_id); ``` ```ruby result = sent_dm.messages.send( to: ["+1234567890"], text: "Thanks for reaching out! Your order shipped this morning." ) puts result.data.recipients[0].message_id ``` To pin a free-form send to one channel, set `channel` exactly as for template sends; see [Channel Selection Strategies](#channel-selection-strategies). On SMS, a free-form body follows the same segmentation rules as template output; see [SMS encoding and length](/start/concepts/sms-encoding-and-length). The response to a free-form send still includes a `template_id` and `template_name` (`FREE_TEXT_SYS_TEMPLATE`). Free-form messages ride Sent's internal free-text system template, and your text renders verbatim as the message body. ### When You Can Send Free-Form Text Free-form text is for replying inside an existing conversation. The first message to a contact who has never messaged you must use a template. Per channel: | Channel | Free-form text allowed | |---------|------------------------| | SMS | After the contact has sent at least one inbound message on any channel (SMS, RCS, or WhatsApp). No time limit. | | RCS | Same rule as SMS. | | WhatsApp | Only within 24 hours of the contact's latest inbound WhatsApp message (Meta's customer-service window). | | Auto-detect (`channel` omitted or `["sent"]`) | Same prior-inbound rule as SMS and RCS. Sent does not route a free-form send to WhatsApp unless the 24-hour window is open. | Standalone compliance keywords from the contact, such as STOP, START, or HELP, don't count as inbound messages for these rules. For how conversation windows open and reset, see [Two-Way Conversations](/start/guides/two-way-conversations). To start a conversation with a contact who has never replied, use a template; see [Working with Templates](/start/guides/working-with-templates). ### Free-Form Send Errors If the request contains both `text` and `template`, or neither, the API rejects it synchronously: ```json { "success": false, "status": 400, "error": { "code": "VALIDATION_001", "message": "Request validation failed", "details": { "template": ["Provide exactly one of 'template' or 'text'"] }, "doc_url": "https://docs.sent.dm/reference/api/error-catalog" } } ``` If the request is valid but the send doesn't meet the per-channel rules, the API still returns `202 Accepted`. The delivery pipeline enforces the gate, and the message fails asynchronously: - SMS, RCS, or auto-detect with no prior inbound from the contact: status `FAILED` with error code `VALIDATION_004` (`'template' is required`) - WhatsApp outside the 24-hour window: status `FAILED` with error code `WHATSAPP_TEMPLATE_REQUIRED` Check the outcome via `GET /messages/{id}` or webhooks; see [Status Tracking](/start/guides/message-status-tracking). ## Channel Selection Strategies ### Automatic Selection (Default) Let Sent choose the optimal channel by omitting the `channel` field: ```bash curl -X POST "https://api.sent.dm/v3/messages" \ -H "x-api-key: $SENT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": ["+1234567890"], "template": { "id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8" } }' ``` ```typescript const response = await client.messages.send({ to: ['+1234567890'], template: { id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8' } // No channel field - automatic selection }); ``` ```python response = client.messages.send( to=["+1234567890"], template={ "id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8" } # No channel parameter - automatic selection ) ``` ```go response, err := client.Messages.Send(context.Background(), sentdm.MessageSendParams{ To: []string{"+1234567890"}, Template: sentdm.MessageSendParamsTemplate{ ID: sentdm.String("7ba7b820-9dad-11d1-80b4-00c04fd430c8"), }, // No channel field - automatic selection }) ``` ```java MessageSendParams params = MessageSendParams.builder() .addTo("+1234567890") .template(MessageSendParams.Template.builder() .id("7ba7b820-9dad-11d1-80b4-00c04fd430c8") .build()) // No channel - automatic selection .build(); var response = client.messages().send(params); ``` ```csharp MessageSendParams parameters = new() { To = new List { "+1234567890" }, Template = new MessageSendParamsTemplate { Id = "7ba7b820-9dad-11d1-80b4-00c04fd430c8" } // No channel - automatic selection }; var response = await client.Messages.Send(parameters); ``` ```php $result = $client->messages->send( to: ['+1234567890'], template: [ 'id' => '7ba7b820-9dad-11d1-80b4-00c04fd430c8' ] // No channel - automatic selection ); ``` ```ruby result = sent_dm.messages.send( to: ["+1234567890"], template: { id: "7ba7b820-9dad-11d1-80b4-00c04fd430c8" } # No channel - automatic selection ) ``` Sent considers: - Contact's available channels - Historical delivery success rates - Cost optimization - Regional preferences Omitting the field is equivalent to `channel: ["sent"]`, the auto-detect value. Automatic selection is also the only mode with cross-channel fallback: when the preferred channel can't deliver, Sent can route the message over another one. ### Force Specific Channel Override automatic selection by specifying the `channel` field. An explicit channel **pins** each message to that channel and disables cross-channel fallback: if the pinned channel can't deliver to a recipient, the message fails rather than switching channels. The request shape is identical for every channel: only the `channel` value changes. This example pins the send to SMS: ```bash curl -X POST "https://api.sent.dm/v3/messages" \ -H "x-api-key: $SENT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": ["+1234567890"], "template": { "id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8" }, "channel": ["sms"] }' ``` ```typescript const response = await client.messages.send({ to: ['+1234567890'], template: { id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8' }, channel: ['sms'] }); ``` ```python response = client.messages.send( to=["+1234567890"], template={ "id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8" }, channel=["sms"] ) ``` ```go response, err := client.Messages.Send(context.Background(), sentdm.MessageSendParams{ To: []string{"+1234567890"}, Channel: []string{"sms"}, Template: sentdm.MessageSendParamsTemplate{ ID: sentdm.String("7ba7b820-9dad-11d1-80b4-00c04fd430c8"), }, }) ``` ```java MessageSendParams params = MessageSendParams.builder() .addTo("+1234567890") .addChannel("sms") .template(MessageSendParams.Template.builder() .id("7ba7b820-9dad-11d1-80b4-00c04fd430c8") .build()) .build(); var response = client.messages().send(params); ``` ```csharp MessageSendParams parameters = new() { To = new List { "+1234567890" }, Channel = new List { "sms" }, Template = new MessageSendParamsTemplate { Id = "7ba7b820-9dad-11d1-80b4-00c04fd430c8" } }; var response = await client.Messages.Send(parameters); ``` ```php $result = $client->messages->send( to: ['+1234567890'], template: [ 'id' => '7ba7b820-9dad-11d1-80b4-00c04fd430c8' ], channel: ['sms'] ); ``` ```ruby result = sent_dm.messages.send( to: ["+1234567890"], template: { id: "7ba7b820-9dad-11d1-80b4-00c04fd430c8" }, channel: ["sms"] ) ``` #### Channel Values | `channel` value | Behavior | |-----------------|----------| | Omitted or `["sent"]` | Automatic selection with cross-channel fallback ([the default](#automatic-selection-default)). | | `["sms"]` | Pins every message to SMS. | | `["whatsapp"]` | Pins every message to WhatsApp. Fails for contacts not reachable on WhatsApp. | | `["rcs"]` | Pins every message to RCS. Fails when the recipient's device or carrier doesn't support RCS. | | Multiple values, for example `["whatsapp", "sms", "rcs"]` | Broadcasts on every listed channel: a separate message per (recipient, channel) pair. | Any other value fails request validation with a `400` error. Refer to the [Channel Routing reference](/reference/channel-routing) for the full resolution and fallback rules. A pinned message never falls back to another channel: if the pinned channel can't deliver to a recipient, that message fails. To reach each recipient over the best available channel, omit the `channel` field ([automatic selection](#automatic-selection-default)). #### Broadcasting to Multiple Channels The `channel` array is a **broadcast list, not a fallback order**. When you specify multiple channels, Sent creates a separate message for each (recipient, channel) pair and dispatches them all. Each message gets its own `message_id` and is billed independently: `["whatsapp", "sms", "rcs"]` sends every recipient three messages. ## Response Overview ### Success Response (202 Accepted) ```json { "success": true, "data": { "status": "QUEUED", "template_id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8", "template_name": "order_confirmation", "recipients": [ { "message_id": "8ba7b830-9dad-11d1-80b4-00c04fd430c8", "to": "+14155551234", "channel": "sms" } ] }, "error": null, "meta": { "request_id": "req_7X9zKp2jDw", "timestamp": "2026-03-04T11:28:25.2096416+00:00", "version": "v3" } } ``` **Important:** Store the `message_id` from each recipient object. You'll need it to track delivery status. ### Error Response (4xx/5xx) ```json { "success": false, "status": 402, "error": { "code": "BUSINESS_003", "message": "Account balance is insufficient to send this message", "doc_url": "https://docs.sent.dm/reference/api/error-catalog" }, "meta": { "request_id": "req_7X9zKp2jDw", "timestamp": "2026-03-04T11:28:25.2096416+00:00", "version": "v3" } } ``` Common error codes: - `RESOURCE_002` - Template not found or not approved - `BUSINESS_003` - Insufficient account balance - `BUSINESS_002` - Rate limit exceeded (200 req/min standard, 10 req/min sensitive) - `VALIDATION_001` - Request validation failed (for example, both or neither of `text` and `template` supplied) - `VALIDATION_002` - Invalid phone number format - `BUSINESS_005` - Template not approved for sending - `BUSINESS_007` - Channel not available for this contact - `BUSINESS_008` - Operation would exceed quota - `WHATSAPP_TEMPLATE_REQUIRED` - Free-form WhatsApp message outside the 24-hour window ## Next Steps ---