MsgX WhatsApp API
MsgX lets you send WhatsApp messages — OTPs, notifications, templates, media, and interactive messages — using your own WhatsApp Business number. You pay only Meta's standard rates. MsgX charges no platform fee.
WhatsApp OTP
Send one-time passwords via WhatsApp — higher delivery than SMS
Simple REST API
One POST call to send a message. JSON in, JSON out.
Webhooks
Get real-time delivery receipts and incoming message events
Base URL
https://msgx.thepistachio.tech/api/public-apiAll endpoints are HTTPS. Requests and responses use JSON.
Quick start
Get your first OTP sent in under 10 minutes.
- 1
Create a free account
Go to msgx.thepistachio.tech/register and sign up. No credit card required.
- 2
Connect your WhatsApp Business number
In the dashboard → Settings, click "Connect WhatsApp number". You'll go through Meta's Embedded Signup — takes ~5 minutes. You need a Facebook Business Account and a phone number not already on WhatsApp.
- 3
Get an OTP template approved
Go to OTP Templates → pick a pre-built template → click "Customise & use". Edit the message text, keeping {{1}} where the OTP code should appear. Submit — Meta approves AUTHENTICATION templates in ~5–15 minutes.
- 4
Generate your API key
Dashboard → API Keys → Generate API Key. Copy the key — it's shown only once.
- 5
Send your first OTP
bashcurl -X POST https://msgx.thepistachio.tech/api/public-api/otp/send \ -H "X-API-Key: pk_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"to":"919876543210","code":"482910"}' # Response { "success": true, "messageId": "wam_AbCdEf123456", "to": "919876543210", "templateUsed": "otp_verification_en", "status": "sent", "timestamp": 1719820800000 }
Authentication
Every API request must include your API key in the X-API-Key header.
X-API-Key: pk_live_YOUR_KEY
Key format: Keys start with pk_live_ for production or pk_test_ for test mode.
Key management: Generate and revoke keys in your dashboard → API Keys. You can have one active key at a time.
Unauthorized response: If your key is missing or invalid, you receive 401 Unauthorized with {"success":false,"error":"UNAUTHORIZED"}.
OTP API
The OTP API is the simplest way to send a verification code via WhatsApp. Your backend generates the code — MsgX delivers it through your approved WhatsApp template.
Send OTP
/otp/sendSends an OTP code to a WhatsApp number using your approved AUTHENTICATION template. The code is injected into the {{1}} variable automatically.
| Parameter | Type | Required | Description |
|---|---|---|---|
| to | string | Yes | Recipient phone number with country code, no + sign. E.g. 919876543210 for India. |
| code | string | Yes | The OTP code your system generated. 4–8 characters. |
| templateName | string | No | Name of a specific approved AUTHENTICATION template to use. If omitted, the most recently approved one is used. |
| language | string | No | Language code — en, hi, or gu. Default: en. Picks the template matching this language. |
curl -X POST https://msgx.thepistachio.tech/api/public-api/otp/send \
-H "X-API-Key: pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "919876543210",
"code": "482910"
}'Response
{
"success": true,
"messageId": "wam_AbCdEf123456",
"to": "919876543210",
"templateUsed": "otp_verification_en",
"status": "sent",
"timestamp": 1719820800000
}Send in Hindi
curl -X POST https://msgx.thepistachio.tech/api/public-api/otp/send \
-H "X-API-Key: pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "919876543210",
"code": "827461",
"language": "hi"
}'Common error: NO_OTP_TEMPLATE
You have no approved AUTHENTICATION template yet. Go to your dashboard → OTP Templates and get one approved first.
List OTP templates
/otp/templatesReturns all your AUTHENTICATION category templates and their approval status.
curl https://msgx.thepistachio.tech/api/public-api/otp/templates \ -H "X-API-Key: pk_live_YOUR_KEY"
{
"success": true,
"templates": [
{
"id": "uuid-...",
"name": "otp_verification_en",
"language": "en",
"status": "APPROVED",
"createdAt": "2024-06-01T10:00:00.000Z"
},
{
"id": "uuid-...",
"name": "my_custom_otp",
"language": "hi",
"status": "PENDING",
"createdAt": "2024-06-02T08:30:00.000Z"
}
]
}Messages API
Send any type of WhatsApp message — plain text, approved templates, images, documents, audio, or interactive buttons and lists.
Send text message
/messages/sendSends a plain text message. Only works within a 24-hour customer service window (the user must have messaged you first, or you must use a template to open the window).
| Parameter | Type | Required | Description |
|---|---|---|---|
| to | string | Yes | Recipient phone number with country code, no + sign. |
| type | string | Yes | Must be "text". |
| text.body | string | Yes | Message content. Max 4096 characters. |
| replyTo | string | No | WhatsApp message ID to reply to (shows as a quoted message). |
curl -X POST https://msgx.thepistachio.tech/api/public-api/messages/send \
-H "X-API-Key: pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "919876543210",
"type": "text",
"text": { "body": "Hello! Your order #1234 has been shipped." }
}'Send template message
/messages/send-templateSend an approved template message. Templates can be sent at any time — no 24-hour window restriction. Use this for proactive notifications, OTPs, order updates, etc.
| Parameter | Type | Required | Description |
|---|---|---|---|
| to | string | Yes | Recipient phone number. |
| template.name | string | Yes | Exact name of an approved template. |
| template.language | string | Yes | Language code, e.g. en, hi, gu. |
| template.components | array | No | Variable values for header/body/buttons. See example below. |
curl -X POST https://msgx.thepistachio.tech/api/public-api/messages/send-template \
-H "X-API-Key: pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "919876543210",
"template": {
"name": "order_shipped",
"language": "en",
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "ORD-9876" },
{ "type": "text", "text": "Blue Denim Jacket" }
]
}
]
}
}'Send media message
/messages/send-mediaSend an image, video, document, or audio file by URL.
| Parameter | Type | Required | Description |
|---|---|---|---|
| to | string | Yes | Recipient phone number. |
| type | string | Yes | image | video | document | audio |
| media.url | string | Yes | Publicly accessible URL of the media file. |
| media.caption | string | No | Caption shown below the media (image/video/document only). |
| media.filename | string | No | Filename shown for documents. |
curl -X POST https://msgx.thepistachio.tech/api/public-api/messages/send-media \
-H "X-API-Key: pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "919876543210",
"type": "image",
"media": {
"url": "https://example.com/invoice.pdf",
"caption": "Your invoice for June 2024"
}
}'Send interactive message
/messages/send-interactiveSend a message with quick-reply buttons or a list menu. Must be within the 24-hour window.
# Button message
curl -X POST https://msgx.thepistachio.tech/api/public-api/messages/send-interactive \
-H "X-API-Key: pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "919876543210",
"type": "button",
"body": { "text": "Did you receive your package?" },
"buttons": [
{ "id": "yes", "title": "Yes, received!" },
{ "id": "no", "title": "No, not yet" }
]
}'Get message status
/messages/:messageId/statusCheck the delivery status of a message using its ID returned by the send endpoint.
curl https://msgx.thepistachio.tech/api/public-api/messages/wam_AbCdEf123456/status \ -H "X-API-Key: pk_live_YOUR_KEY"
{
"success": true,
"messageId": "wam_AbCdEf123456",
"status": "read",
"to": "919876543210",
"sentAt": "2024-06-01T10:00:00.000Z",
"deliveredAt": "2024-06-01T10:00:03.000Z",
"readAt": "2024-06-01T10:01:12.000Z"
}Status values: sent delivered read failed
Templates API
Manage your WhatsApp message templates. Templates must be approved by Meta before they can be sent.
Template lifecycle
POST /templates → submitted to Meta → PENDING → APPROVED (usable) or REJECTED (see rejectionReason)
Meta review usually takes minutes for AUTHENTICATION templates, up to 24–48h for MARKETING/UTILITY.
List templates
/templates| Parameter | Type | Required | Description |
|---|---|---|---|
| status | string | No | PENDING | APPROVED | REJECTED | ALL. Default: APPROVED. |
| page | number | No | Page number. Default 1. |
| limit | number | No | Results per page. Max 100, default 20. |
| language | string | No | Filter by language code. |
| category | string | No | MARKETING | UTILITY | AUTHENTICATION |
curl "https://msgx.thepistachio.tech/api/public-api/templates?status=ALL&category=AUTHENTICATION" \ -H "X-API-Key: pk_live_YOUR_KEY"
Create template (submit to Meta — no login needed)
/templatesSubmits a new template to Meta for approval directly via API. The submission is synchronous — if Meta rejects the payload you get the exact reason immediately as META_REJECTED; if accepted, the template is created with status PENDING.
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Lowercase letters, numbers, underscores only (e.g. order_confirmation). Unique per language. |
| language | string | Yes | Meta language code: en, en_US, hi, gu, ar, es, pt_BR… |
| category | string | Yes | MARKETING | UTILITY | AUTHENTICATION |
| components | array | Yes | Meta component objects: HEADER, BODY (required except OTP auth), FOOTER, BUTTONS. |
Example — UTILITY template with variables
curl -X POST https://msgx.thepistachio.tech/api/public-api/templates \
-H "X-API-Key: pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "order_shipped_update",
"language": "en",
"category": "UTILITY",
"components": [
{ "type": "HEADER", "format": "TEXT", "text": "Order Update" },
{
"type": "BODY",
"text": "Hi {{1}}, your order {{2}} has been shipped and will arrive by {{3}}. Thank you for shopping with us.",
"example": { "body_text": [["Rahul", "#4521", "15 July"]] }
},
{ "type": "FOOTER", "text": "Reply here for any help" }
]
}'
# Response (201)
{
"success": true,
"template": {
"id": "f3a1c2d4-...",
"name": "order_shipped_update",
"language": "en",
"category": "UTILITY",
"status": "PENDING",
"metaTemplateId": "1234567890",
"createdAt": "2026-07-13T10:00:00.000Z"
}
}Example — AUTHENTICATION (OTP) template
curl -X POST https://msgx.thepistachio.tech/api/public-api/templates \
-H "X-API-Key: pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "login_otp",
"language": "en",
"category": "AUTHENTICATION",
"components": [
{ "type": "BODY", "add_security_recommendation": true },
{
"type": "BUTTONS",
"buttons": [{ "type": "OTP", "otp_type": "COPY_CODE", "text": "Copy Code" }]
}
]
}'Avoid Meta rejections — variable rules
- Variables must be sequential: {{1}}, {{2}} — no gaps.
- Don't start or end the body with a variable; don't place two variables side by side.
- Keep roughly 4+ static words per variable and provide
examplevalues. - For OTP templates with a copy-code button, don't send body text — Meta auto-generates it.
| HTTP | Error | Meaning |
|---|---|---|
| 400 | META_REJECTED | Meta refused the submission — message contains Meta's exact reason. |
| 400 | INVALID_COMPONENTS | Missing required BODY component. |
| 400 | WHATSAPP_NOT_CONNECTED | No WhatsApp Business Account connected yet. |
| 409 | TEMPLATE_EXISTS | Same name + language already exists (and is not rejected). |
Media headers — attach a PDF/image/video to the template
Meta requires an already-uploaded media handle for any HEADER with format: IMAGE | VIDEO | DOCUMENT. This is what makes the file render inline with the template text as a single WhatsApp bubble — e.g. sending a receipt or invoice PDF attached directly to the templated message, instead of a template plus a separate document message.
Option 1 — Give MsgX a public HTTPS URL, we upload it for you
Add example.header_url to the HEADER component. MsgX downloads the file server-side, uploads it to Meta, and submits the template with the resulting handle — no separate call needed.
curl -X POST https://msgx.thepistachio.tech/api/public-api/templates \
-H "X-API-Key: pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "invoice_receipt",
"language": "en",
"category": "UTILITY",
"components": [
{
"type": "HEADER",
"format": "DOCUMENT",
"example": { "header_url": "https://yourapp.com/files/invoice-sample.pdf", "header_filename": "invoice.pdf" }
},
{
"type": "BODY",
"text": "Hi {{1}}, please find your invoice {{2}} for {{3}} attached above. Thank you for your business.",
"example": { "body_text": [["Rahul", "#INV-4521", "₹4,500"]] }
}
]
}'Option 2 — Upload once via API, reuse the handle
/media/uploadUseful for creating several template language variants from the same sample file. Accepts a multipart file field, or a JSON body with a url. Max 100MB.
curl -X POST https://msgx.thepistachio.tech/api/public-api/media/upload \
-H "X-API-Key: pk_live_YOUR_KEY" \
-F "file=@invoice-sample.pdf" \
-F "type=document"
# Response
{
"success": true,
"media": { "handle": "4::abc123...", "type": "document" }
}{
"type": "HEADER",
"format": "DOCUMENT",
"example": { "header_handle": ["4::abc123..."] }
}Sending it — attach the real per-recipient file
The header example above is just a sample for Meta's reviewers. When you actually send the template (see Send template message), pass each recipient's real document as a header parameter — that's what attaches their specific PDF to the message bubble.
curl -X POST https://msgx.thepistachio.tech/api/public-api/messages/send-template \
-H "X-API-Key: pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "919876543210",
"template": {
"name": "invoice_receipt",
"language": "en",
"components": [
{
"type": "header",
"parameters": [{ "type": "document", "document": { "link": "https://yourapp.com/files/inv-4521.pdf", "filename": "invoice.pdf" } }]
},
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Rahul" },
{ "type": "text", "text": "#INV-4521" },
{ "type": "text", "text": "₹4,500" }
]
}
]
}
}'Check approval status
/templates/:namePoll this after creating a template (every 5–15 minutes) until status changes from PENDING. If REJECTED, rejectionReason explains why — fix and resubmit under a new name. Optional query: ?language=en.
curl https://msgx.thepistachio.tech/api/public-api/templates/order_shipped_update \
-H "X-API-Key: pk_live_YOUR_KEY"
# Response
{
"success": true,
"template": {
"id": "f3a1c2d4-...",
"name": "order_shipped_update",
"language": "en",
"category": "UTILITY",
"status": "APPROVED",
"rejectionReason": null,
"components": [ ... ],
"metaTemplateId": "1234567890"
}
}AI / LLM integration
Building your integration with an AI coding assistant (Claude, ChatGPT, Cursor, Copilot)? Give it this single URL — it contains the complete API reference in plain markdown that LLMs parse perfectly:
https://msgx.thepistachio.tech/llms.txt
Example prompt for your AI tool:
Read https://msgx.thepistachio.tech/llms.txt and integrate MsgX WhatsApp API into my app: create an order-update template, wait for approval, and send it to customers when their order ships. My API key is in env var MSGX_API_KEY.
A Postman collection is also available at /msgx-postman-collection.json.
Usage & Analytics
/usageReturns OTP and message counts for a given month, with a daily breakdown.
| Parameter | Type | Required | Description |
|---|---|---|---|
| month | string | No | Month in YYYY-MM format. Default: current month. |
curl "https://msgx.thepistachio.tech/api/public-api/usage?month=2024-06" \ -H "X-API-Key: pk_live_YOUR_KEY"
{
"success": true,
"usage": {
"month": "2024-06",
"otpSent": 1240,
"messagesSent": 340,
"messagesReceived": 89,
"templatesSent": 1240,
"failedRequests": 12,
"dailyBreakdown": [
{ "date": "2024-06-01", "otpSent": 87, "sent": 22, "received": 5 },
{ "date": "2024-06-02", "otpSent": 104, "sent": 31, "received": 8 }
]
}
}Webhooks
MsgX sends real-time events to your server when messages are delivered, read, or received. Configure your webhook URL from the dashboard.
Configure webhook
/webhooks/configurecurl -X POST https://msgx.thepistachio.tech/api/public-api/webhooks/configure \
-H "X-API-Key: pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/api/whatsapp-webhook",
"secret": "your_signing_secret",
"events": ["messages", "statuses"]
}'Webhook payload
MsgX sends a POST request to your URL. Verify the request using the X-Webhook-Signature header (HMAC-SHA256 of the raw body using your secret).
// Incoming message event
{
"type": "message.received",
"tenantId": "uuid-...",
"data": {
"from": "919876543210",
"messageId": "wam_InCoMiNg",
"text": "Hello!",
"timestamp": "2024-06-01T10:05:00.000Z"
}
}
// Delivery status event
{
"type": "message.delivered",
"tenantId": "uuid-...",
"data": {
"messageId": "wam_AbCdEf123456",
"status": "delivered",
"timestamp": "2024-06-01T10:00:03.000Z"
}
}Verifying the signature (Node.js)
const crypto = require('crypto');
function verifyWebhook(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express example
app.post('/api/whatsapp-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-webhook-signature'];
if (!verifyWebhook(req.body, sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Unauthorized');
}
const event = JSON.parse(req.body);
console.log('Event:', event.type, event.data);
res.status(200).send('OK');
});View webhook logs
/webhooks/logscurl "https://msgx.thepistachio.tech/api/public-api/webhooks/logs?status=failed&limit=10" \ -H "X-API-Key: pk_live_YOUR_KEY"
Error reference
All errors return a consistent JSON structure:
{
"success": false,
"error": "ERROR_CODE",
"message": "Human-readable description"
}| HTTP | Error code | Meaning & fix |
|---|---|---|
| 401 | MISSING_API_KEY / INVALID_API_KEY / API_KEY_REVOKED | Missing, invalid, or revoked API key. Check the X-API-Key header. |
| 400 | INVALID_PHONE | Phone number format is invalid. Use digits only with country code, no + sign. |
| 400 | NO_OTP_TEMPLATE | No approved AUTHENTICATION template found. Create one via POST /templates or the dashboard. |
| 400 | INVALID_TEMPLATE | Template not found or not yet approved by Meta. |
| 400 | META_REJECTED | Meta refused a template submission — the message field contains Meta's reason. |
| 400 | WHATSAPP_NOT_CONNECTED | No WhatsApp Business Account connected to your account yet. |
| 400 | MISSING_MEDIA | POST /media/upload called without a file or url field. |
| 400 | UPLOAD_FAILED | Media upload to Meta failed — details in message. |
| 409 | TEMPLATE_EXISTS | A template with the same name and language already exists. |
| 500 | DELIVERY_FAILED | Meta's API rejected the message. Check your WhatsApp connection in Settings. |
| 429 | RATE_LIMIT_EXCEEDED | Over 60 requests/minute. Wait retryAfter seconds and retry. |
| 429 | DAILY_LIMIT_EXCEEDED | Your plan's daily message limit is reached. Resets at midnight IST. |
| 404 | NOT_FOUND | The requested resource (message, template, delivery log) was not found. |
Code examples
Full integration examples for common languages. Copy and replace pk_live_YOUR_KEY with your actual key.
Node.js / TypeScript
const axios = require('axios');
const MSGX_KEY = process.env.MSGX_API_KEY; // pk_live_...
const BASE = 'https://msgx.thepistachio.tech/api/public-api';
async function sendOtp(phone, code) {
const { data } = await axios.post(`${BASE}/otp/send`, {
to: phone, // e.g. '919876543210'
code: code // your generated OTP
}, {
headers: { 'X-API-Key': MSGX_KEY }
});
return data; // { success, messageId, status }
}
// Usage
const otp = Math.floor(100000 + Math.random() * 900000).toString();
// Store otp in your DB/Redis with TTL
await sendOtp('919876543210', otp);Python
import requests, random, os
MSGX_KEY = os.environ['MSGX_API_KEY'] # pk_live_...
BASE = 'https://msgx.thepistachio.tech/api/public-api'
def send_otp(phone: str, code: str) -> dict:
resp = requests.post(
f'{BASE}/otp/send',
json={'to': phone, 'code': code},
headers={'X-API-Key': MSGX_KEY},
timeout=10
)
resp.raise_for_status()
return resp.json()
# Usage
otp = str(random.randint(100000, 999999))
# Store otp in DB/Redis with TTL
send_otp('919876543210', otp)PHP
<?php
$key = getenv('MSGX_API_KEY'); // pk_live_...
$base = 'https://msgx.thepistachio.tech/api/public-api';
function sendOtp(string $phone, string $code): array {
global $key, $base;
$ch = curl_init("$base/otp/send");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['to' => $phone, 'code' => $code]),
CURLOPT_HTTPHEADER => [
'X-API-Key: ' . $key,
'Content-Type: application/json',
],
]);
$body = curl_exec($ch);
curl_close($ch);
return json_decode($body, true);
}
// Usage
$otp = strval(random_int(100000, 999999));
// Store $otp in DB/Redis with TTL
sendOtp('919876543210', $otp);Go
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
const base = "https://msgx.thepistachio.tech/api/public-api"
func sendOTP(phone, code string) error {
payload, _ := json.Marshal(map[string]string{"to": phone, "code": code})
req, _ := http.NewRequest("POST", base+"/otp/send", bytes.NewBuffer(payload))
req.Header.Set("X-API-Key", os.Getenv("MSGX_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { return err }
defer resp.Body.Close()
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
fmt.Println(result)
return nil
}
func main() {
_ = sendOTP("919876543210", "482910")
}Ready to start?
Create a free account — no credit card, no platform fee.
Get started freeQuestions? Email us at support@thepistachio.tech