---
name: whatsapp-integration
description: Integrate WhatsApp Business API for appointment management including message templates, webhooks, media handling, and conversation flows. Use when building WhatsApp messaging features, appointment reminders, confirmations, or two-way chat for Mawidi's GCC medical clinic customers.
version: 1.0.0
tags: [whatsapp, messaging, appointments, webhooks, business-api, conversations]
allowed-tools: [Read, Write, Edit, Bash, Grep]
related-skills:
  [api-development, i18n-management, email-composer, error-handling]
last-updated: 2025-12-09
---

# WhatsApp Integration

WhatsApp Business API integration patterns for Mawidi's appointment management platform.

**Token Savings**: ~8,000 tokens per WhatsApp feature implementation
**Stack**: WhatsApp Business API, Next.js API routes, Webhooks, Media handling

## Overview

This skill provides production-ready patterns for:

- Sending appointment reminders and confirmations via WhatsApp
- Handling incoming messages and status updates
- Managing message templates (HSM/HNM)
- Processing media (images, documents, voice notes)
- Building conversation flows
- Tracking message delivery status
- Rate limiting and queue management

## When to Use This Skill

Use this skill when:

- Sending appointment confirmations or reminders via WhatsApp
- Building two-way chat features for clinic-patient communication
- Processing incoming WhatsApp messages from patients
- Implementing template message approval workflows
- Handling media uploads (prescription images, documents)
- Tracking message delivery and read receipts
- Building WhatsApp webhook handlers

## Quick Start Examples

### Example 1: Send Appointment Reminder

**Scenario**: Send reminder 24 hours before appointment

```typescript
// lib/services/whatsapp.service.ts
import axios from "axios";

const WHATSAPP_API_URL = "https://graph.facebook.com/v18.0";
const PHONE_NUMBER_ID = process.env.WHATSAPP_PHONE_NUMBER_ID!;
const ACCESS_TOKEN = process.env.WHATSAPP_ACCESS_TOKEN!;

export async function sendAppointmentReminder({
  to,
  patientName,
  appointmentDate,
  doctorName,
  clinicName,
  language = "en",
}: {
  to: string;
  patientName: string;
  appointmentDate: Date;
  doctorName: string;
  clinicName: string;
  language: "en" | "ar";
}) {
  const formattedDate = appointmentDate.toLocaleString(
    language === "ar" ? "ar-SA" : "en-US",
    { dateStyle: "full", timeStyle: "short" },
  );

  const response = await axios.post(
    `${WHATSAPP_API_URL}/${PHONE_NUMBER_ID}/messages`,
    {
      messaging_product: "whatsapp",
      to: to.replace(/\D/g, ""), // Remove non-digits
      type: "template",
      template: {
        name: "appointment_reminder", // Pre-approved template
        language: { code: language === "ar" ? "ar" : "en" },
        components: [
          {
            type: "body",
            parameters: [
              { type: "text", text: patientName },
              { type: "text", text: formattedDate },
              { type: "text", text: doctorName },
              { type: "text", text: clinicName },
            ],
          },
          {
            type: "button",
            sub_type: "quick_reply",
            index: "0",
            parameters: [{ type: "payload", payload: "CONFIRM_APPOINTMENT" }],
          },
        ],
      },
    },
    {
      headers: {
        Authorization: `Bearer ${ACCESS_TOKEN}`,
        "Content-Type": "application/json",
      },
    },
  );

  // Store message ID for tracking
  await saveToBothDatabases("whatsappMessage", {
    messageId: response.data.messages[0].id,
    to,
    templateName: "appointment_reminder",
    status: "sent",
    sentAt: new Date(),
  });

  return response.data;
}
```

### Example 2: Handle Incoming Message Webhook

**Scenario**: Patient replies to appointment reminder

