Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/lerichardv/patolab-platform/llms.txt

Use this file to discover all available pages before exploring further.

In a clinical laboratory setting, timely communication dramatically improves the patient experience. PatoLab integrates with the WhatsApp Business API (Meta Cloud API) to automatically dispatch specimen status updates — such as finalization notifications with a direct report download link — to patients’ WhatsApp numbers. Because WhatsApp is the dominant messaging platform across Latin America, this integration reaches patients on the channel they check most frequently, reducing inbound calls and front-desk workload.

Prerequisites

Before configuring the integration, you will need:
  • A Meta Business account with the WhatsApp product enabled
  • A WhatsApp Business API app created in the Meta Developer Portal
  • A phone number ID associated with the WhatsApp Business account (found in the app’s WhatsApp > API Setup panel)
  • A permanent System User access token with whatsapp_business_messaging permission

Environment Variables

Add the following keys to your .env file. All WhatsApp-related variables are grouped under the # WhatsApp Service section:
.env
# WhatsApp Service
WHATSAPP_TOKEN=                    # Permanent System User access token from Meta
WHATSAPP_PHONE_NUMBER_ID=          # The numeric Phone Number ID from the API Setup panel
WHATSAPP_BUSINEESS_ACCOUNT_ID=     # Your WhatsApp Business Account (WABA) ID
WHATSAPP_VERSION=v23.0             # Graph API version (default: v23.0)
WHATSAPP_VERIFY_TOKEN=             # A secret string you choose for webhook verification
The variable WHATSAPP_BUSINEESS_ACCOUNT_ID contains a typo (BUSINEESS) — this is the exact key name used in the codebase and must be spelled this way in your .env to match the application configuration.
These values are consumed through Laravel’s config/services.php binding and are referenced in WhatsAppService as:
config('services.whatsapp.token')
config('services.whatsapp.version', 'v23.0')
config('services.whatsapp.phone_number_id')

WhatsAppService

The App\Services\WhatsAppService class wraps the Meta Cloud API and exposes three public methods.

sendText()

Sends a free-form text message to a phone number. This method can only be used within the 24-hour customer service window (i.e., after the patient has messaged the business number first).
app/Services/WhatsAppService.php
public function sendText(string $phone, string $message)
{
    $cleanPhone = preg_replace('/\D/', '', $phone);

    $response = Http::withToken(config('services.whatsapp.token'))
        ->post(
            'https://graph.facebook.com/'.config('services.whatsapp.version', 'v23.0').'/'.config('services.whatsapp.phone_number_id').'/messages',
            [
                'messaging_product' => 'whatsapp',
                'recipient_type' => 'individual',
                'to' => $cleanPhone,
                'type' => 'text',
                'text' => [
                    'preview_url' => false,
                    'body' => $message,
                ],
            ]
        );

    return $response->json();
}
Phone number cleaning: Before sending, the method strips all non-digit characters using preg_replace('/\D/', '', $phone). This means you can pass numbers in any common format (+504 9900-0000, (504) 99000000, etc.) and the service will normalize them to a digit-only string before submitting to the API.

sendTemplate()

Sends a pre-approved template message. Templates are required when initiating a conversation with a customer outside the 24-hour service window, or as the very first message sent to a customer who has never messaged the business.
app/Services/WhatsAppService.php
public function sendTemplate(string $phone, string $templateName, string $languageCode = 'en_US', array $components = [])
{
    $cleanPhone = preg_replace('/\D/', '', $phone);

    $payload = [
        'messaging_product' => 'whatsapp',
        'recipient_type' => 'individual',
        'to' => $cleanPhone,
        'type' => 'template',
        'template' => [
            'name' => $templateName,
            'language' => [
                'code' => $languageCode,
            ],
        ],
    ];

    if (! empty($components)) {
        $payload['template']['components'] = $components;
    }

    $response = Http::withToken(config('services.whatsapp.token'))
        ->post(
            'https://graph.facebook.com/'.config('services.whatsapp.version', 'v23.0').'/'.config('services.whatsapp.phone_number_id').'/messages',
            $payload
        );

    return $response->json();
}
The optional $components array lets you inject dynamic values into template placeholders (body variables, header media, URL button suffixes, etc.).

sendLinkButtonTemplate()

A convenience wrapper around sendTemplate() for the common pattern of sending a template that contains a body text variable and a dynamic URL button — for example, a “View your report” button that links to the patient’s unique report URL.
app/Services/WhatsAppService.php
public function sendLinkButtonTemplate(string $phone, string $templateName, string $languageCode, string $bodyText, string $buttonUrlSuffix)
{
    $components = [
        [
            'type' => 'body',
            'parameters' => [
                ['type' => 'text', 'text' => $bodyText],
            ],
        ],
        [
            'type' => 'button',
            'sub_type' => 'url',
            'index' => '0',
            'parameters' => [
                ['type' => 'text', 'text' => $buttonUrlSuffix],
            ],
        ],
    ];

    return $this->sendTemplate($phone, $templateName, $languageCode, $components);
}

