TL;DR - Stop chasing no‑shows and post‑visit tasks manually. With OhMD’s Special Delivery API you can trigger a series of scheduled, consent‑aware texts the moment an appointment ends—collect feedback, push care‑plan reminders, and drive revenue cycle touch‑points automatically.

1. What Counts as a “Patient Follow‑Up”?

Goal

Typical Timing

SMS Example

Satisfaction Survey

1 hour after visit

“How was your visit with Dr. Oh? Reply 1‑5.”

Medication Reminder

Same night

“Remember to start amoxicillin tonight. Reply DONE when taken.”

Physical Therapy Video

+24 h

“Ready to stretch? Watch your Day‑1 video → {{link}}”

Invoice Reminder

+7 d

“Your copay is ready. Pay securely → {{pay‑link}}.”

Each is just a timed SMS tied to the same encounter—perfect for automation.

2. Architecture Overview

EHR / Practice Mgmt ──▶ OhMD API ──▶ Carrier ──▶ Patient SMS
▲ │
│ └─◀─ Webhooks (SOLICITED_RESPONSE_RECEIVED, MESSAGE_STATUS_CHANGE) └────── Encounter completed (event)

  1. EHR fires an “appointment complete” event to your service.

  2. Your service calls responseSolicitationStart to create the follow‑up workflow

  3. OhMD sends timed messages; replies post to the same channel.

  4. Webhooks feed responses/results back to the EHR.

3. Prereqs

  • Patient has SMS consent (see previous consent post).

  • You know the appointment’s external ID and end-time.

  • You’ve created any custom calendars (e.g. CLINIC_HOURS).


4. Kick‑Off the Workflow (responseSolicitationStart)

POST /api/responseSolicitationStart
{
"event": {
"eventType": "APPOINTMENT",
"timelineType": "REQUEST", // follow‑ups begin after event
"eventDate": "2025-06-27T15:00:00-04:00",
"identifier": {"id": "APPT_47832", "authority": "EXTERNAL"},
"owner": {"identifier": {"id": "0188bf4c-bd7d-…"}}
},
"workflow": {
"name": "POST_VISIT_FOLLOW_UP",
"steps": [
{
"offset": {"hours": 1},
"function": "messageDispatch",
"body": {
"recipient": [{
"patientIdentity": {"identifier": {"id": "2000"}},
"device": {"phone": {"number": "+16464624000"}},
"destinationCode": "SMS"
}],
"body": {"actualBody": "How was today’s visit? Reply 1‑5."},
"consentAgreementCode": "SMS",
"schedule": {"calendarCode": "FEDERAL_TCPA"}
}
},
{
"offset": {"hours": 24}, // 24 h after event
"function": "messageDispatch",
"body": {
"recipient": [{"patientIdentity": {"identifier": {"id": "2000"}}}],
"body": {"actualBody": "Watch your rehab video → {{link}}"},
"trackableLinks": [{"url": "https://rehab.example.com/v/abc"}],
"consentAgreementCode": "SMS",
"schedule": {"calendarCode": "FEDERAL_TCPA"}
}
}
]
}
}
POST /api/responseSolicitationStart
{
"event": {
"eventType": "APPOINTMENT",
"timelineType": "REQUEST", // follow‑ups begin after event
"eventDate": "2025-06-27T15:00:00-04:00",
"identifier": {"id": "APPT_47832", "authority": "EXTERNAL"},
"owner": {"identifier": {"id": "0188bf4c-bd7d-…"}}
},
"workflow": {
"name": "POST_VISIT_FOLLOW_UP",
"steps": [
{
"offset": {"hours": 1},
"function": "messageDispatch",
"body": {
"recipient": [{
"patientIdentity": {"identifier": {"id": "2000"}},
"device": {"phone": {"number": "+16464624000"}},
"destinationCode": "SMS"
}],
"body": {"actualBody": "How was today’s visit? Reply 1‑5."},
"consentAgreementCode": "SMS",
"schedule": {"calendarCode": "FEDERAL_TCPA"}
}
},
{
"offset": {"hours": 24}, // 24 h after event
"function": "messageDispatch",
"body": {
"recipient": [{"patientIdentity": {"identifier": {"id": "2000"}}}],
"body": {"actualBody": "Watch your rehab video → {{link}}"},
"trackableLinks": [{"url": "https://rehab.example.com/v/abc"}],
"consentAgreementCode": "SMS",
"schedule": {"calendarCode": "FEDERAL_TCPA"}
}
}
]
}
}
POST /api/responseSolicitationStart
{
"event": {
"eventType": "APPOINTMENT",
"timelineType": "REQUEST", // follow‑ups begin after event
"eventDate": "2025-06-27T15:00:00-04:00",
"identifier": {"id": "APPT_47832", "authority": "EXTERNAL"},
"owner": {"identifier": {"id": "0188bf4c-bd7d-…"}}
},
"workflow": {
"name": "POST_VISIT_FOLLOW_UP",
"steps": [
{
"offset": {"hours": 1},
"function": "messageDispatch",
"body": {
"recipient": [{
"patientIdentity": {"identifier": {"id": "2000"}},
"device": {"phone": {"number": "+16464624000"}},
"destinationCode": "SMS"
}],
"body": {"actualBody": "How was today’s visit? Reply 1‑5."},
"consentAgreementCode": "SMS",
"schedule": {"calendarCode": "FEDERAL_TCPA"}
}
},
{
"offset": {"hours": 24}, // 24 h after event
"function": "messageDispatch",
"body": {
"recipient": [{"patientIdentity": {"identifier": {"id": "2000"}}}],
"body": {"actualBody": "Watch your rehab video → {{link}}"},
"trackableLinks": [{"url": "https://rehab.example.com/v/abc"}],
"consentAgreementCode": "SMS",
"schedule": {"calendarCode": "FEDERAL_TCPA"}
}
}
]
}
}
POST /api/responseSolicitationStart
{
"event": {
"eventType": "APPOINTMENT",
"timelineType": "REQUEST", // follow‑ups begin after event
"eventDate": "2025-06-27T15:00:00-04:00",
"identifier": {"id": "APPT_47832", "authority": "EXTERNAL"},
"owner": {"identifier": {"id": "0188bf4c-bd7d-…"}}
},
"workflow": {
"name": "POST_VISIT_FOLLOW_UP",
"steps": [
{
"offset": {"hours": 1},
"function": "messageDispatch",
"body": {
"recipient": [{
"patientIdentity": {"identifier": {"id": "2000"}},
"device": {"phone": {"number": "+16464624000"}},
"destinationCode": "SMS"
}],
"body": {"actualBody": "How was today’s visit? Reply 1‑5."},
"consentAgreementCode": "SMS",
"schedule": {"calendarCode": "FEDERAL_TCPA"}
}
},
{
"offset": {"hours": 24}, // 24 h after event
"function": "messageDispatch",
"body": {
"recipient": [{"patientIdentity": {"identifier": {"id": "2000"}}}],
"body": {"actualBody": "Watch your rehab video → {{link}}"},
"trackableLinks": [{"url": "https://rehab.example.com/v/abc"}],
"consentAgreementCode": "SMS",
"schedule": {"calendarCode": "FEDERAL_TCPA"}
}
}
]
}
}

