# WhatsApp Business API — Full Setup Tutorial

## Overview

El-Captain uses the **Meta Cloud API** (free hosting, pay per conversation) for all customer notifications. No SMS. No third-party BSPs.

**End result:** Your Laravel app sends templated WhatsApp messages to parents/admins via a simple HTTP POST.

---

## Prerequisites

- Facebook personal account (yours)
- A new SIM card (dedicated number for API — can't use WhatsApp app on it after)
- Egyptian commercial registration or tax ID (for business verification)
- Credit card or PayPal for Meta billing (pay-as-you-go, no minimum)

---

## Step 1: Create Meta Business Account

1. Go to https://business.facebook.com
2. Click "Create Account"
3. Fill in:
   - Business name: `El-Captain Sports Management` (or the academy's name if per-customer)
   - Your name
   - Business email
4. Confirm email
5. You now have a Business Manager ID

---

## Step 2: Create a Meta App

1. Go to https://developers.facebook.com
2. Click "My Apps" → "Create App"
3. Select **"Other"** use case
4. Select **"Business"** app type
5. App name: `El-Captain WhatsApp`
6. Connect to your Business Account from Step 1
7. On the app dashboard, click **"Add Product"** → find **WhatsApp** → click **"Set Up"**

---

## Step 3: Add Phone Number

1. In the WhatsApp section of your app, go to **"Getting Started"**
2. Meta gives you a **test number** — good for development (sends to 5 verified numbers only)
3. For production: click **"Add Phone Number"**
4. Enter your dedicated number (the new SIM)
5. Verify via SMS or voice call to that SIM
6. Choose a display name (this is what recipients see): e.g. `El-Captain` or the academy name
7. Display name goes through Meta review (24-48h typically)

**Important:**
- Once registered, that number is locked to the API. The WhatsApp app will stop working on it.
- Use a clean number with no prior WhatsApp history for fastest approval.
- The display name must match your business or be clearly associated with it.

---

## Step 4: Business Verification

Meta requires business verification before you can send messages to non-test numbers.

1. Go to Business Manager → **Settings** → **Security Center** → **Start Verification**
2. Upload ONE of:
   - Commercial Registration (السجل التجاري)
   - Tax Card (البطاقة الضريبية)
   - Utility bill with business name + address
3. Enter business details exactly as they appear on the document
4. Meta reviews in 2-5 business days
5. Status shows "Verified" when approved

**If rejected:** Usually a name mismatch. Business name in Meta must match the document exactly (including Arabic/English).

---

## Step 5: Generate Permanent Access Token

The "Getting Started" page gives you a temporary token (expires in 24h). For production:

1. Go to Business Manager → **Settings** → **Users** → **System Users**
2. Click "Add" → name it `whatsapp-api` → set role to **Admin**
3. Click on the system user → **"Generate Token"**
4. Select your WhatsApp app
5. Check these permissions:
   - `whatsapp_business_management`
   - `whatsapp_business_messaging`
6. Click "Generate Token"
7. **Copy the token immediately** — it won't show again

Store in your `.env`:
```
WHATSAPP_TOKEN=your_permanent_token_here
WHATSAPP_PHONE_ID=your_phone_number_id
WHATSAPP_BUSINESS_ID=your_business_id
```

---

## Step 6: Create Message Templates

Templates are pre-approved message formats. Meta must approve each one before you can use it.

1. In the WhatsApp section → **"Message Templates"**
2. Click **"Create Template"**
3. Category: **Utility** (for transactional — cheaper than Marketing)
4. Name: `attendance_notification` (lowercase, underscores only)
5. Language: **Arabic**
6. Body:

### Template: attendance_notification
```
مرحباً، ابنكم {{1}} سجّل {{2}} في حصة {{3}} اليوم.
```
Variables: child name, status (حاضر/غايب/متأخر), group name

### Template: payment_reminder
```
تذكير: مبلغ {{1}} ج.م مستحق على حساب {{2}}.
آخر موعد للسداد: {{3}}.

للاستفسار تواصل معنا.
```
Variables: amount, child name, due date

### Template: enrollment_confirmed
```
تم تسجيل {{1}} في برنامج {{2}} بنجاح.
تاريخ البداية: {{3}}.

نتمنى له تجربة ممتعة!
```
Variables: child name, program name, start date

### Template: schedule_change
```
تنبيه: تم تغيير موعد حصة {{1}} من {{2}} إلى {{3}}.

نعتذر عن أي إزعاج.
```
Variables: group name, old time, new time

### Template: absence_alert
```
تنبيه: ابنكم {{1}} غاب عن حصة {{2}} اليوم {{3}}.

في حالة وجود عذر يرجى التواصل مع الإدارة.
```
Variables: child name, group name, date

### Template: payment_received
```
تم استلام مبلغ {{1}} ج.م من حساب {{2}} بنجاح.

رقم الإيصال: {{3}}
الرصيد المتبقي: {{4}} ج.م

شكراً لكم.
```
Variables: amount, child name, receipt number, remaining balance

---

**Submit each template.** Meta reviews within 24 hours usually. Status:
- **Approved** → ready to use
- **Rejected** → reason given, edit and resubmit
- **Pending** → still in review

**Common rejection reasons:**
- Template contains a URL (not allowed in utility templates without URL button)
- Placeholder text is too vague
- Missing example values in submission

**Tip:** When submitting, provide example values for each `{{N}}` — Meta uses these to understand context.

---

## Step 7: Set Up Webhook (Receive Messages + Delivery Status)

1. In your app → WhatsApp → **Configuration**
2. Callback URL: `https://your-app.caprover.al-arcade.com/api/whatsapp/webhook`
3. Verify Token: generate a random string, store it in `.env` as `WHATSAPP_VERIFY_TOKEN`
4. Subscribe to:
   - `messages` (incoming messages from parents)
   - `message_status` (sent/delivered/read receipts)

### Laravel webhook route:

```php
// routes/api.php
Route::get('whatsapp/webhook', [WhatsAppWebhookController::class, 'verify']);
Route::post('whatsapp/webhook', [WhatsAppWebhookController::class, 'handle']);
```

### Verification (GET request from Meta):

```php
public function verify(Request $request)
{
    $mode = $request->query('hub_mode');
    $token = $request->query('hub_verify_token');
    $challenge = $request->query('hub_challenge');

    if ($mode === 'subscribe' && $token === config('services.whatsapp.verify_token')) {
        return response($challenge, 200);
    }

    return response('Forbidden', 403);
}
```

### Handling incoming (POST):

```php
public function handle(Request $request)
{
    $payload = $request->all();

    // Message status updates (sent/delivered/read)
    $statuses = data_get($payload, 'entry.0.changes.0.value.statuses', []);
    foreach ($statuses as $status) {
        NotificationLog::where('external_id', $status['id'])
            ->update(['delivery_status' => $status['status']]);
    }

    // Incoming messages from parents
    $messages = data_get($payload, 'entry.0.changes.0.value.messages', []);
    foreach ($messages as $message) {
        // Store or route to support queue
    }

    return response('OK', 200);
}
```

---

## Step 8: Send Messages from Laravel

### Service Class:

```php
namespace App\Domain\Notification\Services;

use Illuminate\Support\Facades\Http;

class WhatsAppService
{
    private string $baseUrl;
    private string $token;

    public function __construct()
    {
        $phoneId = config('services.whatsapp.phone_id');
        $this->baseUrl = "https://graph.facebook.com/v21.0/{$phoneId}/messages";
        $this->token = config('services.whatsapp.token');
    }

    public function sendTemplate(
        string $to,
        string $templateName,
        array $parameters = [],
        string $language = 'ar'
    ): ?string {
        $components = [];

        if (!empty($parameters)) {
            $components[] = [
                'type' => 'body',
                'parameters' => array_map(
                    fn($value) => ['type' => 'text', 'text' => (string) $value],
                    $parameters
                ),
            ];
        }

        $response = Http::withToken($this->token)
            ->post($this->baseUrl, [
                'messaging_product' => 'whatsapp',
                'to' => $this->formatNumber($to),
                'type' => 'template',
                'template' => [
                    'name' => $templateName,
                    'language' => ['code' => $language],
                    'components' => $components,
                ],
            ]);

        if ($response->successful()) {
            return $response->json('messages.0.id');
        }

        logger()->error('WhatsApp send failed', [
            'to' => $to,
            'template' => $templateName,
            'error' => $response->json(),
        ]);

        return null;
    }

    private function formatNumber(string $phone): string
    {
        $phone = preg_replace('/[\s\-()]/', '', $phone);

        // 01012345678 → 201012345678
        if (str_starts_with($phone, '0')) {
            $phone = '20' . substr($phone, 1);
        }

        // +201012345678 → 201012345678
        $phone = ltrim($phone, '+');

        return $phone;
    }
}
```

### Config:

```php
// config/services.php
'whatsapp' => [
    'token' => env('WHATSAPP_TOKEN'),
    'phone_id' => env('WHATSAPP_PHONE_ID'),
    'business_id' => env('WHATSAPP_BUSINESS_ID'),
    'verify_token' => env('WHATSAPP_VERIFY_TOKEN'),
],
```

### Usage (from a Listener):

```php
class SendAttendanceNotification implements ShouldQueue
{
    public function handle(AttendanceMarked $event): void
    {
        $record = $event->attendanceRecord;
        $participant = $record->participant;
        $guardian = $participant->primaryGuardian;

        if (!$guardian || !$guardian->phone) {
            return;
        }

        $messageId = app(WhatsAppService::class)->sendTemplate(
            to: $guardian->phone,
            templateName: 'attendance_notification',
            parameters: [
                $participant->name_ar,
                $record->status->label(),
                $record->session->group->name_ar,
            ]
        );

        NotificationLog::create([
            'academy_id' => $participant->academy_id,
            'channel' => 'whatsapp',
            'recipient_phone' => $guardian->phone,
            'template_name' => 'attendance_notification',
            'external_id' => $messageId,
            'delivery_status' => $messageId ? 'sent' : 'failed',
        ]);
    }
}
```

---

## Step 9: Go Live Checklist

- [ ] Business verified (green checkmark in Business Manager)
- [ ] Phone number approved + display name live
- [ ] Permanent System User token generated
- [ ] At least 3 templates approved (attendance, payment_reminder, enrollment)
- [ ] Webhook URL registered and responding 200
- [ ] `.env` variables set on production (CapRover env vars)
- [ ] Test message sent and received successfully
- [ ] NotificationLog recording delivery statuses
- [ ] Rate limit handling (queue with throttle)

---

## Smart Notification Routing (CRITICAL for Cost Control)

**The #1 mistake:** WhatsApping "ابنك حضر" every session day. At scale this DESTROYS margins.

### Routing Rules

| Event | Channel | Logic |
|---|---|---|
| Attendance: PRESENT | In-app + push only | Routine — no WhatsApp needed |
| Attendance: ABSENT | WhatsApp | Urgent — parent must know |
| Attendance: LATE | WhatsApp | Parent needs to know |
| Payment due (3 days before) | WhatsApp | Drives collection |
| Payment overdue | WhatsApp | Urgent follow-up |
| Enrollment confirmed | WhatsApp | One-time confirmation |
| Schedule change | WhatsApp | Affects their plans |
| Session cancelled | WhatsApp | Immediate impact |
| Daily digest (opt-in) | WhatsApp | ONE message per parent/day MAX |
| General updates | In-app only | Not worth a conversation |

### Implementation in Laravel

```php
// In AttendanceMarkedListener — route by status
public function handle(AttendanceMarked $event): void
{
    $record = $event->attendanceRecord;
    
    // Only WhatsApp for PROBLEMS — not routine presence
    if (!in_array($record->status->value, ['absent', 'late', 'no_show'])) {
        // In-app notification only
        $this->createInAppNotification($record);
        return;
    }

    // Absence/Late → WhatsApp
    $this->sendWhatsAppAlert($record);
}
```

### Why This Works (Math)

A 300-player academy has ~220 unique parents. Average absence rate is 15%.

**Naive (every attendance):** 220 parents × 14 session days = 3,080 conversations = 3,388 EGP (38% of revenue) ❌

**Smart (absence-only):** 33 absences/day × 24 days + payments + other = 850 conversations = 935 EGP (10% of revenue) ✓

### Premium Add-on: Daily Summary

Some academies WANT "ابنك حضر" every day. Offer it as a toggle:

- Default: OFF (absence-only — free tier)
- Premium: ON (+200 EGP/month for academy, covers the extra conversations)
- Implementation: End-of-day cron batches all attendance into ONE WhatsApp per parent

---

## Rate Limits & Scaling

| Tier | Daily conversation limit | How to unlock |
|------|---:|---|
| Unverified | 250 | Default before business verification |
| Tier 1 | 1,000 | After business verification |
| Tier 2 | 10,000 | Send 1,000+ in 7 days with good quality |
| Tier 3 | 100,000 | Send 10,000+ in 7 days with good quality |
| Tier 4 | Unlimited | Send 100,000+ with good quality |

**Quality rating** = low block/report rate. Keep templates relevant and expected (absence-only strategy helps quality — parents never feel spammed).

### Platform-Wide Rate Limit Planning

| Platform size | Daily convos (peak) | Required Tier |
|---|---:|---|
| 36 customers (Scenario B) | ~200 | Tier 1 ✓ |
| 100 customers | ~600 | Tier 1 ✓ |
| 500 customers | ~3,000 | Tier 2 |
| 1000+ customers | ~6,000 | Tier 2 ✓ |

**Tier 1 (1,000/day) covers the platform until ~150 customers.** Auto-upgrades with sustained volume.

---

## Cost Summary (Smart Routing — Absence-Only)

| Customers | Avg convos/customer/month | Total convos | Monthly cost (EGP) |
|---:|---:|---:|---:|
| 10 (all small) | 63 | 630 | 693 |
| 36 (mixed) | 75 | 2,700 | 2,970 |
| 100 (mixed) | 80 | 8,000 | 8,800 |

### Cost Per Academy By Size (Smart Routing)

| Academy size | Convos/month | Cost (EGP) | Revenue | % of Revenue |
|---:|---:|---:|---:|---:|
| 50 players (Small) | 63 | 69 | 2,449 | 3% |
| 200 players (Medium) | 450 | 495 | 6,499 | 8% |
| 300 players | 850 | 935 | 8,999 | 10% |
| 500 players | 1,430 | 1,573 | 12,499 | 13% |
| 1000 players (Large) | 2,850 | 3,135 | 18,499 | 17% |

Formula: `conversations × 1.10 EGP` (utility rate). All sustainable.

---

## Billing Setup

1. Go to Business Manager → **Billing** → **Payment Settings**
2. Add credit card or PayPal
3. Set billing threshold (or monthly auto-charge)
4. Invoices come from Meta monthly

---

## Troubleshooting

| Problem | Solution |
|---|---|
| "Number not registered" error | Number must have WhatsApp app installed first, THEN register via API (it takes over) |
| Template rejected | Check: no URLs in body (use URL buttons instead), provide example values, category matches content |
| Messages not delivered | Check recipient has WhatsApp + internet. Check your quality rating isn't "Red" |
| Webhook not receiving | Verify HTTPS, valid SSL, responding 200 within 5 seconds |
| Token expired | System User tokens don't expire. If using temporary token, switch to System User. |
| Rate limited | Queue messages with delay. Upgrade tier by sustained volume. |
| "Re-engagement" error | Can't initiate conversation after 24h without a new template. That's expected — use template. |

---

## Multi-Tenant Considerations

For El-Captain (multi-tenant SaaS):

**Option A: Single number for all academies (SIMPLER)**
- One WhatsApp number: "El-Captain"
- Templates include academy name: "أكاديمية {{academy}} — ابنكم {{child}}..."
- Cheaper (one setup)
- Parents see "El-Captain" as sender

**Option B: Per-academy numbers (PREMIUM)**
- Each academy gets their own number + display name
- Sell as premium feature (setup fee: 500 EGP)
- Requires separate Meta Business verification per academy (complex)
- Better UX — parent sees "أكاديمية النجم" not "El-Captain"

**Recommendation:** Start with Option A. Move to Option B as a paid addon for Large tier.

---

## Environment Variables (.env)

```
WHATSAPP_TOKEN=EAAxxxxxxxxxxxxxxxxxxxxxxxx
WHATSAPP_PHONE_ID=1234567890123456
WHATSAPP_BUSINESS_ID=9876543210987654
WHATSAPP_VERIFY_TOKEN=random_string_for_webhook_verification
WHATSAPP_API_VERSION=v21.0
```
