📞

Twilio Phone & Text

J4H lets you log entries, get AI summaries, and focus on specific symptoms — all by phone call or text message, no app required. This explainer covers how it works, every word the voice says, the text commands, and how to customize it.

🌐
What is Twilio?
The service behind J4H phone & text

Twilio is a cloud communications platform. It gives developers a real phone number that can send and receive calls and text messages. Your code never touches a cell tower — Twilio handles all the carrier infrastructure and just calls your app's web endpoints when something arrives.

In J4H, one Twilio phone number does two things:

  • Inbound SMS (text) — log a diary entry, or send a command to get an AI summary.
  • Inbound voice call — speak your symptoms, J4H transcribes and saves them, then offers to read you an AI summary.
Key concept — webhooks: When a message or call arrives, Twilio makes an HTTP POST request to a URL you configured (called a webhook). J4H's Flask app receives that POST, does the work, and replies with XML instructions called TwiML that tell Twilio what to say or do next.
💬
Texting a Diary Entry
Send a message — it becomes a health entry immediately

First register your number at /phone-setup. After that, just text naturally:

Patient texts: "knee pain after walking 6"
Twilio POSTs it to j4h.org/api/sms
J4H looks up the phone number, finds the patient, strips the trailing pain level
Entry saved: content = "[SMS] knee pain after walking", pain_level = 6
Reply: "Entry saved for James Harrison. Pain level 6 recorded. (Text HELP for commands.)"

Pain level shorthand

End your text with a number 1–10 and it's automatically saved as the pain level:

"headache all morning 8" # entry="headache all morning", pain=8 "neck stiffness when I woke up 5" # entry="neck stiffness...", pain=5 "tired today" # entry="tired today", pain=None
Entries logged by text are tagged [SMS]. Voice entries are tagged [VOICE]. Both appear in the app alongside normal entries.
🤖
Text Commands — AI Summaries by SMS
SUMMARY, FOCUS, and HELP — get your health summary without opening the app

If the first word of your text is a command keyword, J4H handles it as a command instead of a diary entry. Commands are case-insensitive.

Text thisWhat you get back
SUMMARY AI summary of your last 30 days — general health focus
SUMMARY 7 AI summary of the last 7 days (any number 1–365 works)
SUMMARY dentist Summary tailored for a dentist appointment
SUMMARY neurologist Summary focused on neurological symptoms
FOCUS knee Focused summary on "knee" — searches 90 days of entries
FOCUS lower back pain Focused summary on any topic or body part you name
HELP Lists all commands + all available specialty names

SUMMARY command — live demo

SUMMARY 14
You → J4H number
J4H Summary (General, 14d, 8 entries): I've been having knee pain on the right side most days, usually around a 6 or 7 out of 10. The pain gets worse after walking or standing for long periods, and I also noticed some lower back stiffness that tends to flare up in the evenings. Over the last two weeks the knee pain has been gradually getting worse. Cold weather seems to make both areas tighten up. Resting usually helps, and I've noticed the mornings are typically better than the afternoons.
J4H → You (SMS reply)

FOCUS command — live demo

FOCUS knee
You → J4H number
J4H Focus — knee (5 entries): I've been having pain on the right side of my knee, usually a 6 or 7 out of 10. It comes on after I've been walking or standing for a while, and sometimes I feel it when I first get up in the morning. I've noticed it started getting worse about two weeks ago. Cold weather makes it tighter, and sitting down for a few minutes usually helps. It hasn't stopped me from walking but I'm aware of it most days.
J4H → You (SMS reply)

Available specialties for SUMMARY

These keywords work after SUMMARY:

general · dentist · podiatrist · orthopedist · neurologist cardiologist · gastroenterologist · rheumatologist · physical_therapist
SMS length: A single text segment is 160 characters. Twilio automatically chains longer messages into one thread. A typical 2-paragraph summary is ~500 characters — about 3–4 segments. Each segment costs ~$0.0075, so a full summary costs roughly $0.03 to receive.

How the decision tree works in code

# In receive_sms() — runs before diary-entry logic words = body.split() cmd = words[0].upper() # first word arg = ' '.join(words[1:]) # everything after it if cmd == 'HELP': # → send command list if cmd == 'SUMMARY': # → generate AI summary (arg sets days or specialty) if cmd == 'FOCUS': # → generate focus summary on arg topic # otherwise → save as diary entry
🎙️
The Voice Call Script
Every word Polly.Joanna says — including the new AI summary option

When someone calls the J4H Twilio number, they hear Polly.Joanna — Amazon Polly's neural text-to-speech voice. After logging an entry and pain level, callers can now press 1 to hear an AI summary read aloud.

