TypeScript
Send WhatsApp messages from Node or the browser with @pingmate/sdk, the axios-based TypeScript client.
@pingmate/sdk is an axios-based client that runs in Node, Webpack, and the browser. It ships both ESM and CommonJS builds with full type definitions, so types resolve automatically in TypeScript and the same import works in plain JavaScript.
Install
npm install @pingmate/sdkConfigure
Create a Configuration with your base URL and API key, then construct the API you need. Keep the key in an environment variable, never in source.
import { Configuration, MessagesApi, BulkApi } from '@pingmate/sdk';
const config = new Configuration({
basePath: 'https://pingmate.app',
apiKey: process.env.PINGMATE_API_KEY,
});
const messages = new MessagesApi(config);
const bulk = new BulkApi(config);apiKey is sent as the X-API-Key header on every request. basePath defaults to http://localhost, so set it explicitly to your PingMate host (or your white-label domain).
Examples
Every message type flows through the same sendMessage call. The message object is a discriminated union: set message_type and fill the fields for that variant. The examples below assume the messages client from above and a recipient const to = '919876543210' (E.164 digits, country code, no leading +).
Text
await messages.sendMessage({
to,
message: { message_type: 'text', text: 'Hello from the PingMate SDK 👋' },
});Reply in a thread
Set context_id to the ID of the message you are replying to.
await messages.sendMessage({
to,
message: {
message_type: 'text',
text: 'Yes, your order is on the way.',
context_id: 'wamid.HBgL...',
},
});Image with caption
attachment is a public URL or a media ID from /api/v1/media/upload.
await messages.sendMessage({
to,
message: {
message_type: 'image',
attachments: { type: 'image', attachment: 'https://picsum.photos/600/400' },
text: 'Photo of the day',
},
});Video with caption
await messages.sendMessage({
to,
message: {
message_type: 'video',
attachments: { type: 'video', attachment: 'https://example.com/demo.mp4' },
text: 'A 30-second product tour',
},
});Document
file_name sets the display name the recipient sees.
await messages.sendMessage({
to,
message: {
message_type: 'document',
attachments: {
type: 'document',
attachment: 'https://example.com/invoice.pdf',
file_name: 'Invoice-2026-001.pdf',
},
text: 'Here is your invoice',
},
});Location
await messages.sendMessage({
to,
message: {
message_type: 'location',
location: {
latitude: '19.0760',
longitude: '72.8777',
name: 'Mumbai Office',
address: 'Bandra Kurla Complex, Mumbai',
},
},
});Contact card
await messages.sendMessage({
to,
message: {
message_type: 'contact',
contact: {
name: 'PingMate Support',
phone_number: '919876500000',
organization: 'PingMate',
},
},
});Quick-reply buttons
Up to three tappable buttons. The button_payload comes back to your webhook on tap.
await messages.sendMessage({
to,
message: {
message_type: 'buttons',
text: 'How would you like to proceed?',
header_text: 'Order #12345',
footer_text: 'Reply within 24 hours',
buttons: [
{ button_type: 'text', button_text: 'Confirm', button_payload: 'confirm_order' },
{ button_type: 'text', button_text: 'Cancel', button_payload: 'cancel_order' },
],
},
});Call-to-action buttons
url and call buttons open a link or dial a number instead of replying.
await messages.sendMessage({
to,
message: {
message_type: 'buttons',
text: 'Your order has shipped.',
buttons: [
{ button_type: 'url', button_text: 'Track order', button_payload: 'https://example.com/track/12345' },
{ button_type: 'call', button_text: 'Call support', button_payload: '919876500000' },
],
},
});Interactive list
await messages.sendMessage({
to,
message: {
message_type: 'interactive_list',
text: 'Please select a service',
header_text: 'Our Services',
interactive_list: {
title: 'Select a service',
sections: [
{
title: 'Support',
rows: [
{ id: 'billing', title: 'Billing Support', description: 'Invoices and payments' },
{ id: 'technical', title: 'Technical Support', description: 'Product issues' },
],
},
{
title: 'Sales',
rows: [
{ id: 'demo', title: 'Request a Demo' },
{ id: 'pricing', title: 'Get Pricing' },
],
},
],
},
},
});Carousel
A swipeable row of cards, each with its own image, text, and buttons. Up to ten cards.
await messages.sendMessage({
to,
message: {
message_type: 'carousel',
text: "This week's bestsellers 🛍️",
cards: [
{
attachments: { type: 'image', attachment: 'https://example.com/serum.jpg' },
text: 'Aurora Serum, ₹1,299',
buttons: [
{ button_type: 'url', button_text: 'View', button_payload: 'https://example.com/p/serum' },
{ button_type: 'text', button_text: 'Add to cart', button_payload: 'add_serum' },
],
},
{
attachments: { type: 'image', attachment: 'https://example.com/lip-tint.jpg' },
text: 'Velvet Lip Tint, ₹699',
buttons: [
{ button_type: 'url', button_text: 'View', button_payload: 'https://example.com/p/lip-tint' },
{ button_type: 'text', button_text: 'Add to cart', button_payload: 'add_tint' },
],
},
],
},
});Template (basic)
Templates are the only message type you can send to open a new conversation. The template must be approved first.
await messages.sendMessage({
to,
message: {
message_type: 'template',
template_name: 'hello_world',
template_language: 'en_US',
},
});Template with media header and variables
header_variables and body_variables fill the {{1}}, {{2}} placeholders in order.
await messages.sendMessage({
to,
message: {
message_type: 'template',
template_name: 'order_complete',
template_language: 'en_US',
header_variables: ['ORD-12345'],
body_variables: ['John', '₹2,499', 'March 15, 2026'],
attachments: { type: 'image', attachment: 'https://example.com/order-confirmation.jpg' },
},
});Schedule for later
Add scheduleTime at the top level. The format is DD-MM-YYYY:HH-MM in IST, and the wallet is charged when you call, not when it sends. See Scheduling.
await messages.sendMessage({
to,
scheduleTime: '25-12-2026:09-30',
message: { message_type: 'text', text: 'Your festive offer is live.' },
});Bulk send
One message (commonly a template) to up to 50 recipients, deduplicated server-side.
await bulk.sendBulkMessage({
to: ['919876543210', '919876543211', '919876543212'],
message: {
message_type: 'template',
template_name: 'promotion_offer',
template_language: 'en_US',
body_variables: ['30%', 'March 31, 2026'],
},
});Workflows
These combine the calls above into the patterns you will actually ship.
Upload media, then send it
Upload large or reused files once. uploadMedia returns an id you reference from any media message instead of a public URL. Limits: images 5 MB, video and audio 16 MB, PDF 100 MB.
import { MediaApi } from '@pingmate/sdk';
import { openAsBlob } from 'node:fs';
const mediaApi = new MediaApi(config);
const blob = await openAsBlob('./catalog.pdf');
const { data: upload } = await mediaApi.uploadMedia(new File([blob], 'catalog.pdf'));
const mediaId = upload.data?.id;
await messages.sendMessage({
to,
message: {
message_type: 'document',
attachments: { type: 'document', attachment: mediaId!, file_name: 'Spring Catalog.pdf' },
text: 'Our spring catalog is here 📖',
},
});Bulk campaign with per-recipient results
sendBulkMessage returns one result per recipient. Each carries either an id (queued) or an error, so you can log or retry the failures.
const { data: res } = await bulk.sendBulkMessage({
to: ['919876543210', '919876543211', '919800000000'],
message: {
message_type: 'template',
template_name: 'promotion_offer',
template_language: 'en_US',
body_variables: ['30%', 'March 31, 2026'],
},
});
const results = res.data?.results ?? [];
for (const r of results) {
if (r.error) console.warn(`✗ ${r.to}: ${r.error}`);
}
console.log(`Queued ${results.filter((r) => !r.error).length} of ${results.length}`);Resilient sending
A send returns 402 Payment Required when the wallet is empty or the plan is inactive. Catch it and surface a clear action instead of crashing the job.
import { isAxiosError } from 'axios';
import type { SendRequest } from '@pingmate/sdk';
async function safeSend(req: SendRequest) {
try {
return await messages.sendMessage(req);
} catch (e) {
if (isAxiosError(e) && e.response?.status === 402) {
throw new Error('PingMate wallet is empty. Top up and retry.');
}
throw e;
}
}Reference
Every method and model is documented in the package's own docs/ directory, alongside the API reference.
Browser usage
In the browser the API key is exposed to anyone who opens devtools. Only call PingMate directly from the browser for low-risk reads, or proxy sends through your own server.
How is this guide?


