Recurring schedules

A schedule sends one message to one number on a repeating wall-clock time: every morning at 8:00, every Monday and Friday at 9:00, the 1st of each month. You create it once; SimpleSMS sends each occurrence.

bash
curl -X POST https://api.joinsimplesms.com/v1/schedules \
  -H "Authorization: Bearer $SIMPLESMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155550132",
    "body": "Good morning. You have got this.",
    "repeat": { "frequency": "daily", "time": "08:00", "timezone": "America/New_York" }
  }'
ParameterTypeNotes
tostringDestination number (E.164, US/Canada).
fromstringA number you own. Optional when the account has one number for the key's mode, like POST /v1/messages.
bodystringUp to 1600 characters, the same as a normal send.
repeat.frequencystringdaily, weekly or monthly.
repeat.timestring24-hour local time, "HH:MM".
repeat.timezonestringIANA time zone, e.g. America/New_York.
repeat.days_of_weekstring[]Weekly only, required: one or more of sun, mon, tue, wed, thu, fri, sat.
repeat.day_of_monthintegerMonthly only, required: 1 to 31. In a shorter month it runs on the last day.
start_datestringOptional. First local date it may run, YYYY-MM-DD. Defaults to today.
end_datestringOptional. Last local date it may run (inclusive).
max_occurrencesintegerOptional. The schedule completes after this many messages have been sent.

Returns 201 with a Schedule object. Pass an Idempotency-Key header to make a retried create safe.

The Schedule object

json
{
  "id": "sch_a1B2c3D4e5F6g7H8",
  "object": "schedule",
  "to": "+14155550132",
  "from": "+15005550100",
  "body": "Good morning. You have got this.",
  "repeat": { "frequency": "daily", "time": "08:00", "timezone": "America/New_York" },
  "start_date": null,
  "end_date": null,
  "max_occurrences": null,
  "active": true,
  "status": "active",
  "paused_reason": null,
  "next_run_at": "2026-10-06T12:00:00.000Z",
  "last_run_at": "2026-10-05T12:00:00.000Z",
  "last_status": "sent",
  "last_error": null,
  "last_message_id": "msg_a1B2c3D4e5F6g7H8",
  "counts": { "sent": 1, "skipped": 0, "failed": 0 },
  "test": true,
  "created_at": "2026-10-05T11:42:10.000Z",
  "updated_at": "2026-10-05T12:00:02.000Z"
}
FieldNotes
statusactive, paused or completed (end_date passed or max_occurrences reached). active is the same thing as a boolean.
paused_reasonmanual (you paused it), recipient_opted_out, repeated_failures or sender_unavailable. null unless paused.
next_run_atThe next occurrence, in UTC. null unless active.
last_statussent, skipped_opted_out, skipped_missed or failed. last_error says why for the last three.
countsMessages sent, occurrences skipped (opted out or missed), occurrences that failed.
testtrue for a schedule made with a test key. It only ever sends in the sandbox.
recent_occurrencesOnly on GET /v1/schedules/{id}: the last 10 occurrences with date, due_at, status, message_id and error.

Every occurrence is a normal send

When an occurrence is due, the message goes through exactly what a request to POST /v1/messages goes through at that moment: the opt-out list, sender registration, your quota and spend limit, content screening. Nothing is reserved in advance, and a refused occurrence costs nothing.

Each occurrence is an ordinary message with schedule_id set. It appears in GET /v1/messages (filter with ?schedule_id=sch_...), in the console, and fires the usual message.sent and message.delivered events.

Time zones and daylight saving

time is a wall-clock time in timezone. 08:00 in New York stays 08:00 in New York all year; the UTC time in next_run_at moves by an hour when the clocks change.

  • A time that does not exist (clocks jump from 02:00 to 03:00, and the schedule says 02:30): that day it runs the same distance past the jump, at 03:30. It is not skipped.
  • A time that happens twice (clocks fall back and 01:30 occurs twice): it runs once, the first time.

A schedule sends at most one message per local calendar day, whatever you edit.

What happens when

  • The recipient has opted out. The occurrence is skipped and recorded (last_status: "skipped_opted_out"), and nothing is sent. After 3 skipped occurrences in a row the schedule pauses itself with paused_reason: "recipient_opted_out". If they opt back in, resume it.
  • An occurrence fails (quota reached, sender not registered, spend limit, not enough credits, carrier error): it is recorded as failed with the error code and the schedule carries on. Credits are drawn when an occurrence fires, not when the schedule is created; one the balance cannot cover fails with insufficient_credits and is not charged. After 5 failures in a row it pauses itself (repeated_failures). If the sending number was released it pauses at once (sender_unavailable).
  • We miss an occurrence. If our scheduler reaches an occurrence more than 1 hour late, the message is not sent late; it is recorded as skipped_missed. After a longer outage only the most recent occurrence is considered, and only if it is less than 1 hour old. You never get a burst of catch-up messages.
  • Exactly once. Each occurrence is claimed before it is sent, so it cannot be sent twice.

Two webhook events report this: schedule.occurrence_skipped (schedule_id, to, occurrence_date, due_at, reason) and schedule.paused (schedule_id, to, reason).

Pause, resume, change, delete

bash
# Pause
curl -X PATCH https://api.joinsimplesms.com/v1/schedules/sch_a1B2c3D4e5F6g7H8 \
  -H "Authorization: Bearer $SIMPLESMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "active": false }'
  • PATCH /v1/schedules/{id} with { "active": false } pauses and { "active": true } resumes. Resuming starts from now: nothing that came due while paused is sent.
  • The same call changes to, from, body, repeat, start_date, end_date or max_occurrences. Send the whole repeat object.
  • GET /v1/schedules lists them, GET /v1/schedules/{id} returns one.
  • DELETE /v1/schedules/{id} stops it for good. Messages it already sent stay in your message log.

In the console, schedules are on the Scheduled page with their next run and Pause, Resume and Delete.

With the SDKs

javascript
const schedule = await sms.schedules.create({
  to: '+14155550132',
  body: 'Good morning. You have got this.',
  repeat: { frequency: 'daily', time: '08:00', timezone: 'America/New_York' },
});
await sms.schedules.pause(schedule.id);
await sms.messages.list({ scheduleId: schedule.id }); // what it has sent
python
schedule = client.schedules.create(
    to="+14155550132",
    body="Good morning. You have got this.",
    repeat={"frequency": "daily", "time": "08:00", "timezone": "America/New_York"},
)
client.schedules.pause(schedule["id"])

Limits

  • 25 active schedules per account, 100 stored in total.
  • At most one message per local day per schedule (the shortest frequency is daily).
  • body up to 1600 characters.
  • Key scopes: messages:send to create, change and delete; messages:read to list and retrieve.

What is stored

A schedule stores the recipient's number and the message text until you delete it, so it can send the next occurrence. A record of each occurrence (date, outcome, message id; never the text) is kept for the last 90 occurrences. Deleting the schedule deletes both. The messages it sent are ordinary messages and follow normal message retention.