StartImplementation Guides

Scheduled Messages

Schedule a message to deliver at a specific future time by including scheduled_at in the send request. The API accepts the request immediately (returning QUEUED), then holds each message as SCHEDULED until the release instant, at which point it routes and dispatches like any other message.

How It Works

The send endpoint returns 202 Accepted with status: "QUEUED". Each message then moves to SCHEDULED once the consumer parks it. A message.scheduled webhook fires at that point. The release sweeper checks every 30 seconds and dispatches any message whose scheduled_at has passed.

Schedule a Message

Add scheduled_at to any send request. Everything else (template, recipients, channel) works the same as an immediate send:

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": "Jane",
        "appointmentTime": "2pm tomorrow"
      }
    },
    "scheduled_at": "2026-09-24T14:00:00-05:00"
  }'
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: 'Jane',
      appointmentTime: '2pm tomorrow'
    }
  },
  scheduled_at: '2026-09-24T14:00:00-05:00'
});

console.log(`Message ID: ${response.data.recipients[0].message_id}`);
console.log(`Releases at: ${response.data.scheduled_at}`);
from sent_dm import SentDm

client = SentDm()

response = client.messages.send(
    to=["+1234567890"],
    template={
        "id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
        "parameters": {
            "customerName": "Jane",
            "appointmentTime": "2pm tomorrow"
        }
    },
    scheduled_at="2026-09-24T14:00:00-05:00"
)

print(f"Message ID: {response.data.recipients[0].message_id}")
print(f"Releases at: {response.data.scheduled_at}")
import (
    "context"
    "github.com/sentdm/sent-dm-go"
)

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":    "Jane",
            "appointmentTime": "2pm tomorrow",
        },
    },
    ScheduledAt: sentdm.String("2026-09-24T14:00:00-05:00"),
})
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("Jane"))
            .putAdditionalProperty("appointmentTime", JsonValue.from("2pm tomorrow"))
            .build())
        .build())
    .scheduledAt("2026-09-24T14:00:00-05:00")
    .build();

var response = client.messages().send(params);
System.out.println("Releases at: " + response.data().scheduledAt());
using Sentdm;
using Sentdm.Models.Messages;

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", "Jane" },
            { "appointmentTime", "2pm tomorrow" }
        }
    },
    ScheduledAt = "2026-09-24T14:00:00-05:00"
};

var response = await client.Messages.Send(parameters);
Console.WriteLine($"Releases at: {response.Data.ScheduledAt}");
<?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' => 'Jane',
            'appointmentTime' => '2pm tomorrow'
        ]
    ],
    scheduled_at: '2026-09-24T14:00:00-05:00'
);

echo "Releases at: " . $result->data->scheduled_at . "\n";
require "sentdm"

sent_dm = Sentdm::Client.new

result = sent_dm.messages.send_(
  to: ["+1234567890"],
  template: {
    id: "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
    parameters: {
      customerName: "Jane",
      appointmentTime: "2pm tomorrow"
    }
  },
  scheduled_at: "2026-09-24T14:00:00-05:00"
)

puts "Releases at: #{result.data.scheduled_at}"

scheduled_at Rules

RuleDetail
Explicit UTC offset required2026-10-01T09:00:00+02:00 or 2026-10-01T07:00:00Z. A value with no offset is rejected with 400.
Minimum 1 minute aheadLess than 1 minute from now is rejected with 400.
Maximum 30 days aheadMore than 30 days from now is rejected with 400.
Stored and echoed in UTCThe offset you send is used only to fix the instant. scheduled_at on the response and GET /v3/messages/{id} is always UTC.

Response

A scheduled send returns 202 Accepted with a ScheduledSendMessageResponse. The top-level status is QUEUED, the same as an immediate send. The extra scheduled_at field on the response is the release instant in UTC:

{
  "success": true,
  "data": {
    "status": "QUEUED",
    "scheduled_at": "2026-09-24T19:00:00Z",
    "template_id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
    "template_name": "appointment_reminder",
    "recipients": [
      {
        "message_id": "9ca8b840-9dad-11d1-80b4-00c04fd430c9",
        "to": "+1234567890",
        "channel": null
      }
    ]
  },
  "error": null,
  "meta": {
    "request_id": "req_8Y0aLq3kEx",
    "timestamp": "2026-09-23T10:00:00Z",
    "version": "v3"
  }
}