```typescript
// app/api/webhooks/whatsapp/route.ts
export async function POST(request: Request) {
  const body = await request.json();

  // Verify webhook (Meta requires verification)
  if (body.object === "whatsapp_business_account") {
    for (const entry of body.entry) {
      for (const change of entry.changes) {
        if (change.field === "messages") {
          const message = change.value.messages?.[0];

          if (message) {
            await handleIncomingMessage(message, change.value.metadata);
          }

          // Handle status updates (sent, delivered, read)
          const status = change.value.statuses?.[0];
          if (status) {
            await handleMessageStatus(status);
          }
        }
      }
    }
  }

  return Response.json({ success: true });
}

async function handleIncomingMessage(message: any, metadata: any) {
  const { from, text, type } = message;

  // Find associated appointment
  const conversation = await db.whatsappConversation.findFirst({
    where: { phoneNumber: from },
    include: { appointment: true },
  });

  if (type === "text") {
    const messageText = text.body.toLowerCase();

    // Handle common responses
    if (messageText.includes("confirm") || messageText.includes("نعم")) {
      await confirmAppointment(conversation.appointmentId);
      await sendConfirmationMessage(from);
    } else if (
      messageText.includes("cancel") ||
      messageText.includes("إلغاء")
    ) {
      await cancelAppointment(conversation.appointmentId);
      await sendCancellationMessage(from);
    } else {
      // Forward to staff for manual response
      await notifyStaff({
        from,
        message: text.body,
        conversationId: conversation.id,
      });
    }
  } else if (type === "button") {
    // Handle button responses
    const payload = message.button.payload;
    if (payload === "CONFIRM_APPOINTMENT") {
      await confirmAppointment(conversation.appointmentId);
    }
  }

  // Save message to database
  await saveToBothDatabases("whatsappMessage", {
    conversationId: conversation.id,
    messageId: message.id,
    from,
    type,
    content: type === "text" ? text.body : null,
    timestamp: new Date(parseInt(message.timestamp) * 1000),
    direction: "incoming",
  });
}
```

### Example 3: Send Media Message

**Scenario**: Send appointment details as PDF document

```typescript
// Send document via WhatsApp
export async function sendAppointmentDocument({
  to,
  documentUrl,
  caption,
  language = "en",
}: {
  to: string;
  documentUrl: string;
  caption: string;
  language: "en" | "ar";
}) {
  const response = await axios.post(
    `${WHATSAPP_API_URL}/${PHONE_NUMBER_ID}/messages`,
    {
      messaging_product: "whatsapp",
      to: to.replace(/\D/g, ""),
      type: "document",
      document: {
        link: documentUrl, // Must be HTTPS and publicly accessible
        caption,
        filename: "appointment-details.pdf",
      },
    },
    {
      headers: {
        Authorization: `Bearer ${ACCESS_TOKEN}`,
        "Content-Type": "application/json",
      },
    },
  );

  return response.data;
}
```

## Message Template Patterns

### Template Structure (Meta Approved)

WhatsApp requires pre-approved templates. Submit templates via Meta Business Manager.

**Template Example (English):**

```
Name: appointment_reminder
Category: APPOINTMENT_UPDATE
Language: en

Body:
Hello {{1}}, this is a reminder of your appointment on {{2}} with Dr. {{3}} at {{4}}.

Please reply:
- "Confirm" to confirm your appointment
- "Cancel" to cancel

Buttons:
1. Quick Reply: "Confirm Appointment" (payload: CONFIRM_APPOINTMENT)
2. Quick Reply: "Reschedule" (payload: RESCHEDULE)
```

**Template Example (Arabic):**

```
Name: appointment_reminder
Category: APPOINTMENT_UPDATE
Language: ar

Body:
مرحباً {{1}}، هذا تذكير بموعدك في {{2}} مع الدكتور {{3}} في {{4}}.

يرجى الرد:
- "تأكيد" لتأكيد موعدك
- "إلغاء" للإلغاء

Buttons:
1. Quick Reply: "تأكيد الموعد" (payload: CONFIRM_APPOINTMENT)
2. Quick Reply: "إعادة الجدولة" (payload: RESCHEDULE)
```

### Send Template Message

```typescript
export async function sendTemplateMessage({
  to,
  templateName,
  language,
  components,
}: {
  to: string;
  templateName: string;
  language: "en" | "ar";
  components: any[];
}) {
  const response = await axios.post(
    `${WHATSAPP_API_URL}/${PHONE_NUMBER_ID}/messages`,
    {
      messaging_product: "whatsapp",
      to: to.replace(/\D/g, ""),
      type: "template",
      template: {
        name: templateName,
        language: { code: language },
        components,
      },
    },
    {
      headers: {
        Authorization: `Bearer ${ACCESS_TOKEN}`,
        "Content-Type": "application/json",
      },
    },
  );

  return response.data;
}
```

## Webhook Verification

### Initial Webhook Setup

