SSISPL Panel
Sign inGet started

WhatsApp API reference

Send WhatsApp messages from your own software — school ERP, CRM or website.

Create an account

Getting started

  1. Sign in to your panel and create an API key under API & Webhooks. It is shown once.
  2. Create and get approval for the message templates you intend to send.
  3. Call the endpoints below with your key.

Base URL

https://msg.sisplerp.com/api/v1

Every request carries your key:

Authorization: Bearer sk_live_xxxxxxxxxxxxxxxx

X-API-Key: sk_live_… is accepted as an alternative. The key identifies your account and the number messages are sent from — there is no separate account, sender or licence id.

Templates vs free text

WhatsApp only allows free text within 24 hours of the customer's last message. Anything you initiate — fee reminders, attendance alerts, results — must use a template approved by Meta. Create templates under Templates; approval usually takes minutes.

Do not build a “passthrough” template

It is tempting to get one template approved like Dear Parent, {{1}} and push whatever text you want through the variable. This breaks WhatsApp's Business Messaging Policy: template review exists so Meta knows what is being sent, and a variable carrying the whole message defeats it. Templates that are mostly variable are rejected, and ones that slip through get flagged later — after you have built your software around them. The cost lands on your number's quality rating.

Create a real template per message type instead — fee due, exam schedule, holiday notice, attendance alert. They are free, approve in minutes, and your code still just picks a name and supplies variables.

Endpoints

POST/messages

Send one message.

bash
curl -X POST https://msg.sisplerp.com/api/v1/messages \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "9934849371",
    "template": "fee_receipt",
    "variables": ["Riya Sharma", "4500", "R-1042"]
  }'

Response

json
{
  "id": "cms3x...",
  "waMessageId": "wamid.HBgM...",
  "conversationId": "cms3y...",
  "status": "sent",
  "costPaise": 78
}

Keep id — delivery webhooks reference it. to accepts 9934849371, +91 99348 49371 or 09934849371; numbers are normalised for you. variables fill {{1}}, {{2}}… in order. Send "text" instead of "template" to reply inside the 24-hour window.

POST/messages — with a file or a link button

A template approved with a media header or a URL button only fixes the shape. The actual file and link are chosen per send, so pass them here.

json
{
  "to": "9934849371",
  "template": "fee_receipt",
  "variables": ["Riya Sharma", "4500"],
  "headerMediaUrl": "https://erp.yourschool.in/receipts/8891.pdf",
  "headerFileName": "Receipt-8891.pdf",
  "buttonUrlParam": "8891"
}
headerMediaUrlPDF, image or video for the header. Meta downloads this itself, so it must be publicly reachable — a link behind your firewall will fail.
headerMediaIdUse instead of the URL if you already uploaded the file to Meta.
headerFileNameFilename shown for a document header. Omit it and WhatsApp displays the raw URL, which looks broken to the recipient.
buttonUrlParamFills {{1}} in a dynamic URL button — e.g. a template button of https://erp.yourschool.in/receipt/{{1}} with "8891" here.

Put links in a URL button rather than a body variable. Meta reviews templates with URLs in the body far more harshly, and a button is what recipients expect to tap.

A URL button cannot change domain

buttonUrlParam replaces only the {{1}} at the end of the approved URL. The domain and path are fixed at approval — passing a full URL does not redirect anywhere, it is appended as a path segment. A template approved with https://a.example.com/receipt/{{1}} can never point at b.example.com.

This matters if one template must serve several schools on different domains. It cannot. Either attach the file instead (below), or route every link through one stable host that redirects onward.

Attaching a PDF instead

A DOCUMENT header fixes only the format, never the file — so headerMediaUrl may be any domain, different on every send. That makes one approved template usable by every school, which a URL button cannot do. The file is also saved to the recipient's phone, with no link to expire.

json
{
  "to": "9934849371",
  "template": "fee_receipt_pdf",
  "language": "en",
  "variables": ["Aarav Sharma", "2 A", "R-004821", "30 Jul 2026",
                "April, May, June", "12,600", "R.P.K PUBLIC SCHOOL"],
  "headerMediaUrl": "https://erp.yourschool.in/r/9f3a1c7e-4b21-4e88-9c05-2ad7f1e6b0c4.pdf",
  "headerFileName": "Receipt-R-004821.pdf"
}

Requirements for the file URL

Meta fetches this URL from its own servers, not from the recipient's phone. Everything below follows from that.

  • Public, no authentication. Behind a login, session cookie, VPN, firewall or IP allowlist it will fail — as will localhost or an internal hostname.
  • Serve the bytes directly: 200 OK with Content-Type: application/pdf. Not an HTML viewer page, not a redirect to a login.
  • Use an unguessable path. Because the file must be public, /receipt/1042 lets anyone walk 1041, 1043 and read other people's records. Use a random UUID or a signed token per file, and expire it if you can.
  • One URL per file. Do not reuse a path that serves different content over time — Meta may cache by URL and deliver a stale file.
  • Always send headerFileName, ending in .pdf. Without it WhatsApp shows the raw URL as the document name.
  • Keep it small. The ceiling is 100 MB for a PDF, 5 MB for an image, 16 MB for a video — but a receipt should be well under 1 MB, or Meta's fetch can time out.
  • Live at the moment of sending. If you generate the file lazily on first request, confirm that path works for an anonymous caller.

If a public URL is not acceptable, upload the file to Meta yourself and pass headerMediaId instead — nothing is exposed, and ids stay valid about 30 days.

