PayPerWA API Documentation
Send WhatsApp messages, manage contacts and templates, run campaigns, and receive delivery webhooks through the PayPerWA REST API. Authenticate with your API key and start sending in minutes — no BSP markup, just ₹0.20 per message + Meta's standard charges.
Getting Started
The PayPerWA API lets you send WhatsApp messages, manage contacts, templates, and campaigns programmatically. All endpoints use the /api/v1 prefix and require API key authentication.
Base URL
https://payperwa.com/api/v1Response Format
All responses follow a consistent JSON format:
// Success
{
"success": true,
"data": { ... }
}
// Error
{
"success": false,
"error": "Human-readable error message"
}Authentication
Include your API key in the Authorization header as a Bearer token with every request.
Authorization: Bearer ppw_live_sk_your_key_hereGenerate your API key in Dashboard → Settings → API tab. You can create up to 5 keys per account. Keys are shown only once at creation time.
Quick Example — Check Balance
curl https://payperwa.com/api/v1/balance \
-H "Authorization: Bearer ppw_live_sk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"Pick a language once — every other code sample on this page switches with you.
Rate Limits
Each API key has a configurable rate limit (default: 100 requests per minute). Exceeding this limit returns a 429 status code. Wait and retry after a few seconds.
| Plan | Rate Limit |
|---|---|
| Default | 100 requests/minute per key |
| Custom (configurable per key) | 10 -- 1,000 requests/minute |
Channels (Multi-WABA)
If your account has more than one connected WhatsApp Business Account (WABA) — for example, separate numbers for Sales and Support — every API call can be scoped to a specific channel. Pass the channel ID via the X-Channel-Id header. If omitted, the request runs against your primary channel.
How to specify a channel
Three options, in resolution order:
X-Channel-IdHTTP header (recommended)channelIdin the request body (POST/PUT only)?channelId=query parameter (GET only)
Where it applies
| Endpoint | Effect of X-Channel-Id |
|---|---|
POST /messages/send | Sends from that channel's phone number; template must be approved on it |
GET /contacts | Filters list to contacts on that channel only |
POST /contacts | Creates / upserts the contact under that channel |
GET /templates | Filters templates to that channel |
GET /campaigns | Filters campaigns to that channel |
GET /balance | Not used — wallet is shared across all channels |
Example — Send from a specific channel
curl -X POST https://payperwa.com/api/v1/messages/send \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "X-Channel-Id: ch_550e8400e29b41d4a716446655440000" \
-H "Content-Type: application/json" \
-d '{
"to": "919876543210",
"template_name": "order_confirmation",
"variables": ["Rahul", "ORD-4521"]
}'Find your channel IDs in Dashboard → Settings → Channels. Sending an invalid or unauthorized channel ID returns 400 Invalid channelId.
Send Message
/api/v1/messages/sendSend a single WhatsApp message using an approved template. The cost (Meta fee + ₹0.20 platform fee) is deducted from your wallet balance.
Permission required: messages:send
Request Body
{
"to": "919876543210",
"template_name": "order_confirmation",
"language": "en",
"variables": ["Rahul", "ORD-4521", "₹1,299"]
}| Field | Type | Required | Description |
|---|---|---|---|
to | string | Yes | Phone number with country code (e.g. 919876543210) |
template_name | string | Yes | Name of your approved template |
language | string | No | Template language code (default: "en") |
variables | string[] | No | Template variable values in order |
Response
{
"success": true,
"data": {
"message_id": "wamid.HBgLMTIzNDU2Nzg5MA==",
"status": "sent",
"cost": {
"meta_fee": 0.86,
"platform_fee": 0.20,
"total": 1.06
}
}
}Example
curl -X POST https://payperwa.com/api/v1/messages/send \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "919876543210",
"template_name": "order_confirmation",
"language": "en",
"variables": ["Rahul", "ORD-4521", "₹1,299"]
}'Contacts
/api/v1/contactsList all contacts with optional pagination and search.
Permission: contacts:read
Query Parameters
| Param | Default | Description |
|---|---|---|
page | 1 | Page number |
pageSize | 50 | Items per page (max 100) |
search | - | Search by name or phone |
// Response
{
"success": true,
"data": {
"contacts": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Rahul Sharma",
"phone": "+919876543210",
"email": "[email protected]",
"optedIn": true,
"tags": ["customer", "delhi"],
"createdAt": "2026-01-15T10:30:00.000Z"
}
],
"total": 1250,
"page": 1,
"pageSize": 50
}
}Example
curl "https://payperwa.com/api/v1/contacts?page=1&pageSize=50" \
-H "Authorization: Bearer YOUR_API_KEY"/api/v1/contactsCreate a contact, or update it if the phone already exists on the same channel (upsert). Tags are auto-created if they don't exist. Returns 201 on create and 200 on update; the response includes a created boolean so you can branch.
Permission: contacts:write
Body Fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Contact display name (1-100 chars) |
phone | string | Yes | Phone number; auto-normalized to E.164 |
email | string | No | Optional email |
optedIn | boolean | No | Default true |
tags | string[] | No | Tag names. New names are auto-created on your account |
upsert | boolean | No | Default true. Set false to fail with 409 if the phone already exists |
tagsMode | string | No | "merge" (default) keeps existing tags and adds new ones; "replace" wipes existing tags first |
Soft-deleted contacts are reactivated automatically when the same phone is upserted again — no need to handle that case separately.
// Request — create or update by phone, merge tags
{
"name": "Priya Patel",
"phone": "9123456789",
"email": "[email protected]",
"tags": ["lead", "mumbai"]
}
// Response — 201 Created (new) or 200 OK (updated)
{
"success": true,
"created": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440001",
"name": "Priya Patel",
"phone": "+919123456789",
"email": "[email protected]",
"optedIn": true,
"tags": ["lead", "mumbai"],
"createdAt": "2026-03-20T14:00:00.000Z",
"updatedAt": "2026-03-20T14:00:00.000Z"
}
}Example — Create or update (upsert + merge tags)
# Default: upsert + merge tags
curl -X POST https://payperwa.com/api/v1/contacts \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Priya Patel","phone":"9123456789","tags":["lead","mumbai"]}'
# Replace tags entirely instead of merging
curl -X POST https://payperwa.com/api/v1/contacts \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Priya Patel","phone":"9123456789","tags":["customer"],"tagsMode":"replace"}'
# Strict create-only — return 409 if the phone already exists
curl -X POST https://payperwa.com/api/v1/contacts \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Priya Patel","phone":"9123456789","upsert":false}'/api/v1/contacts/{id}Get a single contact by ID with tags and groups.
/api/v1/contacts/{id}Update a contact. Send only the fields you want to change. Replacing tags replaces all existing tags.
// Request
{
"name": "Priya P.",
"tags": ["customer", "vip"]
}/api/v1/contacts/{id}Soft-delete a contact (can be restored by support).
Templates
/api/v1/templatesList your approved message templates. Filter by status or category.
Permission: templates:read
Query Parameters
| Param | Default | Description |
|---|---|---|
status | APPROVED | DRAFT, PENDING, APPROVED, REJECTED |
category | - | MARKETING, UTILITY, AUTHENTICATION |
// Response
{
"success": true,
"data": {
"templates": [
{
"id": "550e8400-e29b-41d4-a716-446655440002",
"name": "order_confirmation",
"category": "UTILITY",
"language": "en",
"status": "APPROVED",
"body": "Hi {{1}}, your order {{2}} of {{3}} has been confirmed!",
"header": null,
"footer": "Thank you for shopping with us",
"buttons": null,
"createdAt": "2026-02-01T12:00:00.000Z"
}
]
}
}Example
curl "https://payperwa.com/api/v1/templates?status=APPROVED" \
-H "Authorization: Bearer YOUR_API_KEY"Campaigns
/api/v1/campaignsList all campaigns with status and delivery stats.
Permission: campaigns:read
// Response
{
"success": true,
"data": {
"campaigns": [
{
"id": "550e8400-e29b-41d4-a716-446655440003",
"name": "Diwali Offer 2026",
"status": "COMPLETED",
"template": "promo_offer",
"templateCategory": "MARKETING",
"totalMessages": 5000,
"sentCount": 5000,
"deliveredCount": 4850,
"readCount": 3200,
"failedCount": 150,
"estimatedCost": "5300.00",
"actualCost": "5300.00",
"createdAt": "2026-03-10T09:00:00.000Z"
}
],
"total": 42,
"page": 1,
"pageSize": 20
}
}/api/v1/campaignsCreate and optionally send a campaign. Set send: true to immediately queue messages, or omit it to create as DRAFT.
Permission: campaigns:send
// Request
{
"name": "March Sale",
"templateId": "550e8400-e29b-41d4-a716-446655440002",
"groupIds": ["group-uuid-1"],
"variables": {
"1": "name",
"2": "20%",
"3": "MARCH20"
},
"send": true
}
// Response (201)
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440004",
"name": "March Sale",
"status": "SENDING",
"totalContacts": 5000,
"estimatedCost": {
"meta_fee": "4300.00",
"platform_fee": "1000.00",
"total": "5300.00"
},
"message": "Campaign queued. 5000 messages will be sent."
}
}Example
curl -X POST https://payperwa.com/api/v1/campaigns \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "March Sale",
"templateId": "your-template-uuid",
"groupIds": ["your-group-uuid"],
"variables": { "1": "name", "2": "20%", "3": "MARCH20" },
"send": true
}'Wallet / Balance
/api/v1/balanceCheck your current wallet balance. All amounts in INR.
Permission: balance:read
// Response
{
"success": true,
"data": {
"balance": 4520.60,
"currency": "INR"
}
}Example
curl https://payperwa.com/api/v1/balance \
-H "Authorization: Bearer YOUR_API_KEY"Webhooks
PayPerWA can send delivery status callbacks to your server whenever a message status changes (sent, delivered, read, or failed). Configure your webhook URL in Dashboard → Settings.
Webhook Payload
{
"event": "message.status",
"message_id": "wamid.HBgLMTIzNDU2Nzg5MA==",
"status": "delivered",
"timestamp": "2026-03-20T14:35:00Z",
"recipient": "919876543210",
"campaign_id": "camp_abc123"
}Status Values
| Status | Description |
|---|---|
sent | Message sent to WhatsApp servers |
delivered | Message delivered to recipient's device |
read | Recipient read the message |
failed | Message could not be delivered (wallet refunded) |
Verifying Webhooks
Each webhook request includes an X-PayPerWA-Signature header. Verify it using your webhook secret (available in Settings) with HMAC-SHA256.
const crypto = require("crypto");
function verifyWebhook(body, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(JSON.stringify(body))
.digest("hex");
return signature === expected;
}Error Codes
All errors follow a consistent format:
{
"success": false,
"error": "Human-readable error message"
}| Code | Status | Description |
|---|---|---|
400 | Bad Request | Invalid request body or missing required fields |
401 | Unauthorized | Invalid, missing, expired, or deactivated API key |
402 | Payment Required | Insufficient wallet balance. Recharge at Dashboard → Billing |
403 | Forbidden | API key lacks the required permission for this endpoint |
404 | Not Found | Resource not found (contact, template, or campaign) |
409 | Conflict | Duplicate resource (e.g. contact with same phone number) |
429 | Too Many Requests | Rate limit exceeded. Wait and retry. |
500 | Server Error | Internal server error. Contact support if this persists. |
Permissions
Each API key can be scoped with specific permissions. New keys are created with all permissions by default. You can restrict them when creating or editing a key.
| Permission | Grants Access To |
|---|---|
contacts:read | GET /api/v1/contacts, GET /api/v1/contacts/:id |
contacts:write | POST /api/v1/contacts, PUT /api/v1/contacts/:id, DELETE /api/v1/contacts/:id |
templates:read | GET /api/v1/templates |
messages:send | POST /api/v1/messages/send |
campaigns:read | GET /api/v1/campaigns |
campaigns:send | POST /api/v1/campaigns (create + send) |
balance:read | GET /api/v1/balance |
Ready to Integrate?
Sign up, grab your API key, and start sending WhatsApp messages in minutes.
Get Your API Key