```typescript
// app/api/webhooks/whatsapp/route.ts
export async function GET(request: Request) {
  const url = new URL(request.url);

  // Meta sends verification challenge
  const mode = url.searchParams.get("hub.mode");
  const token = url.searchParams.get("hub.verify_token");
  const challenge = url.searchParams.get("hub.challenge");

  if (mode === "subscribe" && token === process.env.WHATSAPP_VERIFY_TOKEN) {
    return new Response(challenge, { status: 200 });
  }

  return Response.json({ error: "Forbidden" }, { status: 403 });
}
```

## Message Status Tracking

### Handle Status Updates

```typescript
async function handleMessageStatus(status: any) {
  const { id, status: messageStatus, timestamp } = status;

  await saveToBothDatabases("whatsappMessage", {
    where: { messageId: id },
    data: {
      status: messageStatus, // 'sent' | 'delivered' | 'read' | 'failed'
      [`${messageStatus}At`]: new Date(parseInt(timestamp) * 1000),
    },
  });

  // If failed, log reason
  if (messageStatus === "failed" && status.errors) {
    console.error("Message failed:", status.errors);
    await saveToBothDatabases("whatsappMessage", {
      where: { messageId: id },
      data: {
        errorCode: status.errors[0].code,
        errorMessage: status.errors[0].title,
      },
    });
  }
}
```

### Query Message Status

```typescript
// Check if appointment reminder was read
const message = await db.whatsappMessage.findFirst({
  where: {
    conversationId,
    templateName: "appointment_reminder",
  },
});

if (message?.status === "read" && message.readAt) {
  console.log(`Patient read reminder at ${message.readAt}`);
}
```

## Rate Limiting & Queue Management

### WhatsApp Rate Limits

**Official Limits:**

- **Tier 1**: 1,000 business-initiated conversations per 24 hours
- **Tier 2**: 10,000 conversations per 24 hours
- **Tier 3**: 100,000 conversations per 24 hours
- **Unlimited**: Customer-initiated messages (replies within 24-hour window)

### Queue Implementation

```typescript
// Use Bull queue for rate-limited sending
import Queue from "bull";

const whatsappQueue = new Queue("whatsapp-messages", {
  redis: process.env.REDIS_URL,
});

// Add message to queue
export async function queueWhatsAppMessage(message: WhatsAppMessage) {
  await whatsappQueue.add(message, {
    attempts: 3,
    backoff: {
      type: "exponential",
      delay: 2000,
    },
  });
}

// Process queue with rate limiting
whatsappQueue.process(async (job) => {
  const message = job.data;

  try {
    const result = await sendWhatsAppMessage(message);
    return result;
  } catch (error) {
    if (error.response?.status === 429) {
      // Rate limited, retry later
      throw new Error("Rate limited, will retry");
    }
    throw error;
  }
});
```

## Media Handling

### Send Image

```typescript
export async function sendImage({
  to,
  imageUrl,
  caption,
}: {
  to: string;
  imageUrl: string;
  caption?: string;
}) {
  const response = await axios.post(
    `${WHATSAPP_API_URL}/${PHONE_NUMBER_ID}/messages`,
    {
      messaging_product: "whatsapp",
      to: to.replace(/\D/g, ""),
      type: "image",
      image: {
        link: imageUrl, // Must be HTTPS
        caption,
      },
    },
    {
      headers: {
        Authorization: `Bearer ${ACCESS_TOKEN}`,
        "Content-Type": "application/json",
      },
    },
  );

  return response.data;
}
```

### Receive and Download Media

```typescript
async function handleIncomingMedia(message: any) {
  const { type, image, document, audio } = message;

  let mediaId: string;
  let mimeType: string;

  if (type === "image") {
    mediaId = image.id;
    mimeType = image.mime_type;
  } else if (type === "document") {
    mediaId = document.id;
    mimeType = document.mime_type;
  } else if (type === "audio") {
    mediaId = audio.id;
    mimeType = audio.mime_type;
  } else {
    return;
  }

  // Get media URL
  const mediaResponse = await axios.get(`${WHATSAPP_API_URL}/${mediaId}`, {
    headers: { Authorization: `Bearer ${ACCESS_TOKEN}` },
  });

  const mediaUrl = mediaResponse.data.url;

  // Download media
  const fileResponse = await axios.get(mediaUrl, {
    headers: { Authorization: `Bearer ${ACCESS_TOKEN}` },
    responseType: "arraybuffer",
  });

  // Upload to your storage (S3, Cloudinary, etc.)
  const uploadedUrl = await uploadToStorage({
    buffer: fileResponse.data,
    mimeType,
    folder: "whatsapp-media",
  });

  // Save to database
  await saveToBothDatabases("whatsappMedia", {
    messageId: message.id,
    mediaId,
    mediaType: type,
    mimeType,
    url: uploadedUrl,
    caption: image?.caption || document?.caption,
  });

  return uploadedUrl;
}
```

