WhatsApp Business API (Meta)
Category: Notification — Messaging
Package: @leadmetrics/provider-whatsapp → WhatsAppBusinessProvider
External SDK: axios (Meta Graph API v19.0)
Purpose
WhatsApp Business API enables template-based WhatsApp messages to clients and staff. The platform uses WhatsApp for high-priority notifications — HITL review requests, monthly report delivery, and budget alerts — where WhatsApp achieves much higher open rates than email or SMS.
Both the platform and tenants use the same Meta WhatsApp Business API. The platform has its own WhatsApp Business number for default notifications; tenants can configure their own verified business number so messages arrive from their brand identity.
Key Constraint: Template-Only Messaging
WhatsApp Business API does not allow arbitrary free-text messages. All outbound messages must use pre-approved message templates. Templates must be submitted to Meta for approval before use. The platform maintains a catalogue of approved templates; tenants using their own account must register the same templates with Meta.
Config Structure
Platform default (env vars)
WHATSAPP_API_KEY=EAAxxxxxxxx # Meta Graph API access token
WHATSAPP_API_BASE_URL=https://graph.facebook.com/v19.0
WHATSAPP_FROM=123456789012345 # Platform's verified WhatsApp Business phone number IDTenant config (stored in notification_providers.config, encrypted)
interface WhatsAppBusinessConfig {
apiKey: string; // Meta Graph API access token (permanent token from System User)
baseUrl: string; // "https://graph.facebook.com/v19.0"
phoneNumberId: string; // Phone Number ID from Meta Business Manager (not the phone number itself)
}How to get phoneNumberId
In Meta Business Manager → WhatsApp → Phone Numbers → select number → copy the “Phone Number ID” (a 15-digit numeric string). This is not the human-readable phone number.
Templates
Platform-registered templates
| Template name | Category | Variables |
|---|---|---|
hitl_review_request | UTILITY | {{1}} = activity title, {{2}} = review URL |
monthly_report_ready | UTILITY | {{1}} = client name, {{2}} = month, {{3}} = report URL |
budget_warning | UTILITY | {{1}} = credits remaining, {{2}} = percent |
credit_topup_confirmed | UTILITY | {{1}} = credits added, {{2}} = new balance |
welcome_onboarding | UTILITY | {{1}} = user name, {{2}} = portal URL |
All templates use the UTILITY category (transactional). Marketing templates require separate approval and cannot be sent without user opt-in.
Integration Pattern
Provider class (packages/provider-whatsapp/src/providers/whatsapp-business.ts)
import axios from 'axios';
class WhatsAppBusinessProvider implements WhatsAppProvider {
readonly name = 'whatsapp_business_api';
constructor(
private apiKey: string,
private baseUrl: string,
private phoneNumberId: string,
) {}
async sendTemplate(message: WhatsAppMessage): Promise<WhatsAppSendResult> {
const url = `${this.baseUrl}/${this.phoneNumberId}/messages`;
const body = {
messaging_product: 'whatsapp',
to: message.to, // E.164: "+919876543210"
type: 'template',
template: {
name: message.template.name,
language: { code: message.template.languageCode ?? 'en' },
components: message.template.components?.map(comp => ({
type: comp.type, // 'body' | 'header' | 'button'
parameters: comp.parameters.map(p => ({
type: 'text',
text: message.variables[p.key],
})),
})),
},
};
const response = await axios.post(url, body, {
headers: {
Authorization: `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
});
return {
messageId: response.data.messages[0].id,
provider: 'whatsapp_business_api',
status: response.data.messages[0].message_status,
};
}
async verify(): Promise<void> {
// Fetch phone number info to verify credentials and phone number ID
const url = `${this.baseUrl}/${this.phoneNumberId}`;
const response = await axios.get(url, {
headers: { Authorization: `Bearer ${this.apiKey}` },
});
if (!response.data.id) {
throw new Error('WhatsApp Business API verification failed — phone number not found');
}
}
}Template definition type
interface WhatsAppTemplateDefinition {
name: string; // e.g. "hitl_review_request"
languageCode?: string; // e.g. "en" (default), "hi", "ta"
components?: WhatsAppComponent[];
}
interface WhatsAppComponent {
type: 'body' | 'header' | 'button';
parameters: { key: string }[]; // keys into message.variables
}Templates are defined in apps/notifications/src/templates/whatsapp-templates.ts and referenced by the WhatsApp handler.
Test Cases
Unit tests (packages/provider-whatsapp/src/providers/whatsapp-business.test.ts)
| Test | Approach |
|---|---|
sendTemplate() POSTs to correct Graph API URL | Mock axios.post; assert URL contains phoneNumberId |
sendTemplate() sends correct Authorization header | Assert Bearer ${apiKey} |
sendTemplate() maps variables to template components | Assert parameters[0].text === variables['{{1}}'] |
sendTemplate() returns messageId from response | Mock { messages: [{ id: 'wamid.xxx' }] } |
sendTemplate() throws on 401 (invalid token) | Mock 401 response; assert error propagated |
sendTemplate() throws on unknown template name | Mock 400 (#131030); assert propagated |
verify() fetches phone number metadata | Mock axios.get; assert URL and header |
verify() throws when id absent | Mock response {}; assert throws |
Integration tests
| Test | Approach |
|---|---|
| Send template via Meta Graph API sandbox | Use Meta test phone number; send hello_world template; assert wamid returned |
| Platform default used when no tenant row | Seed no notification_providers row; assert WhatsAppBusinessProvider uses env vars |
| Tenant provider used when verified | Seed verified row; assert tenant phoneNumberId used |
Meta test numbers
Meta provides test business phone numbers in the Meta Developer Console (Apps → WhatsApp → Getting Started). Test numbers can send to up to 5 registered recipient numbers for free.
Related
- Telegram Bot Provider — alternative messaging channel for ops/internal
- Notification Packages —
@leadmetrics/provider-whatsappstructure - Notification Providers — resolution pattern
- Notification Channels — WhatsApp handler detail