Messaging API
Send WhatsApp and SMS messages from your own systems — order confirmations, reminders, alerts — without going through the portal UI.
What you can do today
Send an approved WhatsApp template or a free-text SMS, check delivery status, and pull a history of what you’ve sent — all with the same API key. Every request carries an explicit channel field, which decides what the rest of the body means:
whatsapp— send an approved template by name. Meta requires templates to be approved before they can open a conversation.sms— send free text incontent. No approval step, no conversation threading, and billed per 160-character segment (70 if your text contains an emoji or other non-GSM character). Each response tells you how many segments were billed.
Omitting channel still defaults to whatsapp, so an existing integration needs no change. Free-text WhatsApp replies, interactive WhatsApp Flows and inbound SMS replies are planned for a later phase.
Get an API key
API keys are self-service — a business Admin creates and revokes them from the API keys tab in the Datagroup portal. The key is shown once at creation — store it somewhere safe (a secrets manager, not source control). Every request authenticates with Authorization: Bearer <your key>.
Quickstart — WhatsApp
Send a template message. Use GET /templates first to see which approved templates you can send and how many bodyParameters each expects.
curl -X POST https://datagroupwebfxapp-cyexbwg3csgkdcbq.southafricanorth-01.azurewebsites.net/api/public/v1/messages \
-H "Authorization: Bearer dgwa_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1029-ready" \
-d '{
"channel": "whatsapp",
"to": "+27821234567",
"templateName": "order_ready",
"bodyParameters": ["Thabo", "ORD-1029"]
}'Pass an Idempotency-Key header on every send — retrying a request with the same key replays the original result instead of sending a second message.
Quickstart — SMS
No template needed — put the message text in content. The response includes a segments count so you know what was billed.
curl -X POST https://datagroupwebfxapp-cyexbwg3csgkdcbq.southafricanorth-01.azurewebsites.net/api/public/v1/messages \
-H "Authorization: Bearer dgwa_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1029-sms" \
-d '{
"channel": "sms",
"to": "+27821234567",
"content": "Your order ORD-1029 is ready for collection."
}'SMS has no conversation to thread into, so conversationId comes back null, and delivery stops at Delivered — there is no read receipt for SMS.
Delivery webhooks — don't poll
Register one HTTPS endpoint in the portal (Admin → API keys → Delivery webhook) and we POST every status change to it as it happens, on both channels. You get a signing secret once, at registration. Polling GET /messages/{id} still works, but at volume the webhook is the supported path.
Each POST carries X-Datagroup-Timestamp and X-Datagroup-Signature — sha256= plus an HMAC-SHA256 of the exact string {timestamp}.{raw body} in lowercase hex. The timestamp is part of what's signed, so a captured payload can't be replayed later.
// Node — verify before trusting the payload. Use the RAW body, not a re-serialised object.
import crypto from "node:crypto";
app.post("/datagroup/status", express.raw({ type: "application/json" }), (req, res) => {
const timestamp = req.header("X-Datagroup-Timestamp");
const signature = req.header("X-Datagroup-Signature");
const expected =
"sha256=" +
crypto
.createHmac("sha256", process.env.DATAGROUP_WEBHOOK_SECRET)
.update(`${timestamp}.${req.body.toString("utf8")}`)
.digest("hex");
const ok =
signature &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)) &&
Math.abs(Date.now() / 1000 - Number(timestamp)) < 300; // reject replays
if (!ok) return res.sendStatus(401);
const { messageId, status, occurredAt } = JSON.parse(req.body);
// Statuses can repeat and can arrive out of order — upsert on messageId, keep the latest occurredAt.
res.sendStatus(200);
});Delivery is at-least-once and unordered: the same status can arrive twice, and two transitions seconds apart can arrive the wrong way round. Make the handler idempotent and trust occurredAt over arrival order. Return 2xx promptly — a non-2xx is retried five times, about a minute apart, after which we stop trying (the status is still there via GET /messages/{id}).
Rate limits & errors
Each key is limited to a generous per-minute request rate; going over it returns 429 with a Retry-After header. Errors share one JSON shape:
{ "error": { "code": "template_not_approved", "message": "...", "requestId": "..." } }Quote the requestId if you need to contact us about a specific call.
Full reference
Every endpoint, request and response shape: API reference. The raw spec is also published at /openapi/public-api-v1.yaml (OpenAPI 3.0) for import into Postman, Insomnia, or a client-code generator.
Questions?
Reach us at info@datagroup.co.za, or see pricing if you’re not yet a Datagroup client.