## Conversation Flow Patterns

### Appointment Booking Flow

```typescript
// Multi-step conversation for booking
const CONVERSATION_STATES = {
  IDLE: "idle",
  AWAITING_DATE: "awaiting_date",
  AWAITING_TIME: "awaiting_time",
  AWAITING_CONFIRMATION: "awaiting_confirmation",
  COMPLETED: "completed",
} as const;

async function handleConversation(from: string, messageText: string) {
  // Get or create conversation
  let conversation = await db.whatsappConversation.findFirst({
    where: { phoneNumber: from },
  });

  if (!conversation) {
    conversation = await saveToBothDatabases("whatsappConversation", {
      phoneNumber: from,
      state: CONVERSATION_STATES.IDLE,
      startedAt: new Date(),
    });
  }

  switch (conversation.state) {
    case CONVERSATION_STATES.IDLE:
      if (messageText.includes("book") || messageText.includes("حجز")) {
        await sendMessage(from, "What date would you like? (DD/MM/YYYY)");
        await updateConversationState(
          conversation.id,
          CONVERSATION_STATES.AWAITING_DATE,
        );
      }
      break;

    case CONVERSATION_STATES.AWAITING_DATE:
      const date = parseDate(messageText);
      if (date) {
        await saveToBothDatabases("whatsappConversation", {
          where: { id: conversation.id },
          data: {
            selectedDate: date,
            state: CONVERSATION_STATES.AWAITING_TIME,
          },
        });
        await sendMessage(from, "What time? (e.g., 10:00 AM)");
      } else {
        await sendMessage(from, "Invalid date. Please use DD/MM/YYYY format.");
      }
      break;

    case CONVERSATION_STATES.AWAITING_TIME:
      const time = parseTime(messageText);
      if (time) {
        const appointment = await createAppointment({
          phoneNumber: from,
          date: conversation.selectedDate,
          time,
        });
        await sendMessage(
          from,
          `Appointment booked for ${formatDateTime(appointment.dateTime)}. Reply "Confirm" to confirm.`,
        );
        await updateConversationState(
          conversation.id,
          CONVERSATION_STATES.AWAITING_CONFIRMATION,
        );
      }
      break;

    case CONVERSATION_STATES.AWAITING_CONFIRMATION:
      if (messageText.includes("confirm") || messageText.includes("تأكيد")) {
        await confirmAppointment(conversation.appointmentId!);
        await sendMessage(from, "Your appointment is confirmed! See you then.");
        await updateConversationState(
          conversation.id,
          CONVERSATION_STATES.COMPLETED,
        );
      }
      break;
  }
}
```

## Database Schema

### WhatsApp Message Model

```prisma
model WhatsAppMessage {
  id              String   @id @default(cuid())
  conversationId  String?
  conversation    WhatsAppConversation? @relation(fields: [conversationId], references: [id])

  // Message details
  messageId       String   @unique // WhatsApp message ID
  from            String?  // Sender phone number
  to              String?  // Recipient phone number
  direction       String   // 'incoming' | 'outgoing'

  // Content
  type            String   // 'text' | 'template' | 'image' | 'document' | 'audio'
  content         String?  // Message text
  templateName    String?  // Template name if template message
  mediaUrl        String?  // Media URL if media message

  // Status tracking
  status          String   // 'sent' | 'delivered' | 'read' | 'failed'
  sentAt          DateTime?
  deliveredAt     DateTime?
  readAt          DateTime?

  // Error tracking
  errorCode       String?
  errorMessage    String?

  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt

  @@index([conversationId])
  @@index([from])
  @@index([status])
}

model WhatsAppConversation {
  id              String   @id @default(cuid())
  phoneNumber     String   @unique

  // Conversation state
  state           String   @default("idle")
  selectedDate    DateTime?
  selectedTime    String?

  // Relationships
  appointmentId   String?
  appointment     Appointment? @relation(fields: [appointmentId], references: [id])
  messages        WhatsAppMessage[]

  // Lifecycle
  startedAt       DateTime @default(now())
  completedAt     DateTime?

  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt

  @@index([phoneNumber])
  @@index([state])
}
```

