Create a recurring schedule

POST /v1/schedules

Sends one message to one number on a repeating wall-clock time in an IANA time zone, correct across daylight saving. Each occurrence goes through the same checks as POST /messages at the moment it fires (opt-out, sender registration, quota, spend limit, content screening) and is a normal message with schedule_id. A local time that does not exist (spring forward) runs after the gap that day; one that occurs twice (fall back) runs once, the first time. An occurrence reached more than 1 hour late is skipped (skipped_missed), never sent late. Limits: 25 active and 100 total schedules per account, at most one message per local day per schedule. Supports the Idempotency-Key header. Scope: messages:send.

Send your API key as a bearer token: Authorization: Bearer ssms_sk_.... Test keys run this endpoint against the sandbox; see Authentication for key modes and scopes.

Headers

NameTypeRequiredDescription
Idempotency-KeystringNoMakes retries safe for 24 hours: the same key and body replays the original successful response (with Idempotent-Replayed: true); a different body returns 409. Failed requests release the key. At most 255 characters.

Request body

JSON (Content-Type: application/json).

A ScheduleInput object.

FieldTypeRequiredDescription
tostringYesExample: +14155550132.
fromstringNoOne of your numbers. Optional when the account has exactly one number for the key's mode.
bodystringYesAt most 1600 characters.
repeatScheduleRepeatYes
start_datestring (date)NoFirst local date it may run. Defaults to today. Nullable.
end_datestring (date)NoLast local date it may run (inclusive). Nullable.
max_occurrencesintegerNoThe schedule completes after this many messages have been sent. Minimum 1. Nullable.
categorymarketing, transactional, otpNoOptional. What this message is, for quiet hours. Only marketing messages are held or refused outside 8:00am-9:00pm in the recipient's local time. Omitted: a message from a number registered for the Marketing use case is marketing, everything else is transactional. Nullable.
quiet_hoursblock, deferNoOptional. What to do with a marketing message in the recipient's quiet hours: block refuses it (403 quiet_hours), defer holds it until their next 8:00am. Omitted: an occurrence in quiet hours is held until the recipient's 8:00am and recorded as deferred. Nullable.

Responses

StatusMeaningBody
201CreatedSchedule
400Invalid request, or a limit reachedError
401Missing, malformed, or revoked API keyError
402Billable live calls only: the prepaid credit balance cannot cover the call (insufficient_credits); nothing was done or chargedError
409Idempotency conflictError
429Rate limit or quota exceededError

201: Schedule fields

FieldTypeDescription
idstringExample: sch_a1B2c3D4e5F6g7H8.
objectschedule
tostring
fromstring
bodystring
repeatScheduleRepeat
start_datestring (date)Nullable.
end_datestring (date)Nullable.
max_occurrencesintegerNullable.
categorymarketing, transactional, otp, nullNullable.
quiet_hoursblock, defer, nullNullable.
activeboolean
statusactive, paused, completed
paused_reasonmanual, recipient_opted_out, repeated_failures, sender_unavailable, nullNullable.
next_run_atstring (date-time)The next occurrence (UTC). Null unless active. Nullable.
last_run_atstring (date-time)Nullable.
last_statussent, skipped_opted_out, skipped_missed, deferred, failed, nullNullable.
last_errorstringWhy the last occurrence was skipped or failed. Nullable.
last_message_idstringNullable.
countsobject
counts.sentinteger
counts.skippedinteger
counts.failedinteger
testbooleanMade with a test key: only ever sends in the sandbox.
created_atstring (date-time)
updated_atstring (date-time)
recent_occurrencesarray of objectOnly on GET /schedules/{id}: the last 10 occurrences, newest first.
recent_occurrences[].datestring (date)The local calendar date of the occurrence.
recent_occurrences[].due_atstring (date-time)Nullable.
recent_occurrences[].statussent, skipped_opted_out, skipped_missed, deferred, failed
recent_occurrences[].message_idstringNullable.
recent_occurrences[].errorstringNullable.

Errors

StatusWhen
400Invalid request, or a limit reached
401Missing, malformed, or revoked API key
402Billable live calls only: the prepaid credit balance cannot cover the call (insufficient_credits); nothing was done or charged
409Idempotency conflict
429Rate limit or quota exceeded

Every error has the same JSON shape, and request_id matches the X-Request-Id response header. Errors lists every code and what to do about it.

json
{
  "error": {
    "code": "invalid_request",
    "message": "What went wrong, in plain words.",
    "param": "the_field",
    "request_id": "req_a1B2c3D4e5F6g7H8"
  }
}

Idempotency

Send an Idempotency-Key header to make retries safe. For 24 hours the same key with the same body replays the original successful response (with Idempotent-Replayed: true); the same key with a different body returns 409. A failed request releases its key.

Examples

curl

bash
curl -X POST "https://api.joinsimplesms.com/v1/schedules" \
  -H "Authorization: Bearer $SIMPLESMS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2f0e-order-1042" \
  -d '{
  "to": "+14155550132",
  "body": "Hello from SimpleSMS",
  "repeat": {
    "frequency": "daily",
    "time": "08:00",
    "timezone": "America/New_York"
  }
}'

Node.js

The Node.js SDK does not wrap this endpoint yet; call it with fetch.

javascript
const res = await fetch('https://api.joinsimplesms.com/v1/schedules', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SIMPLESMS_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "to": "+14155550132",
    "body": "Hello from SimpleSMS",
    "repeat": {
      "frequency": "daily",
      "time": "08:00",
      "timezone": "America/New_York"
    }
  }),
});

if (!res.ok) throw new Error((await res.json()).error.message);
const data = await res.json();

Python

The Python SDK does not wrap this endpoint yet; call it over HTTP.

python
import json, os, urllib.request

req = urllib.request.Request(
    "https://api.joinsimplesms.com/v1/schedules",
    method="POST",
    headers={
        "Authorization": f"Bearer {os.environ['SIMPLESMS_API_KEY']}",
        "Content-Type": "application/json",
    },
    data=json.dumps({
        "to": "+14155550132",
        "body": "Hello from SimpleSMS",
        "repeat": {
            "frequency": "daily",
            "time": "08:00",
            "timezone": "America/New_York"
        }
    }).encode(),
)
with urllib.request.urlopen(req) as res:
    data = json.load(res)