Datagroup

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 in content. 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-Signaturesha256= 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.