## Error Handling

### Handle WhatsApp API Errors

```typescript
try {
  const result = await sendWhatsAppMessage(message);
} catch (error) {
  if (axios.isAxiosError(error)) {
    const status = error.response?.status;
    const errorData = error.response?.data;

    switch (status) {
      case 400:
        console.error("Invalid request:", errorData);
        // Check template name, parameters
        break;

      case 401:
        console.error("Authentication failed. Check access token.");
        // Refresh access token
        break;

      case 429:
        console.error(
          "Rate limited. Retry after:",
          error.response.headers["retry-after"],
        );
        // Implement exponential backoff
        break;

      case 500:
        console.error("WhatsApp API error:", errorData);
        // Retry with exponential backoff
        break;

      default:
        console.error("Unexpected error:", error);
    }

    // Save error to database
    await saveToBothDatabases("whatsappMessage", {
      where: { messageId: message.id },
      data: {
        status: "failed",
        errorCode: errorData?.error?.code?.toString(),
        errorMessage: errorData?.error?.message,
      },
    });
  }
  throw error;
}
```

## Testing Patterns

### Test Message Sending

```typescript
describe("sendAppointmentReminder", () => {
  it("sends template message via WhatsApp API", async () => {
    const mockResponse = {
      data: {
        messaging_product: "whatsapp",
        contacts: [{ wa_id: "974XXXXXXXX" }],
        messages: [{ id: "wamid.test123" }],
      },
    };

    jest.spyOn(axios, "post").mockResolvedValue(mockResponse);

    const result = await sendAppointmentReminder({
      to: "+974XXXXXXXX",
      patientName: "Ahmed",
      appointmentDate: new Date("2025-12-15T10:00:00Z"),
      doctorName: "Dr. Sarah",
      clinicName: "Doha Medical Center",
      language: "en",
    });

    expect(axios.post).toHaveBeenCalledWith(
      expect.stringContaining("/messages"),
      expect.objectContaining({
        messaging_product: "whatsapp",
        to: expect.any(String),
        type: "template",
        template: expect.objectContaining({
          name: "appointment_reminder",
        }),
      }),
      expect.any(Object),
    );

    // Verify message saved to database
    const savedMessage = await db.whatsappMessage.findFirst({
      where: { messageId: "wamid.test123" },
    });
    expect(savedMessage).toBeTruthy();
  });
});
```

### Test Webhook Handler

```typescript
describe("POST /api/webhooks/whatsapp", () => {
  it("handles incoming message webhook", async () => {
    const mockWebhook = {
      object: "whatsapp_business_account",
      entry: [
        {
          changes: [
            {
              field: "messages",
              value: {
                messaging_product: "whatsapp",
                metadata: { phone_number_id: PHONE_NUMBER_ID },
                messages: [
                  {
                    id: "wamid.incoming123",
                    from: "974XXXXXXXX",
                    timestamp: "1702297200",
                    type: "text",
                    text: { body: "Confirm" },
                  },
                ],
              },
            },
          ],
        },
      ],
    };

    const response = await POST(
      new Request("http://localhost/api/webhooks/whatsapp", {
        method: "POST",
        body: JSON.stringify(mockWebhook),
      }),
    );

    expect(response.status).toBe(200);

    // Verify message saved
    const message = await db.whatsappMessage.findFirst({
      where: { messageId: "wamid.incoming123" },
    });
    expect(message?.direction).toBe("incoming");
    expect(message?.content).toBe("Confirm");
  });
});
```

## Phone Number Formatting

### GCC Phone Numbers

```typescript
// Format phone numbers for WhatsApp API
function formatWhatsAppNumber(phone: string): string {
  // Remove all non-digits
  let cleaned = phone.replace(/\D/g, "");

  // Add country code if missing
  const GCC_COUNTRY_CODES = ["974", "966", "971", "965", "968", "973"];

  if (!GCC_COUNTRY_CODES.some((code) => cleaned.startsWith(code))) {
    // Default to Qatar (+974) if no country code
    cleaned = "974" + cleaned;
  }

  return cleaned;
}

// Examples:
formatWhatsAppNumber("+974 5555 1234"); // → '97455551234'
formatWhatsAppNumber("0555 512 34"); // → '974555512' (adds +974)
formatWhatsAppNumber("966501234567"); // → '966501234567' (Saudi)
```

