Automations
Your app says what happened. SimpleSMS decides what to text and when.
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
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 | |
|---|---|
event | Required. Letters, digits, and _ . : -, up to 64 characters. |
phone | The person's number in E.164. |
user_id | Your own id for the person, up to 128 characters. |
properties | Up 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:
{
"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
| Step | Fields |
|---|---|
send_sms | body, optional from (defaults to the flow's number) |
wait | seconds (1 minute to 30 days) |
wait_for_event | event, timeout_seconds, on_received, on_timeout |
condition | check, on_met, on_not_met |
end | none |
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
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/automations | All flows, with run counts |
POST /v1/automations | Create a draft |
GET / PATCH / DELETE /v1/automations/{id} | Read, edit, delete |
POST /v1/automations/{id}/activate · /pause | Turn on, hold |
GET /v1/automations/{id}/runs | Runs, 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_unconfirmedand 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. Settopic_idon 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
{
"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_idto phone links are kept until you close your account.