SYSTEM
Call connects. J4H checks if the caller's number is registered.
JOANNA
"Welcome to J4H Health Diary. Please enter your 4-digit PIN."
Waits up to 10 seconds for keypad input.
CALLER
Presses their 4-digit PIN. (same as the app passcode)
SYSTEM
Validates PIN against APP_PASSCODE environment variable.
JOANNA
"Hello [Patient Name]. Please describe your symptoms or how you are feeling. Speak now, and pause when you are done."
Listens until the caller pauses.
CALLER
Speaks their diary entry out loud.
SYSTEM
Twilio transcribes speech to text. J4H saves it as a [VOICE] entry.
JOANNA
"Entry saved. On a scale of 1 to 10, what is your pain level? Press the number on your keypad now, or press 0 to skip."
Waits up to 8 seconds.
CALLER
Presses a digit (1–10), or 0 / nothing to skip.
JOANNA
"Pain level [N] recorded." — or — "No pain level recorded."
JOANNA
"Press 1 to hear an AI summary of your recent health entries, or hang up now." NEW
Waits 6 seconds for keypad input.
CALLER
Presses 1 for summary — or hangs up.
JOANNA
"Here is a summary of your recent health, [Patient Name]." NEW
Reads the full 2-paragraph AI-generated summary aloud.
"End of summary. Take care. Goodbye."
SYSTEM
Call ends. Entry and pain level are visible in the app immediately.

Error paths

JOANNA
Number not registered: "Your phone number is not registered with J4H. Please visit j4h dot org slash phone dash setup to link your number, then call back."
JOANNA
Wrong PIN: "Incorrect PIN. Goodbye."
JOANNA
Speech not understood: "We could not understand your entry. Please call back and try again."
JOANNA
No entries in last 30 days: "No entries were found in the last 30 days. Take care. Goodbye."
✏️
Can You Change the Script?
Yes — every word is a plain string in app.py

Every word Joanna says is a Python string passed to resp.say(). There is no separate script file. Changing any line is a one-line edit in app.py followed by a deploy.

Where each line lives

What Joanna saysFunction in app.py
"Welcome to J4H Health Diary…"voice_incoming()
"Hello [name]. Please describe your symptoms…"voice_verify_pin()
"Entry saved. On a scale of 1 to 10…"voice_save_entry()
"Pain level N recorded." / "Press 1 to hear a summary…"voice_save_pain()
"Here is a summary of your recent health…"voice_summary()
All error messagesAll five functions

Example — changing the greeting

# Current (in voice_incoming) gather.say( 'Welcome to J4H Health Diary. Please enter your 4-digit PIN.', voice='Polly.Joanna' ) # Change to anything: gather.say( 'Hi! This is J4H. Enter your PIN to log how you are feeling today.', voice='Polly.Joanna' )

Swapping voices

Replace Polly.Joanna everywhere with any Amazon Polly voice:

  • Polly.Matthew — male, US English
  • Polly.Amy — female, British English
  • Polly.Brian — male, British English
  • Polly.Joanna — female, US English (current)
Tip: Twilio reads text exactly as written. Write "j4h dot org" not "j4h.org", and write "slash phone dash setup" not "/phone-setup" — otherwise Joanna will read the punctuation characters aloud.
📅
How Long Does the Phone Number Last?
Billing, trials, and keeping your number active
ItemCostNotes
Trial account$0 (free credit)Expires when credit runs out. Can only text/call verified numbers.
US local phone number~$1.15/monthYours indefinitely while account is paid
Inbound SMS$0.0075/messageEach text you receive
AI summary reply~$0.03~4 SMS segments for a 2-paragraph summary
Inbound voice~$0.0085/minCharged per minute
Speech transcription~$0.02/30 secPer voice entry recorded
Estimated monthly cost at light usage: $1.15 (number) + ~$1–2 (usage) = roughly $2–3/month total for Twilio, separate from Heroku and the Anthropic API.
Trial account restriction: On a free trial, calls and texts only work with phone numbers you've manually verified in the Twilio console. Upgrade to a paid account to remove this restriction.
🔧
How the Code is Wired Together
All routes, the shared helper, and security validation

All Twilio routes in app.py

RouteWhat it does
POST /api/smsInbound text — commands (SUMMARY, FOCUS, HELP) or save diary entry
POST /api/voiceCall entry point — check registration, ask for PIN
POST /api/voice/verify-pinValidate PIN, ask for spoken entry
POST /api/voice/save-entrySave transcribed speech, ask for pain level
POST /api/voice/save-painSave pain level, offer summary option
POST /api/voice/summaryGenerate & read AI summary aloud if caller pressed 1

Shared helper — _sms_entries()

Both the SMS handler and the voice summary route use the same helper to fetch entries for a patient within a date window:

def _sms_entries(patient_id, days=30): start = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d') return db.get_entries(start_date=start, patient_id=patient_id)

Security — Twilio signature validation

Every incoming call or text is validated with a cryptographic signature Twilio attaches to each request. J4H rejects anything that doesn't match:

def _validate_twilio(req): auth_token = os.getenv('TWILIO_AUTH_TOKEN', '') if not auth_token: return True # skip in dev (no token set) validator = RequestValidator(auth_token) sig = req.headers.get('X-Twilio-Signature', '') return validator.validate(url, req.form, sig)

Environment variables

VariableWhere to get itWhat it does
TWILIO_AUTH_TOKENTwilio console → Account → Auth TokenValidates requests are genuinely from Twilio
APP_PASSCODESet by youPIN callers must enter to log an entry
ANTHROPIC_API_KEYAnthropic consoleRequired for SUMMARY/FOCUS/voice summary
Webhook URLs to configure in the Twilio console:
Voice: https://j4h.org/api/voice (HTTP POST)
SMS: https://j4h.org/api/sms (HTTP POST)