### Validate Phone Number

```typescript
function isValidGCCPhone(phone: string): boolean {
  const cleaned = phone.replace(/\D/g, "");

  const patterns = {
    qatar: /^974[3-7]\d{7}$/, // Qatar: 974 + 3-7 + 7 digits
    saudi: /^966[5][0-9]{8}$/, // Saudi: 966 + 5 + 8 digits
    uae: /^971[5][0-9]{8}$/, // UAE: 971 + 5 + 8 digits
    kuwait: /^965[569]\d{7}$/, // Kuwait: 965 + 5/6/9 + 7 digits
    oman: /^968[79]\d{7}$/, // Oman: 968 + 7/9 + 7 digits
    bahrain: /^973[3-6]\d{7}$/, // Bahrain: 973 + 3-6 + 7 digits
  };

  return Object.values(patterns).some((pattern) => pattern.test(cleaned));
}
```

## Bilingual Message Support

### Using i18n-management Skill

```typescript
import { getTranslation } from "@/lib/i18n";

async function sendBilingualWhatsApp({
  to,
  templateKey,
  language,
  data,
}: {
  to: string;
  templateKey: string;
  language: "en" | "ar";
  data: Record<string, any>;
}) {
  // Get localized template name
  const templateName =
    language === "ar" ? `${templateKey}_ar` : `${templateKey}_en`;

  // Get localized parameter values
  const localizedData = Object.entries(data).map(([key, value]) => ({
    type: "text",
    text: typeof value === "string" ? getTranslation(value, language) : value,
  }));

  await sendTemplateMessage({
    to,
    templateName,
    language,
    components: [{ type: "body", parameters: localizedData }],
  });
}
```

## Common Use Cases

### 1. Appointment Confirmation

```typescript
// After appointment booked
await sendTemplateMessage({
  to: patient.phoneNumber,
  templateName: "appointment_confirmation",
  language: patient.preferredLanguage,
  components: [
    {
      type: "body",
      parameters: [
        { type: "text", text: patient.name },
        { type: "text", text: formatDateTime(appointment.dateTime) },
        { type: "text", text: appointment.doctor.name },
        { type: "text", text: clinic.name },
      ],
    },
  ],
});
```

### 2. Appointment Reminder (24h before)

```typescript
// Cron job runs daily
async function sendDailyReminders() {
  const tomorrow = new Date();
  tomorrow.setDate(tomorrow.getDate() + 1);

  const appointments = await db.appointment.findMany({
    where: {
      dateTime: {
        gte: tomorrow,
        lt: new Date(tomorrow.getTime() + 24 * 60 * 60 * 1000),
      },
      status: "confirmed",
      reminderSent: false,
    },
    include: { patient: true, doctor: true, clinic: true },
  });

  for (const apt of appointments) {
    await sendAppointmentReminder({
      to: apt.patient.phoneNumber,
      patientName: apt.patient.name,
      appointmentDate: apt.dateTime,
      doctorName: apt.doctor.name,
      clinicName: apt.clinic.name,
      language: apt.patient.preferredLanguage,
    });

    await db.appointment.update({
      where: { id: apt.id },
      data: { reminderSent: true },
    });
  }
}
```

### 3. Prescription Sharing

```typescript
// Send prescription image to patient
async function sendPrescription(appointmentId: string) {
  const appointment = await db.appointment.findUnique({
    where: { id: appointmentId },
    include: { patient: true, prescription: true },
  });

  if (!appointment?.prescription?.imageUrl) {
    throw new Error("No prescription available");
  }

  await sendImage({
    to: appointment.patient.phoneNumber,
    imageUrl: appointment.prescription.imageUrl,
    caption: `Prescription from Dr. ${appointment.doctor.name}`,
  });
}
```

## WhatsApp Integration Checklist

### Setup

- [ ] Register WhatsApp Business Account in Meta Business Manager
- [ ] Create WhatsApp Business App
- [ ] Get Phone Number ID and Access Token
- [ ] Configure webhook URL (must be HTTPS, publicly accessible)
- [ ] Verify webhook with Meta's challenge
- [ ] Subscribe to message and status webhooks
- [ ] Store credentials in environment variables

### Message Templates

