<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Dentiva.in]]></title><description><![CDATA[Dentiva.in]]></description><link>https://dentiva.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6aca50ac2d4c9afef727e6d2/377c37e8-4b1d-44c6-90a8-ac264aba7472.png</url><title>Dentiva.in</title><link>https://dentiva.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 20 Oct 2026 19:04:02 GMT</lastBuildDate><atom:link href="https://dentiva.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Replacing SMS with WhatsApp Cloud API for Clinic Appointment Workflows: Architecture and Edge Cases]]></title><description><![CDATA[Technical architecture, webhook handling, and idempotency safeguards for automating clinical appointment reminders using WhatsApp Cloud API.]]></description><link>https://dentiva.hashnode.dev/replacing-sms-with-whatsapp-cloud-api-for-clinic-appointment-workflows-architecture-and-edge-cases</link><guid isPermaLink="true">https://dentiva.hashnode.dev/replacing-sms-with-whatsapp-cloud-api-for-clinic-appointment-workflows-architecture-and-edge-cases</guid><category><![CDATA[Web Development]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[architecture]]></category><category><![CDATA[healthtech]]></category><category><![CDATA[api]]></category><dc:creator><![CDATA[Dentiva]]></dc:creator><pubDate>Sat, 10 Oct 2026 17:17:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6aca50ac2d4c9afef727e6d2/e16eaf87-159b-4bfc-be7f-27e63ebe8def.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When running a clinical SaaS in India, communication channels make or break appointment adherence. When we built <a href="https://dentiva.in/">Dentiva</a>, our initial prototype used transactional SMS gateways for appointment confirmations and reminders.</p>
<p>The drop-off rates were painful. Transactional SMS in India gets dumped into the promotional inbox by modern phone dialers, carrier-level DLT scrubbing templates cause silent delivery drops, and patients rarely read them. In dental outpatient care, a missed 45-minute root canal slot burns chair time that cannot be recovered.</p>
<p>We rebuilt our notification pipeline from scratch around the official WhatsApp Business Cloud API. Here is the technical architecture, webhook handling, and concurrency safeguards we put in place.</p>
<h2><strong>The Bottlenecks of WhatsApp Cloud API</strong></h2>
<p>Unlike sending a simple HTTP POST to an SMS gateway, WhatsApp enforces strict operational constraints:</p>
<ol>
<li><p>Pre-approved Message Templates: You cannot send arbitrary strings outside an open 24-hour service window. Every notification template must be registered, categorized (Utility, Marketing, or Authentication), and approved with strict variable placeholders.</p>
</li>
<li><p>Asynchronous Delivery States: An HTTP 200 from the Meta Graph API only confirms that Meta accepted your payload. It does not confirm delivery to the handset, let alone whether the patient read it.</p>
</li>
<li><p>Webhook Retries and Idempotency: Meta webhooks retry aggressively if your endpoint responds with anything other than a fast 200 OK, or if response times exceed three seconds.</p>
</li>
</ol>
<h2>2. Architecture: Edge Workers and Decoupled Queues</h2>
<p>To keep web latency negligible for front-desk staff booking appointments, we separated the appointment creation flow from message dispatch using background queues on Cloudflare Workers and PostgreSQL.</p>
<ol>
<li><p>Front-Desk Browser: Staff schedules an appointment via the clinic dashboard.</p>
</li>
<li><p>Appointment Service (REST API): Persists the appointment to Postgres with status <code>scheduled</code>.</p>
</li>
<li><p>Notification Queue Worker: Triggers via internal event, formats the template, and calls the Meta Graph API.</p>
</li>
<li><p>Meta Cloud Infrastructure: Dispatches the message directly to the patient on WhatsApp.</p>
</li>
<li><p>Inbound Webhook Worker: Ingests asynchronous status callbacks (sent, delivered, read, failed) from Meta.</p>
</li>
<li><p>Event Store (Postgres): Updates the delivery ledger and refreshes front-desk delivery receipts in real time.</p>
</li>
</ol>
<h2>3. Handling Webhook Webhook Verifications and Callbacks</h2>
<p>Meta requires a verification handshake using a challenge token during setup, followed by signed POST payloads for message status updates.</p>
<p>Here is a minimal TypeScript handler showing how we verify incoming webhook events and return an immediate 200 OK before kicking off background processing:</p>
<p><code>import { NextRequest, NextResponse } from "next/server";</code></p>
<p><code>export async function GET(req: NextRequest) { const { searchParams } = new URL(req.url); const mode = searchParams.get("hub.mode"); const token = searchParams.get("hub.verify_token"); const challenge = searchParams.get("hub.challenge");</code></p>
<p><code>if (mode === "subscribe" &amp;&amp; token === process.env.WHATSAPP_VERIFY_TOKEN) { return new NextResponse(challenge, { status: 200 }); } return new NextResponse("Forbidden", { status: 403 }); }</code></p>
<p><code>export async function POST(req: NextRequest) { const body = await req.json();</code></p>
<p><code>// Return 200 immediately so Meta doesn't queue retries // Process the status updates asynchronously const entry = body.entry?.[0]?.changes?.[0]?.value;</code></p>
<p><code>if (entry?.statuses) { for (const status of entry.statuses) { await updateMessageStatus(status.id, status.status, status.timestamp); } }</code></p>
<p><code>return NextResponse.json({ success: true }, { status: 200 }); }</code></p>
<h2>4. Managing Idempotency to Prevent Spamming Patients</h2>
<p>One catastrophic bug in automated messaging is duplicate delivery. If an edge worker times out during a network stutter and retries a reminder, a patient might receive four identical reminder pings within two minutes.</p>
<p>We enforce idempotency at the database layer using a unique composite index:</p>
<p><code>SQL create table public.notification_dispatches ( id uuid primary key default gen_random_uuid(), appointment_id uuid references public.appointments(id) on delete cascade, notification_type text not null, -- 'booking_confirmation', 'reminder_24h', 'reminder_4h' wa_message_id text unique, status text not null default 'queued', dispatched_at timestamptz default now(), constraint uq_appointment_notification_stage unique (appointment_id, notification_type) );</code></p>
<p>By constraining (appointment_id, notification_type), the worker cannot issue duplicate messages for the same trigger stage. If a cron job or webhook fires twice, the database constraint rejects the second write before any HTTP call reaches Meta.</p>
<h3>5. Real-Time Patient Confirmations</h3>
<p>Automating reminders is only half the battle. When patients reply to confirm or request a reschedule, we route inbound webhook messages into the clinic's front-desk dashboard in real time.</p>
<p>If a patient replies with "Confirm", our webhook parser updates the chair schedule status to confirmed immediately, giving receptionists a live view of chair utilization without manual follow-up calls.</p>
<p>Shifting to native WhatsApp workflows reduced no-show rates in our pilot clinics by roughly 28%. The initial overhead of template approvals and webhook state synchronization pays for itself once appointment pipelines run on complete autopilot.</p>
]]></content:encoded></item></channel></rss>