Concept · Iterable · Two-way SMS

Two-way SMS Keyword Router

The plumbing behind the reply keywords in the STEM Camp Lifecycle atlas, built on the auto-response SMS system I ran at iD Tech. Every reply lands in Iterable as an smsReceived event; the work is deciding which journey it belongs to. Kestrel Tech Camps is the atlas's fictional brand, and Priya is its sample parent.

  • Iterable
  • SMS
  • Two-way texting
  • Journeys
  • Webhooks
  • Compliance
Family replies to 53781→SMS provider · STOP, HELP→smsReceived event→Waiting journey, or the Reply Router

A reply passes through three layers. The SMS provider handles the carrier-required keywords, STOP and HELP, before anything else. Answers to a question, like YES to a seat offer, are caught by the journey that asked it, which is waiting in a Hold Until tile. CALL and anything typed in plain words go to one Reply Router journey, started by the SMS Reply entry source.

Two places to catch a reply

Inside the journey that asked · Waitlist

Send SMS"A seat opened for Maya… Reply YES to claim it or NO to pass."
Hold UntilSMS Reply, for up to 6 hours
smsMessage is any of yes, y, yep, yeah
WebhookClaim the seat
Any of no, n, nope
WebhookRelease to the next family
6 hours pass
Send SMS"This hold has ended. Reply CALL if you still want a seat."

The Reply Router · one journey for every family

Start · SMS ReplyEvery inbound message
FiltersmsMessage is none of: campus, online, dates, hold, yes, y, yep, yeah, no, n, nope, keep, 0 to 10
smsMessage is call
WebhookCRM callback task
Send SMS"An advisor will call you within 2 business hours."
Anything else
WebhookAdvisor shared inbox

Keywords live with the question. YES means "claim the seat" in the waitlist journey and "save us seats" in camp week. Only the journey that asked knows which, so each one catches its own answers and the router steps aside.

Every question closes. A Hold Until with no reply times out into a closing text that points to CALL. A YES that arrives after the hold has ended matches nothing, so the family has already been told what to send instead.

Same word, different jobs

ReplyAsked byCaught byWhat happens
CAMPUS / ONLINELead welcome & nurtureHold UntilSaves preferredFormat and sends the matching guide
DATESBrowse abandonmentHold UntilTexts the open weeks for the last course viewed
HOLDCart & checkout recoveryHold UntilHolds the seat for 24 hours
YES / NOWaitlist & seat alertsHold UntilClaims the held seat, or releases it
YESCamp weekHold UntilSaves seats at the Friday showcase
0 to 10Post-camp surveyHold UntilRecords the score; 0 to 6 opens a support case
CALLAny textReply RouterCreates a CRM callback task; an advisor calls within 2 business hours
HELP / STOPAny textSMS providerCarrier-required help and opt-out
Anything elseAny textReply RouterGoes to an enrollment advisor's shared inbox

Rules the router depends on

Replies are stored in lowercase. Iterable records smsMessage in lowercase, so every filter compares against "call", never "CALL", and capitalization never matters. Punctuation does: "Yes!" isn't "yes", so it goes to an advisor rather than being guessed at.

STOP and HELP come first. The SMS provider handles them before Iterable's journeys see anything, and the opt-out updates the number's subscription. No journey has to remember to honor them.

Only two-way senders take replies. The short code 53781 is two-way; an alphanumeric sender couldn't receive replies at all. MMS replies, like a photo, aren't supported.

Unknown numbers are ignored by default. A reply from a number with no profile still appears in the Inbound SMS log, but it doesn't create a person or start a journey unless placeholder profiles are switched on for the project.

Pick the last text the family got, then send a reply: tap a suggestion or type your own. The route and the event Iterable records update with each reply.

Last text the family got
Kestrel53781

Route

    Recorded in Iterable

    When Priya replies CALL, the reply arrives as an event like the one below. The router's Webhook tile turns it into a callback task, filling the body with Handlebars from the event and her profile, including the lastQuiz answers from the quiz event page, so the advisor knows what she's shopping for before dialing.

    smsReceived · as Iterable records it
    {
      "eventType": "smsReceived",
      "email": "priya.k@example.com",
      "fromPhoneNumber": "+12065550143",
      "toPhoneNumber": "53781",
      "smsMessage": "call",
      "createdAt": "2026-06-02 15:04:11 +00:00"
    }
    POSTcrm.example.com/api/callbacks · Webhook tile body
    {
      "source": "sms_reply",
      "email": "{{email}}",
      "phone": "{{phoneNumber}}",
      "parentName": "{{firstName}}",
      "student": "{{lastQuiz.studentName}}",
      "pathways": "{{join lastQuiz.pathways ", "}}",
      "message": "{{smsMessage}}",
      "receivedAt": "{{createdAt}}"
    }

    Once the CRM creates the task, it posts a callbackRequested event back to Iterable. The cart recovery journey uses that event as an exit rule, so Priya isn't texted about her cart while an advisor is already calling her.

    That's the same exit-rule pattern the API page uses for purchases during an abandoned-cart wait.

    POSThttps://api.iterable.com/api/events/track
    {
      "email": "priya.k@example.com",
      "eventName": "callbackRequested",
      "dataFields": {
        "taskId": "CB-20417",
        "channel": "sms",
        "dueBy": "2026-06-02 17:04:11 +00:00"
      }
    }
    Back to Projects

    © 2026 Cheyne Mosher. All rights reserved.

    Back arrow to go back to main home page.