The recipient's channel is null at this point: channel selection happens at release, once the message exits SCHEDULED.

Each recipient's message moves from QUEUED to SCHEDULED once the consumer parks it. You see this via GET /v3/messages/{id} or the message.scheduled webhook.

Retrieve a Scheduled Message

GET /v3/messages/{id} returns a ScheduledMessageResponse for any message that is or was held: the same fields as a regular message response, plus scheduled_at:

curl "https://api.sent.dm/v3/messages/9ca8b840-9dad-11d1-80b4-00c04fd430c9" \
  -H "x-api-key: $SENT_API_KEY"
const message = await client.messages.get('9ca8b840-9dad-11d1-80b4-00c04fd430c9');

if (message.data.status === 'SCHEDULED') {
  console.log(`Waiting until: ${message.data.scheduled_at}`);
}
message = client.messages.get("9ca8b840-9dad-11d1-80b4-00c04fd430c9")

if message.data.status == "SCHEDULED":
    print(f"Waiting until: {message.data.scheduled_at}")
message, err := client.Messages.Get(context.Background(),
    "9ca8b840-9dad-11d1-80b4-00c04fd430c9")

if message.Data.Status == "SCHEDULED" {
    fmt.Printf("Waiting until: %s\n", message.Data.ScheduledAt)
}
import dm.sent.client.SentDmClient;
import dm.sent.client.okhttp.SentDmOkHttpClient;

SentDmClient client = SentDmOkHttpClient.fromEnv();

var message = client.messages().retrieve("9ca8b840-9dad-11d1-80b4-00c04fd430c9");

if ("SCHEDULED".equals(message.data().status())) {
    System.out.println("Waiting until: " + message.data().scheduledAt());
}
using Sentdm;

SentDmClient client = new();

var message = await client.Messages.Retrieve("9ca8b840-9dad-11d1-80b4-00c04fd430c9");

if (message.Data.Status == "SCHEDULED")
{
    Console.WriteLine($"Waiting until: {message.Data.ScheduledAt}");
}
<?php
require_once 'vendor/autoload.php';

use SentDM\Client;

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

$message = $client->messages->retrieve('9ca8b840-9dad-11d1-80b4-00c04fd430c9');

if ($message->data->status === 'SCHEDULED') {
    echo "Waiting until: " . $message->data->scheduled_at . "\n";
}
require "sentdm"

sent_dm = Sentdm::Client.new

message = sent_dm.messages.retrieve("9ca8b840-9dad-11d1-80b4-00c04fd430c9")

if message.data.status == "SCHEDULED"
  puts "Waiting until: #{message.data.scheduled_at}"
end

Webhooks for Scheduled Messages

EventWhen it fires
message.scheduledEach time the message is parked or re-deferred: fires when the message is first held as SCHEDULED, and again each time a quiet-hours window shifts the release time (with the updated scheduled_at)
message.sentAfter release, when the message is dispatched to the provider
message.deliveredWhen the provider confirms delivery to the device
message.failedIf the send fails after release

The activity log from GET /v3/messages/{id}/activities includes a SCHEDULED entry that carries scheduled_at. A message re-deferred by quiet hours has two SCHEDULED entries, each with the instant as it stood at that moment.

Things to Know

Evaluation happens at release, not at the API call. Account credit balance, template approval, and quiet hours are checked when the message is released, not when you schedule it. A message scheduled for a time inside a recipient's legally protected quiet-hours window is moved to the next allowed time at release, and message.scheduled fires again with the new scheduled_at.

Template parameters are captured at send time. The template body and parameters you supply are frozen when the API call is made. If you update the template's content and resubmit it for review before release, the channel moves to PENDING and the message is blocked at release. If you update the template without resubmitting, the message sends with the content from the time of the API call.

Outstanding scheduled messages are capped at 1,000,000 per account. If accepting the request would push your account past that limit, the API returns 429 with error code LIMIT_001. Wait for scheduled messages to send, or schedule fewer at once.

Scheduled messages use the same POST /v3/messages endpoint as immediate sends. The only addition is scheduled_at. All other options — channel selection, sender profile scoping, multiple recipients — work identically.

Next Steps

On this page