- [ ] Submit templates for approval in Meta Business Manager
- [ ] Create English templates
- [ ] Create Arabic templates
- [ ] Include required category (APPOINTMENT_UPDATE, ACCOUNT_UPDATE, etc.)
- [ ] Test templates in sandbox before production
- [ ] Handle template rejection (resubmit with modifications)

### Sending Messages

- [ ] Format phone numbers correctly (remove non-digits, add country code)
- [ ] Validate phone numbers before sending
- [ ] Use approved templates for business-initiated messages
- [ ] Include all required template parameters
- [ ] Handle rate limiting (queue messages if needed)
- [ ] Store message ID for tracking
- [ ] Implement retry logic for failed messages

### Receiving Messages

- [ ] Verify webhook signature (if applicable)
- [ ] Handle incoming text messages
- [ ] Handle button/quick reply responses
- [ ] Download and store media (images, documents)
- [ ] Update conversation state
- [ ] Send appropriate responses
- [ ] Forward complex queries to staff
- [ ] Mark messages as read/processed

### Status Tracking

- [ ] Handle 'sent' status updates
- [ ] Handle 'delivered' status updates
- [ ] Handle 'read' status updates
- [ ] Handle 'failed' status updates with error details
- [ ] Store timestamps for each status
- [ ] Alert on repeated failures

## Security Best Practices

### 1. Validate Webhook Source

```typescript
// Verify webhook comes from Meta
const WEBHOOK_SECRET = process.env.WHATSAPP_WEBHOOK_SECRET!;

export async function POST(request: Request) {
  const signature = request.headers.get("x-hub-signature-256");
  const body = await request.text();

  // Verify signature
  const expectedSignature = crypto
    .createHmac("sha256", WEBHOOK_SECRET)
    .update(body)
    .digest("hex");

  if (signature !== `sha256=${expectedSignature}`) {
    return Response.json({ error: "Invalid signature" }, { status: 401 });
  }

  // Process webhook...
}
```

### 2. Rate Limit Incoming Messages

```typescript
import { rateLimit } from "@/lib/rate-limit";

const limiter = rateLimit({
  interval: 60 * 1000, // 1 minute
  uniqueTokenPerInterval: 500,
});

// Limit messages per phone number
try {
  await limiter.check(from, 10); // Max 10 messages per minute
} catch {
  console.warn(`Rate limit exceeded for ${from}`);
  return Response.json({ error: "Too many messages" }, { status: 429 });
}
```

### 3. Sanitize User Input

```typescript
// Prevent injection attacks in conversation flows
function sanitizeInput(text: string): string {
  return text
    .replace(/[<>]/g, "") // Remove HTML
    .replace(/[\r\n]+/g, " ") // Single line
    .trim()
    .slice(0, 1000); // Max length
}
```

## Common Issues & Solutions

### Issue 1: Template Not Found

**Error**: `Template name is invalid`

**Solutions**:

1. Check template is approved in Meta Business Manager
2. Verify template name matches exactly (case-sensitive)
3. Check language code matches (`en` vs `en_US`)
4. Ensure template is enabled for your phone number

### Issue 2: Message Not Delivered

**Symptoms**: Message status stays 'sent', never 'delivered'

**Solutions**:

1. Verify recipient phone number is valid WhatsApp account
2. Check recipient hasn't blocked your business number
3. Ensure message sent within 24-hour customer service window
4. Use template messages for business-initiated contacts

### Issue 3: Media Upload Fails

**Error**: `Media upload failed`

**Solutions**:

1. Ensure media URL is HTTPS (not HTTP)
2. Check media is publicly accessible (not localhost)
3. Verify file size under 16MB (images), 100MB (documents)
4. Check MIME type is supported

## Integration with Other Skills

### With email-composer

```typescript
// Fallback to email if WhatsApp fails
try {
  await sendWhatsAppReminder(appointment);
} catch (error) {
  console.error("WhatsApp failed, falling back to email");
  await sendEmailReminder(appointment);
}
```

### With i18n-management

```typescript
// Use translation keys for bilingual support
const messageText = getTranslation("appointment.reminder.title", language);
```

## See Also

- [api-development](../api-development/SKILL.md) - Webhook endpoint patterns
- [i18n-management](../i18n-management/SKILL.md) - Arabic/English handling
- [email-composer](../email-composer/SKILL.md) - Fallback email notifications
- [error-handling](../error-handling/SKILL.md) - Error handling strategies
