Automations

Your app says what happened. SimpleSMS decides what to text and when.

js
await sms.events.track({ user_id: "123", phone: "+14155550132", event: "trial_started" });

A flow starts on one event and runs a list of steps for that person:

trial_started -> Wait 1 hour -> Send SMS -> Wait until subscription_created arrives: end not in 3 days: continue -> Send SMS (reminder)

Build flows in Console → Automations or with the API below. Three templates ship in the console: a welcome series, this trial reminder, and an abandoned-onboarding nudge.

Track an event

bash
curl -X POST https://api.joinsimplesms.com/v1/track \
  -H "Authorization: Bearer $SIMPLESMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"event":"trial_started","user_id":"123","phone":"+14155550132","properties":{"plan":"pro"}}'
Field
eventRequired. Letters, digits, and _ . : -, up to 64 characters.
phoneThe person's number in E.164.
user_idYour own id for the person, up to 128 characters.
propertiesUp to 20 values (text, number, true/false). Usable as merge fields and in conditions.

Pass phone, user_id, or both. A call with both links the two, so later events need only user_id. An event for a user_id that has never been linked is stored, starts nothing, and answers with "phone": null.

The response lists what the event did:

json
{
  "id": "tev_...",
  "object": "tracked_event",
  "event": "trial_started",
  "user_id": "123",
  "phone": "+14155550132",
  "test": false,
  "runs_started": ["run_..."],
  "runs_notified": 0
}

Idempotency-Key works as on POST /v1/messages. The limit is 100 calls per 10 seconds per key. GET /v1/events is a different thing: the log of webhook events SimpleSMS sends you.

Steps

StepFields
send_smsbody, optional from (defaults to the flow's number)
waitseconds (1 minute to 30 days)
wait_for_eventevent, timeout_seconds, on_received, on_timeout
conditioncheck, on_met, on_not_met
endnone

A branch (on_received, on_timeout, on_met, on_not_met) is "continue", "end", or "goto:<step id>". A goto may only point at a later step, so a flow can never loop and a run sends at most one message per send step.

A check is one of:

  • { "kind": "event_received", "event": "onboarding_completed" }: true if the person sent that event since the run started.
  • { "kind": "property", "property": "plan", "op": "eq", "value": "pro" }: a property of the event that started the run.
  • { "kind": "contact_field", "field": "company", "op": "exists" }: the contact with that number (name, tag, or a custom field).

Operators: eq, neq, contains, gt, lt, exists, not_exists.

wait_for_event counts the event if it arrived any time since the run started, not only after the step began: someone who subscribed during an earlier wait has subscribed.

Message bodies take merge fields: every event property by name ({{plan}}), plus {{first_name}}, {{name}}, {{phone}} and {{field:company}} from the contact. A field with no value sends as nothing; a message that merges to nothing is skipped.

Create and activate

bash
curl -X POST https://api.joinsimplesms.com/v1/automations \
  -H "Authorization: Bearer $SIMPLESMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Trial reminder",
    "from": "+15005550100",
    "trigger": { "event": "trial_started" },
    "steps": [
      { "type": "wait", "seconds": 3600 },
      { "type": "send_sms", "body": "Your trial is live, {{first_name}}. Reply STOP to opt out." },
      { "type": "wait_for_event", "event": "subscription_created",
        "timeout_seconds": 259200, "on_received": "end", "on_timeout": "continue" },
      { "type": "send_sms", "body": "Your trial ends soon. Pick a plan to keep your account." }
    ]
  }'

A flow is created as a draft. The response carries issues: everything that stops it from running, each naming its step. POST /v1/automations/{id}/activate turns it on (and answers 400 with the same issues if it is not ready); POST /v1/automations/{id}/pause stops new runs and holds the ones in progress where they are. Activating again resumes them within 5 minutes.

A trigger can filter on properties: "trigger": { "event": "trial_started", "filters": [{ "property": "plan", "op": "eq", "value": "pro" }] }.

Endpoint
GET /v1/automationsAll flows, with run counts
POST /v1/automationsCreate a draft
GET / PATCH / DELETE /v1/automations/{id}Read, edit, delete
POST /v1/automations/{id}/activate · /pauseTurn on, hold
GET /v1/automations/{id}/runsRuns, newest first
GET / DELETE /v1/automations/{id}/runs/{run_id}One run with its timeline; cancel it

All of it needs the automations scope on a restricted key.

Sandbox and live

The flow's from number decides. A flow sending from a sandbox number listens only to events tracked with a test key, and behaves like any sandbox send: magic numbers work, nothing is billed, and only your own verified phone gets a real text. A flow sending from a live number listens only to live-key events. So the same event name can drive a sandbox copy and a live copy of a flow without either seeing the other's traffic.

What a run guarantees

  • One run per person per flow. A repeat of the trigger while a run is in progress is ignored (counted as triggers_skipped). Once the run ends, the next trigger starts a new one.
  • No duplicate texts. Each send step is attempted at most once. If our worker fails mid-send, the step is recorded as send_unconfirmed and not retried.
  • Edits never change a run in flight. Saving an active flow publishes a new version; runs finish on the version they started with.
  • Opt-outs win. Every message goes through the same checks as POST /v1/messages: the opt-out list, content screening, quotas and your spend limit. A run whose recipient has opted out ends (opted_out) the next time it wakes, before anything is sent. Set topic_id on a flow to also skip people unsubscribed from that topic.
  • A failed send does not stop the run. It is recorded on the run's timeline with the error code and the run continues. Transient carrier failures are retried by the normal message retry path.
  • Waits are checked every minute. A step due at 14:00:20 runs by 14:01. A first step that sends goes out with the track call itself.

Runs

json
{
  "id": "run_...",
  "object": "automation_run",
  "automation_id": "auto_...",
  "status": "waiting",
  "to": "+14155550132",
  "step": 2,
  "waiting_for_event": "subscription_created",
  "waiting_until": "2026-10-07T15:00:00.000Z",
  "end_reason": null,
  "messages_sent": 1,
  "timeline": [
    { "at": "...", "type": "started", "detail": "trial_started" },
    { "at": "...", "type": "sent", "step_id": "s2", "message_id": "msg_..." },
    { "at": "...", "type": "waiting_for_event", "step_id": "s3", "detail": "subscription_created" }
  ]
}

status is running, waiting, completed, or failed. end_reason says why: finished, end_step, branch_end (completed); opted_out, canceled, flow_deleted, expired (a run older than 90 days), error (failed).

Three webhook events follow a run: automation.run.started, automation.run.completed and automation.run.failed, each with automation_id, run_id, to and, at the end, reason, messages_sent and messages_failed.

Limits and retention

  • 20 steps per flow, 50 flows per account, 20 active at once, 5 trigger filters.
  • Waits and timeouts: 1 minute to 30 days. A run ends after 90 days whatever it is waiting for.
  • Tracked events are kept for 30 days. A run is kept for 30 days after it ends. The user_id to phone links are kept until you close your account.