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:

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"
      }
    }
  }'
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}`);
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}")
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",
        },
    },
})
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());
using Sentdm;
using Sentdm.Models.Messages;
using System.Collections.Generic;

SentDmClient client = new();

MessageSendParams parameters = new()
{
    To = new List<string> { "+1234567890" },
    Template = new MessageSendParamsTemplate
    {
        Id = "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
        Parameters = new Dictionary<string, string>
        {
            { "customerName", "John Doe" },
            { "orderNumber", "#12345" }
        }
    }
};

var response = await client.Messages.Send(parameters);
Console.WriteLine($"Sent: {response.Data.Recipients[0].MessageId}");
<?php
require_once 'vendor/autoload.php';

use SentDM\Client;

$client = new Client($_ENV['SENT_DM_API_KEY']);

$result = $client->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);
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:

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!"
      }
    }
  }'
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`);
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")
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))
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");
using Sentdm.Models.Messages;

MessageSendParams parameters = new()
{
    To = new List<string> { "+1234567890", "+1987654321", "+1555555555" },
    Template = new MessageSendParamsTemplate
    {
        Id = "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
        Parameters = new Dictionary<string, string>
        {
            { "announcement", "Our store is now open!" }
        }
    }
};

var response = await client.Messages.Send(parameters);
Console.WriteLine($"Sent to {response.Data.Recipients.Count} recipients");
$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";
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.

3. Send on Behalf of a Sender Profile

If your account is an organization with child Sender Profiles, an organization API key can send as any of its profiles by adding the x-profile-id header (the profile UUID). The request then executes as that profile, so its templates, contacts, sender numbers, and compliance settings apply, and the response echoes the scope in an X-Profile-Id header.

curl -X POST "https://api.sent.dm/v3/messages" \
  -H "x-api-key: $SENT_ORGANIZATION_API_KEY" \
  -H "x-profile-id: $PROFILE_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "to": ["+1234567890"],
    "template": {
      "id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8"
    }
  }'
const response = await client.messages.send({
  'x-profile-id': profileId,
  to: ['+1234567890'],
  template: { id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8' }
});
response = client.messages.send(
    x_profile_id=profile_id,
    to=["+1234567890"],
    template={"id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8"}
)
response, err := client.Messages.Send(context.Background(), sentdm.MessageSendParams{
    XProfileID: sentdm.String(profileID),
    To:         []string{"+1234567890"},
    Template: sentdm.MessageSendParamsTemplate{
        ID: sentdm.String("7ba7b820-9dad-11d1-80b4-00c04fd430c8"),
    },
})
MessageSendParams params = MessageSendParams.builder()
    .xProfileId(profileId)
    .addTo("+1234567890")
    .template(MessageSendParams.Template.builder()
        .id("7ba7b820-9dad-11d1-80b4-00c04fd430c8")
        .build())
    .build();

var response = client.messages().send(params);
MessageSendParams parameters = new()
{
    XProfileID = profileId,
    To = new List<string> { "+1234567890" },
    Template = new Template { ID = "7ba7b820-9dad-11d1-80b4-00c04fd430c8" }
};

var response = await client.Messages.Send(parameters);
$result = $client->messages->send(
    to: ['+1234567890'],
    template: ['id' => '7ba7b820-9dad-11d1-80b4-00c04fd430c8'],
    xProfileID: $profileId
);
result = sent_dm.messages.send_(
  x_profile_id: profile_id,
  to: ["+1234567890"],
  template: { id: "7ba7b820-9dad-11d1-80b4-00c04fd430c8" }
)

Only organization API keys may send x-profile-id. A profile-scoped key is rejected with 403 (AUTH_004), a value that is not a UUID with 400 (VALIDATION_003), and a profile outside your organization with 404 (RESOURCE_013). Requests scoped this way draw from the organization's rate limit pool.

The alternative is to authenticate with the profile's own API key, which needs no header. Both patterns, plus per-language customer-to-profile mapping, are in Integrating Sender Profiles into Your Application; the provisioning flow is Create and activate Sender Profiles via the API.

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.

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."
  }'
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}`);
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}")
response, err := client.Messages.Send(context.Background(), sentdm.MessageSendParams{
    To:   []string{"+1234567890"},
    Text: sentdm.String("Thanks for reaching out! Your order shipped this morning."),
})
MessageSendParams params = MessageSendParams.builder()
    .addTo("+1234567890")
    .text("Thanks for reaching out! Your order shipped this morning.")
    .build();

var response = client.messages().send(params);
MessageSendParams parameters = new()
{
    To = new List<string> { "+1234567890" },
    Text = "Thanks for reaching out! Your order shipped this morning."
};

var response = await client.Messages.Send(parameters);
$result = $client->messages->send(
    to: ['+1234567890'],
    text: 'Thanks for reaching out! Your order shipped this morning.'
);

var_dump($result->data->recipients[0]->message_id);
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. On SMS, a free-form body follows the same segmentation rules as template output; see 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:

ChannelFree-form text allowed
SMSAfter the contact has sent at least one inbound message on any channel (SMS, RCS, or WhatsApp). No time limit.
RCSSame rule as SMS.
WhatsAppOnly 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. To start a conversation with a contact who has never replied, use a template; see Working with Templates.

Free-Form Send Errors

If the request contains both text and template, or neither, the API rejects it synchronously:

{
  "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.

Channel Selection Strategies

Automatic Selection (Default)

Let Sent choose the optimal channel by omitting the channel field:

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"
    }
  }'
const response = await client.messages.send({
  to: ['+1234567890'],
  template: {
    id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8'
  }
  // No channel field - automatic selection
});
response = client.messages.send(
    to=["+1234567890"],
    template={
        "id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8"
    }
    # No channel parameter - automatic selection
)
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
})
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);
MessageSendParams parameters = new()
{
    To = new List<string> { "+1234567890" },
    Template = new MessageSendParamsTemplate
    {
        Id = "7ba7b820-9dad-11d1-80b4-00c04fd430c8"
    }
    // No channel - automatic selection
};

var response = await client.Messages.Send(parameters);
$result = $client->messages->send(
    to: ['+1234567890'],
    template: [
        'id' => '7ba7b820-9dad-11d1-80b4-00c04fd430c8'
    ]
    // No channel - automatic selection
);
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:

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"]
  }'
const response = await client.messages.send({
  to: ['+1234567890'],
  template: {
    id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8'
  },
  channel: ['sms']
});
response = client.messages.send(
    to=["+1234567890"],
    template={
        "id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8"
    },
    channel=["sms"]
)
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"),
    },
})
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);
MessageSendParams parameters = new()
{
    To = new List<string> { "+1234567890" },
    Channel = new List<string> { "sms" },
    Template = new MessageSendParamsTemplate
    {
        Id = "7ba7b820-9dad-11d1-80b4-00c04fd430c8"
    }
};

var response = await client.Messages.Send(parameters);
$result = $client->messages->send(
    to: ['+1234567890'],
    template: [
        'id' => '7ba7b820-9dad-11d1-80b4-00c04fd430c8'
    ],
    channel: ['sms']
);
result = sent_dm.messages.send_(
  to: ["+1234567890"],
  template: {
    id: "7ba7b820-9dad-11d1-80b4-00c04fd430c8"
  },
  channel: ["sms"]
)

Channel Values

channel valueBehavior
Omitted or ["sent"]Automatic selection with cross-channel fallback (the 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 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).

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)

{
  "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)

{
  "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


On this page