@msgly/whatsapp
v0.2.2
Published
WhatsApp Cloud API adapter for Msgly
Maintainers
Readme
@msgly/whatsapp
WhatsApp Cloud API adapter for Msgly. Send and receive WhatsApp messages through the unified hub — text, all media types, interactive buttons, quick replies, reactions, and pre-approved templates. Zero classes, runs in Node, Next.js, and Edge runtimes.
Install
npm install @msgly/core @msgly/whatsappQuick start
import express from 'express';
import { createHub } from '@msgly/core';
import { createWhatsAppAdapter } from '@msgly/whatsapp';
const hub = createHub();
hub.register(
createWhatsAppAdapter({
phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
appSecret: process.env.META_APP_SECRET!,
verifyToken: process.env.META_VERIFY_TOKEN!,
}),
);
await hub.connect({ throwOnFailure: true });
hub.on('message', async (msg) => {
if (msg.content.type === 'text') {
await hub.send({
channel: 'whatsapp',
account: msg.account,
contact: msg.contact,
content: { type: 'text', text: `You said: ${msg.content.text}` },
});
}
});
const app = express();
app.use(express.json({ verify: (req, _r, buf) => ((req as any).rawBody = new Uint8Array(buf)) }));
const handlers = hub.createWebhookHandler();
app.get('/webhook/:channel', handlers.get);
app.post('/webhook/:channel', handlers.post);
app.listen(3000);Config
interface WhatsAppConfig {
phoneNumberId: string; // long numeric id from API Setup
accessToken: string; // temporary (24h) or System User token
appSecret: string; // from App Settings → Basic
verifyToken: string; // your chosen string for webhook handshake
apiBase?: string; // defaults to https://graph.facebook.com
apiVersion?: string; // defaults to v20.0
}Setup (20 minutes)
- Create a Meta App. Go to developers.facebook.com → My Apps → Create App → Business type.
- Add the WhatsApp product to your app → Set up.
- Copy test credentials from the API Setup tab:
- Phone number ID (long numeric, NOT the human phone) →
WHATSAPP_PHONE_NUMBER_ID - Temporary access token (24h) →
WHATSAPP_ACCESS_TOKEN
- Phone number ID (long numeric, NOT the human phone) →
- Get the App Secret. Settings → Basic → Show next to App Secret →
META_APP_SECRET. - Pick a verify token. Any random string →
META_VERIFY_TOKEN. - Add a test recipient. API Setup → "To" dropdown → Manage phone number list → add your personal WhatsApp number (max 5 in test mode).
- Subscribe the webhook. WhatsApp → Configuration tab:
- Callback URL:
<PUBLIC_URL>/webhook/whatsapp - Verify token: same as
META_VERIFY_TOKEN - Click Verify and Save (your server must be running)
- Webhook fields → Subscribe to
messages
- Callback URL:
- Test. Message the test number from your personal WhatsApp.
Production tokens. The 24-hour token works for testing only. For production, create a System User token: Business Settings → Users → System Users → create one → Generate Token with scopes
whatsapp_business_messagingandwhatsapp_business_management. System User tokens don't expire.
Capabilities
| Feature | Supported | | ------------- | --------- | | text | ✓ | | image | ✓ | | video | ✓ | | audio | ✓ | | file | ✓ | | location | ✓ | | buttons | ✓ (max 3, 20-char labels) | | quick replies | ✓ | | templates | ✓ | | reactions | ✓ | | typing | — |
The adapter silently truncates button counts and label lengths to fit Meta's limits.
The 24-hour window
WhatsApp's policy: free-form replies (text, media, interactive) only work within 24 hours of an inbound user message. Outside that window you must send a pre-approved template.
Templates are created and approved in Meta dashboard → WhatsApp → Message Templates. Approval usually takes minutes for transactional templates.
await hub.send({
channel: 'whatsapp',
account: { channel: 'whatsapp', channelAccountId: process.env.WHATSAPP_PHONE_NUMBER_ID! },
contact: { channel: 'whatsapp', channelUserId: '919999999999' },
content: {
type: 'template',
templateName: 'order_confirmation',
language: 'en',
variables: { '1': 'Udesh', '2': 'ORDER-12345' },
},
});Variable keys are positional — '1' maps to {{1}} in the template body.
Sending examples
Image
await hub.send({
channel: 'whatsapp',
account, contact,
content: {
type: 'image',
mediaRef: { kind: 'url', value: 'https://example.com/cat.png' },
caption: 'meow',
},
});WhatsApp requires the URL to be publicly accessible HTTPS, or you can upload first:
const adapter = hub.getAdapter('whatsapp');
const ref = await adapter.uploadMedia({
data: new Uint8Array(/* image bytes */),
mimeType: 'image/png',
});
await hub.send({
channel: 'whatsapp',
account, contact,
content: { type: 'image', mediaRef: ref, caption: 'meow' },
});MediaFile.data accepts Uint8Array | Blob | ReadableStream<Uint8Array> — pass whichever your environment naturally produces.
Interactive buttons
await hub.send({
channel: 'whatsapp',
account, contact,
content: {
type: 'interactive',
text: 'Confirm your order?',
buttons: [
{ id: 'confirm', label: 'Confirm' },
{ id: 'cancel', label: 'Cancel' },
],
},
});User taps a button → you receive a text message whose content.text equals the button's id.
Delivery receipts
WhatsApp delivers status updates (delivered/read/failed) as separate webhook events. The hub's standard webhook handler ignores these for hub.on('message') purposes — if you need granular delivery tracking, use adapter.parseStatuses(rawBody):
const adapter = hub.getAdapter('whatsapp') as WhatsAppAdapter;
const receipts = adapter.parseStatuses(req.body);
// [{ status: 'delivered', messageId: '...', timestamp: '...' }, ...]Common pitfalls
(#131047)re-engagement message: you're outside the 24-hour window. Use a template.(#131030)recipient phone not in allowed list: in test mode, recipients must be added under Manage phone number list. Business verification removes the limit.InvalidSignature: wrongappSecret, OR your Express setup isn't capturing the raw body. Theverifycallback inexpress.json()is essential.- Verify handshake fails:
META_VERIFY_TOKENmust match byte-for-byte between code and the form in the Meta dashboard. Server must be reachable when you click Verify. - Template send fails with
(#132001): template name or language code doesn't match an approved template. Templates are case-sensitive. - Token expired after 24h: replace the temporary access token with a System User token.
Documentation
Full setup walkthrough and multi-channel usage: https://github.com/AyushJain070401/msgly
License
MIT
