✅ Meta App Review approved — GoSendAPI Cloud is live. Sign up free →
GuidesHealthcare recipes

Healthcare recipes

Practical, end-to-end examples for the most common healthcare messaging flows. Each recipe includes:

  • The business scenario
  • The Meta-approved template (with exact JSON to submit)
  • The POST /v1/messages request
  • The webhook events you’ll receive back

These are the patterns we see running in production across medical practices, clinics, and diagnostic labs using GoSendAPI Cloud.

All recipes assume you’re familiar with Sending messages and Webhooks. If not, read those first.


Recipe 1 — Appointment reminder with confirm / reschedule buttons

Scenario: 24 hours before an appointment, send the patient a reminder with a clinic image header, appointment details, and two buttons — “Confirmar” and “Reprogramar”. Handle the patient’s response to update your appointment status.

Flow

Cron @ 24h before appointment


POST /v1/messages  (template: appointment_reminder)


Patient receives on WhatsApp

      ├──> Taps "Confirmar"  ──> whatsapp.message.received (button reply)
      │                         └──> Your app: mark status='confirmed'

      ├──> Taps "Reprogramar" ──> whatsapp.message.received (button reply)
      │                         └──> Your app: mark status='reschedule_requested'
      │                                       + send list of alternative slots

      └──> No response          ──> No action (or send follow-up template)

Template — submit to Meta first

Submit this once and wait for Meta approval (usually 2–24h for UTILITY category):

{
  "name": "appointment_reminder",
  "language": "es_AR",
  "category": "UTILITY",
  "components": [
    {
      "type": "HEADER",
      "format": "IMAGE",
      "example": {
        "header_handle": ["https://your-cdn.com/clinic-logo.jpg"]
      }
    },
    {
      "type": "BODY",
      "text": "Hola {{1}} 👋\n\nTe recordamos tu turno con {{2}} el {{3}} a las {{4}}.\n\n📍 Dirección: {{5}}",
      "example": {
        "body_text": [["Juan", "Dr. Pérez", "20/05", "14:00", "Av. Corrientes 123, CABA"]]
      }
    },
    {
      "type": "FOOTER",
      "text": "Gestión Salud"
    },
    {
      "type": "BUTTONS",
      "buttons": [
        { "type": "QUICK_REPLY", "text": "Confirmar" },
        { "type": "QUICK_REPLY", "text": "Reprogramar" }
      ]
    }
  ]
}

Send it

curl https://cloud.gosendapi.com/v1/messages \
  -H "X-API-Key: gsk_live_..." \
  -H "Idempotency-Key: appt-9847-reminder-24h" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number_id": "1104106619461825",
    "to": "5491140123456",
    "type": "template",
    "template": {
      "name": "appointment_reminder",
      "language": { "code": "es_AR" },
      "components": [
        {
          "type": "header",
          "parameters": [
            {
              "type": "image",
              "image": { "link": "https://your-cdn.com/clinic-logo.jpg" }
            }
          ]
        },
        {
          "type": "body",
          "parameters": [
            { "type": "text", "text": "Juan" },
            { "type": "text", "text": "Dr. Pérez" },
            { "type": "text", "text": "20/05" },
            { "type": "text", "text": "14:00" },
            { "type": "text", "text": "Av. Corrientes 123, CABA" }
          ]
        },
        {
          "type": "button",
          "sub_type": "quick_reply",
          "index": "0",
          "parameters": [
            { "type": "payload", "payload": "confirm_appt_9847" }
          ]
        },
        {
          "type": "button",
          "sub_type": "quick_reply",
          "index": "1",
          "parameters": [
            { "type": "payload", "payload": "reschedule_appt_9847" }
          ]
        }
      ]
    }
  }'
⚠️

The payload inside button parameters is what you’ll receive in the webhook when the patient taps. Include your internal appointment ID (appt_9847) — otherwise you won’t know which appointment they’re confirming.

Webhook when patient taps “Confirmar”

You’ll receive on your configured webhook URL:

{
  "event": "whatsapp.message.received",
  "delivery_id": "cf68abb1-522f-4118-b7a0-508920cbb359",
  "occurred_at": "2026-05-19T14:32:07Z",
  "tenant_id": "5",
  "phone_number_id": "1104106619461825",
  "message": {
    "id": "10521",
    "from_phone": "5491140123456",
    "type": "button",
    "button": {
      "text": "Confirmar",
      "payload": "confirm_appt_9847"
    }
  }
}

Extract payload, match your appointment ID, update your DB. Done.


Recipe 2 — Deliver lab results as PDF document

Scenario: When lab results are ready, send the patient the PDF file plus a follow-up template inviting them to schedule a review appointment with their doctor.

Flow

Lab results ready in your system


Step 1: POST /v1/media  (upload PDF once)
      │  ──> { "id": "media_934567890" }


Step 2: POST /v1/messages  (document with body caption)
      │  ──> Patient receives PDF on WhatsApp


Step 3: POST /v1/messages  (template: results_ready_followup)
      │  ──> Patient sees "Schedule review" button


Patient taps or doesn't. Handle as Recipe 1.

Send the PDF (media message)

# Step 1 — upload PDF once (reusable if you send to multiple patients)
curl -X POST https://cloud.gosendapi.com/v1/media \
  -H "X-API-Key: gsk_live_..." \
  -F "phone_number_id=1104106619461825" \
  -F "file=@./results-patient-9847.pdf" \
  -F "type=application/pdf"
 
