Operations

Appointment reminder on WhatsApp through the API: routine, confirmation and LGPD

A no-show is a lost slot. A reminder 24 hours ahead, with reply 1 to confirm and 2 to reschedule, is the lowest-effort automation for cutting no-shows. This recipe builds it from your own system's schedule, without depending on a third-party platform, and handles what usually goes wrong: double sends, messages at the wrong time and sensitive data in the conversation.

Published on · 4 min read

This article is also available in: Português · Español

Where the times come from

The source of truth is your schedule: a database with the appointments table, an ERP or Google Calendar (see the Google Calendar integration). This recipe assumes an appointments table with status, time, name, phone, a consent_whatsapp field (the patient agreed to receive reminders) and reminder_sent_at (null until sent). If your schedule is a closed SaaS, the path is usually a webhook or an export; check what it offers before promising the reminder.

The consent field is not decoration. Health appointment data is personal data, and almost always sensitive by context. Collect consent at registration or when booking ('may I remind you of the appointment on WhatsApp?') and check the field in the send filter.

The routine: window, lock and gap

The routine runs every 15 minutes and fetches confirmed appointments that start 23 to 24 hours ahead, with no reminder yet and with consent. The one-hour window, together with the reminder_sent_at filter, makes each appointment get picked up only once. Before sending, the routine 'claims' the row with a conditional UPDATE: if two runs overlap, only one manages to mark the send. If the send fails, the mark is undone so the next run tries again.

The random gap of a few seconds between messages avoids bursts. For a clinic's reminders the volume is low, but the principle holds: a human pace reduces risk. The send uses POST /api/v1/{instanceId}/send/text with number and text, and ZapZap also applies delay and the typing indicator by default (anti-ban). Check beforehand that the number has WhatsApp with the chat/check route, described in the reference.

Node.js (cron a cada 15 minutos)

// Runs every 15 minutes (cron, queue worker or n8n schedule). Appointments come from YOUR system.
async function remindAppointments() {
  const rows = await db.query(`
    SELECT id, patient_name, patient_phone, starts_at
    FROM appointments
    WHERE status = 'confirmed' AND reminder_sent_at IS NULL AND consent_whatsapp = TRUE
      AND starts_at BETWEEN now() + interval '23 hours' AND now() + interval '24 hours'`);

  for (const a of rows.rows) {
    // claim before sending: a second run cannot pick the same row
    const claim = await db.query("UPDATE appointments SET reminder_sent_at = now() WHERE id = $1 AND reminder_sent_at IS NULL", [a.id]);
    if (!claim.rowCount) continue;

    const when = new Date(a.starts_at).toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "short", timeZone: "America/Sao_Paulo" });
    try {
      await sendWhatsApp(a.patient_phone, "Oi, " + a.patient_name.split(" ")[0] + ". Lembrete da sua consulta amanhã, " + when + ". Responda 1 para confirmar ou 2 para remarcar.");
    } catch (e) {
      await db.query("UPDATE appointments SET reminder_sent_at = NULL WHERE id = $1", [a.id]); // let the next run retry
      console.error("reminder failed", a.id, e.message);
    }
    await new Promise((r) => setTimeout(r, 4000 + Math.random() * 6000)); // human-like gap between sends
  }
}

// Shared helper: one send through the ZapZap API
async function sendWhatsApp(number, text) {
  const r = await fetch(`https://api.zapzapapi.com/api/v1/${process.env.ZAPZAP_INSTANCE}/send/text`, {
    method: "POST",
    headers: {
      "x-api-key": process.env.ZAPZAP_API_KEY,
      "x-api-secret": process.env.ZAPZAP_API_SECRET,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ number, text }),
  });
  if (!r.ok) throw new Error("ZapZap " + r.status + ": " + (await r.text()));
}

Receiving the 1 or the 2

For the reply to work, the instance has to deliver incoming messages to your server: register the URL with PUT /api/v1/instances/{id}/webhook and the messages event (set up the webhook). Discard fromMe messages, or your own automatic reply may re-enter the flow and create a loop. Check the exact payload format with a real message before writing the code; the example below shows only the logic.

Handle unexpected replies carefully: 'ok', 'yes' and 'confirm' are also confirmations. Map a few variations and, if you do not understand, do not answer on your own: forward it to the front desk. To validate the X-ZapZap-Signature header and discard repeated deliveries, follow the guide on webhooks in production.

Node.js (Express)

// Receive the patient's "1" or "2" (message webhook registered with PUT /api/v1/instances/{id}/webhook, event "messages")
app.post("/zapzap", express.json(), async (req, res) => {
  res.sendStatus(200);
  const m = req.body; // check the exact shape with a real message first (see the webhooks reference)
  if (m.fromMe) return; // never react to your own messages, or an automatic reply answers itself in a loop
  const text = String(m.text || "").trim();
  if (text === "1") await markConfirmed(m.from);
  else if (text === "2") await askReschedule(m.from);
});

The text: what to say and what NOT to say

Say the essentials: the patient's name, the day and time, the clinic's name and how to confirm or reschedule. Do not write the procedure, the name of a doctor in a sensitive specialty, an exam result or a diagnosis. WhatsApp is a channel where the device may be with someone else, and health data in a notification is a leak waiting to happen. 'Appointment with Dr. Ana, tomorrow at 2 pm' is enough.

If the clinic has requirements from its professional council on communicating with patients, follow your council's rules. This recipe is technical and does not replace legal or ethical guidance.

Limits and risks

An appointment reminder to someone who booked with you is the lowest-risk use there is. Even so, ZapZap connects numbers through an unofficial, managed channel, and no feature guarantees the absence of blocking. Keep the pace, do not send to anyone who asked to stop and do not use the reminder to advertise a promotion.

Test first with fictional appointments and with your number. Check the time zone (the example uses America/Sao_Paulo), the 23 to 24 hour window and, above all, that running the routine twice in a row does not produce a second message. For specific segments, see the pages for clinics and dentists.

Quick summary

  • A routine every 15 minutes, a 23 to 24 hour window and a conditional UPDATE so you never send twice.
  • Consent at registration and a field checked in the send filter: health data calls for care.
  • A message with no procedure or diagnosis; a 1 or 2 reply by webhook, ignoring fromMe.
  • Test idempotency by running the routine twice and checking that only one message goes out.

Also read

Frequently asked questions

What is the best time to remind the patient?

A reminder 24 hours ahead usually leaves time to reschedule. Some businesses add a second one a few hours before. Test with your schedule; the recipe uses the 23 to 24 hour window.

Do I need consent to remind about an appointment?

Health appointment data is personal data and is usually sensitive. Collect consent at registration or booking, record it and check it before every send. If in doubt, consult your legal counsel.

How do I avoid sending the reminder twice?

With a control field and a conditional UPDATE that claims the row before sending. If two runs overlap, only one manages to mark it. On a send failure, undo the mark to try again.

Can I mention the procedure in the message?

We do not recommend it. The device may be with someone else, and health data in a notification leaks easily. Use name, day, time, clinic and how to confirm or reschedule.

How do I receive the patient's reply?

Register your server's URL as the instance webhook with the messages event, ignore fromMe messages and handle 1 and 2. Check the payload format with a real message before coding.

Can this get the clinic's number blocked?

It can. ZapZap uses an unofficial channel and no feature removes the risk. A reminder to someone who booked, with a human pace and a way to stop, is the lowest-risk use, but it is not zero risk.

Cut no-shows with a simple reminder

Connect the clinic's number and run the recipe routine. R$15 per active number per month, with a welcome credit subject to the current campaign.