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-api

All endpoints are HTTPS. Requests and responses use JSON.

Quick start

Get your first OTP sent in under 10 minutes.

  1. 1

    Create a free account

    Go to msgx.thepistachio.tech/register and sign up. No credit card required.

  2. 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. 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. 4

    Generate your API key

    Dashboard → API Keys → Generate API Key. Copy the key — it's shown only once.

  5. 5

    Send your first OTP

    bash
    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
    }

Authentication

Every API request must include your API key in the X-API-Key header.

http
X-API-Key: pk_live_YOUR_KEY
Keep your key secret. Do not include it in frontend JavaScript or commit it to a public repo. Use environment variables on your server.

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

POST/otp/send

Sends an OTP code to a WhatsApp number using your approved AUTHENTICATION template. The code is injected into the {{1}} variable automatically.

ParameterTypeRequiredDescription
tostringYesRecipient phone number with country code, no + sign. E.g. 919876543210 for India.
codestringYesThe OTP code your system generated. 4–8 characters.
templateNamestringNoName of a specific approved AUTHENTICATION template to use. If omitted, the most recently approved one is used.
languagestringNoLanguage code — en, hi, or gu. Default: en. Picks the template matching this language.
bash
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

json
{
  "success": true,
  "messageId": "wam_AbCdEf123456",
  "to": "919876543210",
  "templateUsed": "otp_verification_en",
  "status": "sent",
  "timestamp": 1719820800000
}

Send in Hindi

bash
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

GET/otp/templates

Returns all your AUTHENTICATION category templates and their approval status.

bash
curl https://msgx.thepistachio.tech/api/public-api/otp/templates \
  -H "X-API-Key: pk_live_YOUR_KEY"
json
{
  "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

POST/messages/send

Sends 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).

ParameterTypeRequiredDescription
tostringYesRecipient phone number with country code, no + sign.
typestringYesMust be "text".
text.bodystringYesMessage content. Max 4096 characters.
replyTostringNoWhatsApp message ID to reply to (shows as a quoted message).
bash
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

POST/messages/send-template

Send 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.

ParameterTypeRequiredDescription
tostringYesRecipient phone number.
template.namestringYesExact name of an approved template.
template.languagestringYesLanguage code, e.g. en, hi, gu.
template.componentsarrayNoVariable values for header/body/buttons. See example below.
bash
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

POST/messages/send-media

Send an image, video, document, or audio file by URL.

ParameterTypeRequiredDescription
tostringYesRecipient phone number.
typestringYesimage | video | document | audio
media.urlstringYesPublicly accessible URL of the media file.
media.captionstringNoCaption shown below the media (image/video/document only).
media.filenamestringNoFilename shown for documents.
bash
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

POST/messages/send-interactive

Send a message with quick-reply buttons or a list menu. Must be within the 24-hour window.

bash
# 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

GET/messages/:messageId/status

Check the delivery status of a message using its ID returned by the send endpoint.

bash
curl https://msgx.thepistachio.tech/api/public-api/messages/wam_AbCdEf123456/status \
  -H "X-API-Key: pk_live_YOUR_KEY"
json
{
  "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 → PENDINGAPPROVED (usable) or REJECTED (see rejectionReason)

Meta review usually takes minutes for AUTHENTICATION templates, up to 24–48h for MARKETING/UTILITY.

List templates

GET/templates
ParameterTypeRequiredDescription
statusstringNoPENDING | APPROVED | REJECTED | ALL. Default: APPROVED.
pagenumberNoPage number. Default 1.
limitnumberNoResults per page. Max 100, default 20.
languagestringNoFilter by language code.
categorystringNoMARKETING | UTILITY | AUTHENTICATION
bash
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)

POST/templates

Submits 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.

ParameterTypeRequiredDescription
namestringYesLowercase letters, numbers, underscores only (e.g. order_confirmation). Unique per language.
languagestringYesMeta language code: en, en_US, hi, gu, ar, es, pt_BR…
categorystringYesMARKETING | UTILITY | AUTHENTICATION
componentsarrayYesMeta component objects: HEADER, BODY (required except OTP auth), FOOTER, BUTTONS.

Example — UTILITY template with variables

bash
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

bash
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 example values.
  • For OTP templates with a copy-code button, don't send body text — Meta auto-generates it.
HTTPErrorMeaning
400META_REJECTEDMeta refused the submission — message contains Meta's exact reason.
400INVALID_COMPONENTSMissing required BODY component.
400WHATSAPP_NOT_CONNECTEDNo WhatsApp Business Account connected yet.
409TEMPLATE_EXISTSSame 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.

bash
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

POST/media/upload

Useful 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.

bash
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" }
}
json
{
  "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.

bash
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" }
          ]
        }
      ]
    }
  }'
Result: one WhatsApp message with the PDF attached directly to the templated text — not a template followed by a separate document message.

Check approval status

GET/templates/:name

Poll 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.

bash
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:

text
https://msgx.thepistachio.tech/llms.txt

Example prompt for your AI tool:

text
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

GET/usage

Returns OTP and message counts for a given month, with a daily breakdown.

ParameterTypeRequiredDescription
monthstringNoMonth in YYYY-MM format. Default: current month.
bash
curl "https://msgx.thepistachio.tech/api/public-api/usage?month=2024-06" \
  -H "X-API-Key: pk_live_YOUR_KEY"
json
{
  "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

POST/webhooks/configure
bash
curl -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).

json
// 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)

javascript
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

GET/webhooks/logs
bash
curl "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:

json
{
  "success": false,
  "error": "ERROR_CODE",
  "message": "Human-readable description"
}
HTTPError codeMeaning & fix
401MISSING_API_KEY / INVALID_API_KEY / API_KEY_REVOKEDMissing, invalid, or revoked API key. Check the X-API-Key header.
400INVALID_PHONEPhone number format is invalid. Use digits only with country code, no + sign.
400NO_OTP_TEMPLATENo approved AUTHENTICATION template found. Create one via POST /templates or the dashboard.
400INVALID_TEMPLATETemplate not found or not yet approved by Meta.
400META_REJECTEDMeta refused a template submission — the message field contains Meta's reason.
400WHATSAPP_NOT_CONNECTEDNo WhatsApp Business Account connected to your account yet.
400MISSING_MEDIAPOST /media/upload called without a file or url field.
400UPLOAD_FAILEDMedia upload to Meta failed — details in message.
409TEMPLATE_EXISTSA template with the same name and language already exists.
500DELIVERY_FAILEDMeta's API rejected the message. Check your WhatsApp connection in Settings.
429RATE_LIMIT_EXCEEDEDOver 60 requests/minute. Wait retryAfter seconds and retry.
429DAILY_LIMIT_EXCEEDEDYour plan's daily message limit is reached. Resets at midnight IST.
404NOT_FOUNDThe 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

javascript
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

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
<?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

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 free

Questions? Email us at support@thepistachio.tech