Sending a Notification from Application Code

PatoLab automatically fires a WhatsApp notification when a specimen report is finalized (inside ReportEditorController::transitionState). You can also call the service directly from any controller or action:
use App\Services\WhatsAppService;

$whatsapp = new WhatsAppService();
$whatsapp->sendText('+50499900000', 'Your specimen result is ready.');
Or using the service container:
use App\Services\WhatsAppService;

$whatsapp = app(WhatsAppService::class);
$whatsapp->sendText($customer->phone, "Hello {$customer->name}, your report is ready.");
The finalization flow in ReportEditorController normalizes Honduran local numbers (8-digit) by prepending the country code 504:
$cleanPhone = preg_replace('/\D/', '', $phone);
if (strlen($cleanPhone) === 8) {
    $cleanPhone = '504' . $cleanPhone;
}
Phone numbers must be submitted to the API as digit-only strings in E.164 format — no dashes, spaces, parentheses, or leading +. For example, +504 9900-0000 must be passed as 50499000000. The preg_replace('/\D/', '', $phone) call in WhatsAppService handles this automatically, but verify your data source contains a correct country code before sending.
In non-production environments (APP_ENV !== 'production'), all outbound WhatsApp messages in the finalization flow are redirected to the test number defined in ReportEditorController. This prevents accidental messages to real patients during development and staging. Set APP_ENV=production only on your live server.

Webhook Setup

Meta requires a publicly accessible HTTPS endpoint to verify your webhook and forward incoming events. PatoLab exposes two routes in routes/api.php:
MethodEndpointHandlerPurpose
GET/api/whatsapp/webhookWhatsAppWebhookController@verifyMeta’s one-time verification challenge
POST/api/whatsapp/webhookWhatsAppWebhookController@handleIncomingIncoming messages and delivery status events

Verification Endpoint (GET)

When you register your webhook URL in the Meta Developer Portal, Meta sends a GET request with three query parameters: hub.mode, hub.verify_token, and hub.challenge. The controller checks that hub.mode === 'subscribe' and that hub.verify_token matches your WHATSAPP_VERIFY_TOKEN environment variable, then echoes back the hub.challenge value as plain text:
app/Http/Controllers/Api/WhatsAppWebhookController.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 === env('WHATSAPP_VERIFY_TOKEN')) {
        return response($challenge, 200)->header('Content-Type', 'text/plain');
    }

    return response('Unauthorized', 403);
}

Incoming Message Handler (POST)

Every message sent to your business number, and every delivery status update (sent, delivered, read, failed), is forwarded to the POST endpoint. The current implementation logs all incoming payloads for observability:
app/Http/Controllers/Api/WhatsAppWebhookController.php
public function handleIncoming(Request $request)
{
    $payload = $request->all();
    Log::info('WhatsApp Webhook Raw Payload:', $payload);

    // Handles status updates: sent, delivered, read, failed
    if (isset($payload['entry'][0]['changes'][0]['value']['statuses'])) {
        // ... logs each status event
    }

    // Handles inbound text messages from patients
    if (isset($payload['entry'][0]['changes'][0]['value']['messages'])) {
        // ... logs sender, message ID, type, and body
    }

    return response('EVENT_RECEIVED', 200);
}
The handler always returns 200 EVENT_RECEIVED so Meta does not retry the delivery.

Registering the Webhook in Meta Developer Portal

1

Open your App in Meta Developer Portal

Navigate to developers.facebook.com, select your app, then go to WhatsApp > Configuration.
2

Enter your Callback URL

In the Webhook section, click Edit and enter your publicly reachable URL:
https://your-domain.com/api/whatsapp/webhook
3

Enter your Verify Token

Paste the same value you set for WHATSAPP_VERIFY_TOKEN in your .env file.
4

Subscribe to webhook fields

Click Verify and Save. Once verified, subscribe to at minimum the messages webhook field to receive inbound messages and status updates.
Template messages must be created and approved in your Meta Business Manager (WhatsApp Manager > Message Templates) before they can be sent via sendTemplate() or sendLinkButtonTemplate(). Approval typically takes a few minutes to a few hours.
Sending a free-form text message with sendText() to a customer who has not messaged you within the last 24 hours will result in a 131047 error from the Meta API. Always use an approved template for business-initiated conversations.

Build docs developers (and LLMs) love