Send WhatsApp messages from your own software — school ERP, CRM or website.
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.
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.
/messagesSend one message.
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
{
"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.
/messages — with a file or a link buttonA 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.
{
"to": "9934849371",
"template": "fee_receipt",
"variables": ["Riya Sharma", "4500"],
"headerMediaUrl": "https://erp.yourschool.in/receipts/8891.pdf",
"headerFileName": "Receipt-8891.pdf",
"buttonUrlParam": "8891"
}headerMediaUrl | PDF, image or video for the header. Meta downloads this itself, so it must be publicly reachable — a link behind your firewall will fail. |
headerMediaId | Use instead of the URL if you already uploaded the file to Meta. |
headerFileName | Filename shown for a document header. Omit it and WhatsApp displays the raw URL, which looks broken to the recipient. |
buttonUrlParam | Fills {{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.
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.
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.
{
"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"
}Meta fetches this URL from its own servers, not from the recipient's phone. Everything below follows from that.
localhost or an internal hostname.200 OK with Content-Type: application/pdf. Not an HTML viewer page, not a redirect to a login./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.headerFileName, ending in .pdf. Without it WhatsApp shows the raw URL as the document name.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.
/messages — rules for variable valuesMeta 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.
$clean = preg_replace('/\s+/', ' ', trim($value));Call GET /templates for variableCount rather than hardcoding it — a template edited later may take a different number.
/messages/bulkUp to 200 recipients per request.
{
"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:
{
"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.
/messages/{id}Delivery status: sent → delivered → read, or failed. Accepts our id or Meta's waMessageId.
/templatesApproved templates and how many variables each takes. Call this at startup rather than hardcoding names.
{ "templates": [
{ "name": "fee_receipt", "language": "en", "status": "APPROVED",
"body": "Hi {{1}}, we received Rs {{2}}. Receipt {{3}}.",
"variableCount": 3 }
]}/contactsList contacts. Filter with ?tag=class-8a, page with the returned nextCursor.
/contactsCreate or update a contact, keyed on the number.
{ "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.
Set your URL under API & Webhooks in the panel. We POST when a customer replies and when a message is delivered, read or fails.
{
"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.
{
"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"
}
}| Status | Meaning |
|---|---|
sent | Accepted by Meta and on its way |
delivered | Reached the recipient's device |
read | Opened. Only if the recipient has read receipts on |
failed | Not 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.
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
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.
{ "error": { "code": "insufficient_balance",
"message": "Insufficient balance. Please recharge your wallet." } }| Status | Meaning | What to do |
|---|---|---|
| 401 | key missing, wrong or revoked | show a config error |
| 402 | wallet is empty | alert the admin — sending has stopped |
| 403 | account suspended or unapproved | alert the admin |
| 400 | bad request — see code | fix the input; do not retry |
| 503 | WhatsApp not connected | connect a number in Settings |
<?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.