# Response:
# { "id": "934567890123456" }
 
# Step 2 — send using the media ID
curl https://cloud.gosendapi.com/v1/messages \
  -H "X-API-Key: gsk_live_..." \
  -H "Idempotency-Key: appt-9847-lab-results" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number_id": "1104106619461825",
    "to": "5491140123456",
    "type": "document",
    "document": {
      "id": "934567890123456",
      "filename": "resultados-laboratorio.pdf",
      "caption": "Hola Juan, aquí tenés tus resultados del análisis del 15/05."
    }
  }'

Send-by-URL is simpler but the URL must be public and remain accessible for Meta to fetch. Use upload-once if you’re sending the same document to multiple patients (marketing) or if your URLs are behind auth.

Then send the follow-up template

Body says: “Tus resultados fueron enviados. ¿Querés agendar consulta con el médico para revisarlos?” with an interactive button. Same pattern as Recipe 1 — omitted for brevity.


Recipe 3 — Post-visit satisfaction survey

Scenario: 2 hours after the appointment ends, send a template with a rating list (1-5 stars). Aggregate responses to measure clinic NPS.

Template

{
  "name": "post_visit_survey",
  "language": "es_AR",
  "category": "UTILITY",
  "components": [
    {
      "type": "BODY",
      "text": "Hola {{1}}, esperamos que tu visita del {{2}} haya sido excelente.\n\n¿Cómo calificarías la atención?",
      "example": {
        "body_text": [["Juan", "hoy"]]
      }
    }
  ]
}

Send with an interactive list

After the template, immediately follow up with a free-form interactive list (works because Customer Service Window is open once the patient replies to any message):

curl https://cloud.gosendapi.com/v1/messages \
  -H "X-API-Key: gsk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number_id": "1104106619461825",
    "to": "5491140123456",
    "type": "interactive",
    "interactive": {
      "type": "list",
      "body": { "text": "Elegí tu calificación:" },
      "action": {
        "button": "Puntuar",
        "sections": [
          {
            "title": "Calificación",
            "rows": [
              { "id": "rating_5", "title": "⭐⭐⭐⭐⭐ Excelente" },
              { "id": "rating_4", "title": "⭐⭐⭐⭐ Muy buena" },
              { "id": "rating_3", "title": "⭐⭐⭐ Buena" },
              { "id": "rating_2", "title": "⭐⭐ Regular" },
              { "id": "rating_1", "title": "⭐ Mala" }
            ]
          }
        ]
      }
    }
  }'

Webhook when patient picks a rating

{
  "event": "whatsapp.message.received",
  "message": {
    "type": "interactive",
    "interactive": {
      "type": "list_reply",
      "list_reply": {
        "id": "rating_5",
        "title": "⭐⭐⭐⭐⭐ Excelente"
      }
    }
  }
}

Match id to your rating scale, save to your NPS table. Aggregate weekly.


Recipe 4 — Cancellation waitlist broadcast

Scenario: A patient cancels their appointment 3 hours out. Your app has a waitlist of N patients who want an earlier slot. Broadcast to the top N asking who can take it — first “Sí, la tomo” wins.

The trick: idempotency + first-response wins

Cancellation happens at 15:00 (appt was 18:00)


Query waitlist: top 5 patients matching criteria


Broadcast POST /v1/messages × 5 (parallel, each with unique Idempotency-Key)


Patients receive on WhatsApp:
   "Se liberó un turno hoy a las 18:00. ¿Podés tomarlo? [Sí, lo tomo]"


First response wins:
      ├──> Patient 3 taps "Sí, lo tomo" @ 15:04
      │    ──> Your app: assign slot to patient 3
      │    ──> Send template to patients 1,2,4,5: "El turno ya fue asignado. Te avisaremos del próximo."

      └──> Race condition: patients 4 and 5 tap at 15:04:03 (after 3 already won)
           ──> Your app: detect race, send them the "already taken" template

Send the broadcast

# For each patient in your waitlist:
for patient_id in 12401 12402 12403 12404 12405; do
  curl https://cloud.gosendapi.com/v1/messages \
    -H "X-API-Key: gsk_live_..." \
    -H "Idempotency-Key: waitlist-slot-18h00-2026-05-19-p${patient_id}" \
    -H "Content-Type: application/json" \
    -d "{
      \"phone_number_id\": \"1104106619461825\",
      \"to\": \"$(get_patient_phone $patient_id)\",
      \"type\": \"template\",
      \"template\": {
        \"name\": \"waitlist_slot_available\",
        \"language\": { \"code\": \"es_AR\" },
        \"components\": [
          {
            \"type\": \"body\",
            \"parameters\": [
              { \"type\": \"text\", \"text\": \"18:00\" }
            ]
          },
          {
            \"type\": \"button\",
            \"sub_type\": \"quick_reply\",
            \"index\": \"0\",
            \"parameters\": [
              { \"type\": \"payload\", \"payload\": \"waitlist_take_slot_18h_${patient_id}\" }
            ]
          }
        ]
      }
    }" &
done
wait
⚠️

Handle the race in your app, not on our side. When the first webhook arrives, atomically assign the slot in your DB (SQL UPDATE ... WHERE slot_status='available' RETURNING id or a Redis lock). If update returns nothing, someone else got it — send the “already taken” template to that patient.

Don’t rely on us to reject the second acceptor: our webhook is delivered to your app after Meta processed it. Race resolution is your responsibility.