Why use responseSolicitationStart? It ties every outbound message and inbound reply to the same workflowId, simplifying audit & analytics.

5. Capture Patient Replies (SOLICITED_RESPONSE_RECEIVED Webhook)

{
“workflow”: {“identifiers”: [{“id”: “3018fa3a-49…”, “authority”: “INTERNAL”}]},
“message”: {
“message”: {“body”: {“actualBody”: “5”}}, // patient’s rating
“previousMessage”: {}
},
“presumedEvent”: {“eventType”:”APPOINTMENT”, “eventDate”:”2025-06-27T15:00:00-04:00}
}
{
“workflow”: {“identifiers”: [{“id”: “3018fa3a-49…”, “authority”: “INTERNAL”}]},
“message”: {
“message”: {“body”: {“actualBody”: “5”}}, // patient’s rating
“previousMessage”: {}
},
“presumedEvent”: {“eventType”:”APPOINTMENT”, “eventDate”:”2025-06-27T15:00:00-04:00}
}
{
“workflow”: {“identifiers”: [{“id”: “3018fa3a-49…”, “authority”: “INTERNAL”}]},
“message”: {
“message”: {“body”: {“actualBody”: “5”}}, // patient’s rating
“previousMessage”: {}
},
“presumedEvent”: {“eventType”:”APPOINTMENT”, “eventDate”:”2025-06-27T15:00:00-04:00}
}
{
“workflow”: {“identifiers”: [{“id”: “3018fa3a-49…”, “authority”: “INTERNAL”}]},
“message”: {
“message”: {“body”: {“actualBody”: “5”}}, // patient’s rating
“previousMessage”: {}
},
“presumedEvent”: {“eventType”:”APPOINTMENT”, “eventDate”:”2025-06-27T15:00:00-04:00}
}

Use the workflow.identifiers[0].id to correlate the 5‑star reply back to the appointment.

6. Error‑Handling & Edge Cases

Situation

Response

Patient opts out (STOP)

Consent flips to DENY; remaining workflow steps auto‑cancel.

Appointment canceled

Call workflowCancel with workflowId to halt messages.

After‑hours end time

Offsets queue relative to next open time defined by calendar.

7. Observability: Track Every Step

- MESSAGE_STATUS_CHANGE delivered, failed, queued.
- WORKFLOW_STATUS_CHANGE started, completed, canceled

- MESSAGE_STATUS_CHANGE delivered, failed, queued.
- WORKFLOW_STATUS_CHANGE started, completed, canceled

- MESSAGE_STATUS_CHANGE delivered, failed, queued.
- WORKFLOW_STATUS_CHANGE started, completed, canceled

- MESSAGE_STATUS_CHANGE delivered, failed, queued.
- WORKFLOW_STATUS_CHANGE started, completed, canceled

Store these in your analytics DB for cohort analysis (e.g., NPS vs. response rate).

8. Developer Checklist

  • Create one workflow template per follow‑up type (survey, PT, billing).

  • Always set schedule.calendarCode for each step.

  • Subscribe to both SOLICITED_RESPONSE_RECEIVED & CONSENT_CHANGE_DEFAULT webhooks.

  • Build a cron that retries stuck QUEUED messages if network issues occur.

  • Use x-ohmd-testflight:true to dry‑run workflows in staging.

Next Steps

  1. Prototype the JSON above in Postman (sandbox key).

  2. Visualize workflows in your dashboard using workflow.identifiers.

  3. Go live and start measuring decreased no‑shows & higher CSAT in days.

Automated follow‑ups turn every visit into an ongoing conversation - and you ship it with < 50 lines of code.

Automate calls. Engage patients. Reduce phone stress.

See how OhMD can help you.

Automate calls. Engage patients. Reduce phone stress.

See how OhMD can help you.

Automate calls. Engage patients. Reduce phone stress.

See how OhMD can help you.