POST/messages — rules for variable values

Meta validates each value in variables and rejects the send — not the template — if any of these are broken. Realistic when a value comes from a remarks field or a spreadsheet import, so sanitise before sending.

  • No line breaks and no tab characters
  • No four or more consecutive spaces
  • Send exactly as many values as the template has variables, in order
  • Never omit a position — every later value shifts up if you do
php
$clean = preg_replace('/\s+/', ' ', trim($value));

Call GET /templates for variableCount rather than hardcoding it — a template edited later may take a different number.

POST/messages/bulk

Up to 200 recipients per request.

json
{
  "template": "fee_reminder",
  "recipients": [
    { "to": "9934849371", "variables": ["Riya", "4500"] },
    { "to": "7053978898", "variables": ["Amit", "3200"] }
  ]
}

Same message to everyone? Use "to": [numbers] with one shared variables array.

Each recipient can carry its own file and link — what a receipt run needs, since every parent gets a different PDF:

json
{
  "template": "fee_receipt",
  "recipients": [
    { "to": "9934849371", "variables": ["Riya", "4500"],
      "headerMediaUrl": "https://erp.yourschool.in/receipts/8891.pdf",
      "headerFileName": "Receipt-8891.pdf",
      "buttonUrlParam": "8891" }
  ]
}

Returns HTTP 207 when some succeeded and some failed, with a results array giving the outcome per number. Treat 207 as partial success, not failure.

GET/messages/{id}

Delivery status: sentdeliveredread, or failed. Accepts our id or Meta's waMessageId.

GET/templates

Approved templates and how many variables each takes. Call this at startup rather than hardcoding names.

json
{ "templates": [
  { "name": "fee_receipt", "language": "en", "status": "APPROVED",
    "body": "Hi {{1}}, we received Rs {{2}}. Receipt {{3}}.",
    "variableCount": 3 }
]}
GET/contacts

List contacts. Filter with ?tag=class-8a, page with the returned nextCursor.

POST/contacts

Create or update a contact, keyed on the number.

json
{ "waId": "9934849371", "name": "Riya's mother",
  "tags": ["class-8a", "parent"],
  "attributes": { "studentId": "S-1042" } }

Optional — sending creates the contact automatically. Useful so the inbox shows real names and tags can be used as broadcast audiences.

Webhooks — replies and receipts

Set your URL under API & Webhooks in the panel. We POST when a customer replies and when a message is delivered, read or fails.

json
{
  "event": "message.received",
  "data": {
    "messageId": "cms...",
    "contact": { "waId": "919934849371", "name": "Riya's mother" },
    "type": "text",
    "body": "Yes, I'll pay by Friday",
    "receivedAt": "2026-07-28T10:14:58.000Z"
  }
}

Delivery receipts arrive as message.status. This is how you confirm a message actually reached the handset — the API response only tells you Meta accepted it.

json
{
  "event": "message.status",
  "data": {
    "messageId": "cms...",          // the id returned by POST /messages
    "waMessageId": "wamid.HBgMOTE5...",
    "status": "delivered",          // sent | delivered | read | failed
    "updatedAt": "2026-07-28T10:15:02.000Z"
  }
}
StatusMeaning
sentAccepted by Meta and on its way
deliveredReached the recipient's device
readOpened. Only if the recipient has read receipts on
failedNot delivered — wrong number, no WhatsApp account, or blocked

Match on data.messageId — the same id returned when you sent. Statuses arrive out of order and a message may never reach read; treat only failed as a definite negative.

Checking whether a number is on WhatsApp

There is no lookup for this — Meta withdrew the number-validation API, and no WhatsApp provider can offer a reliable one. The only way to know is to send and watch the status: a number with no WhatsApp account comes back failed. Record that against the contact and stop retrying it.

Verify X-SISPL-Signature on every callback before trusting it:

php
<?php
function verify(string $rawBody, string $header, string $secret): bool {
    $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
    return hash_equals($expected, $header);
}

Use the raw body, before JSON parsing — re-serialising changes the bytes and the signature will not match. Delivery is at-least-once, so make your handler idempotent on data.messageId. Respond 2xx quickly; we retry three times.

Errors

json
{ "error": { "code": "insufficient_balance",
            "message": "Insufficient balance. Please recharge your wallet." } }
StatusMeaningWhat to do
401key missing, wrong or revokedshow a config error
402wallet is emptyalert the admin — sending has stopped
403account suspended or unapprovedalert the admin
400bad request — see codefix the input; do not retry
503WhatsApp not connectedconnect a number in Settings
Never retry a 4xx — it will fail identically. Retry only 5xx and network errors, with backoff.

Drop-in PHP

php
<?php
function wa_send(string $to, string $template, array $vars = []): array
{
    $base = 'https://msg.sisplerp.com/api/v1';
    $key  = 'sk_live_...';

    $payload = ['to' => $to, 'template' => $template];
    if ($vars) $payload['variables'] = array_values($vars);

    $ch = curl_init("$base/messages");
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 20,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . $key,
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS     => json_encode($payload),
    ]);
    $body   = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $json = json_decode($body, true) ?: [];
    if ($status >= 200 && $status < 300) {
        return ['ok' => true, 'id' => $json['id'] ?? null];
    }
    return ['ok' => false, 'error' => $json['error']['message'] ?? "HTTP $status"];
}

Store the returned id against your record so delivery webhooks can be matched back. And never let a failed WhatsApp send roll back the transaction that triggered it — log it and carry on.