Download OpenAPI specification:
Send WhatsApp and SMS messages from your own systems and check delivery status — the API behind Datagroup's messaging automation. Authenticate with a per-tenant API key created in the Datagroup portal (Admin → API keys). The same key works for every channel.
Channels. Every request and response carries an explicit channel field:
whatsapp |
sms |
|
|---|---|---|
| What you send | an approved template (templateName) |
free text (content) |
| Approval needed | yes, by Meta | no |
| Threading | messages join a 24-hour conversation | none — each SMS stands alone |
| Delivery states | Sent → Delivered → Read | Sent → Delivered (no read receipt) |
| Billed by length | no | yes — per 160-character segment |
Omitting channel still defaults to whatsapp, so existing integrations are unaffected.
SMS is billed per segment. A message longer than 160 characters is split and billed as
multiple segments (70 characters if it contains any non-GSM character, such as an emoji). The
segments field on every SMS response tells you how many were billed.
Delivery-status webhooks — don't poll. Instead of calling GET /messages/{messageId} on a
timer, register one HTTPS endpoint in the portal (Admin → API keys → Delivery webhook) and we POST
every status change to it as it happens, for both channels. Polling still works, but at volume the
webhook is the supported path.
Each POST carries:
{
"event": "message.status",
"messageId": "3f9c…",
"providerMessageId": "wamid.HBg…",
"channel": "whatsapp",
"status": "Delivered",
"to": "+27821234567",
"templateName": "order_confirmation",
"errorDetail": null,
"occurredAt": "2026-08-09T13:04:31.6339263+00:00"
}
Verify the signature before trusting a payload. Two headers accompany the body:
| Header | Value |
|---|---|
X-Datagroup-Timestamp |
Unix seconds when we signed |
X-Datagroup-Signature |
sha256= + HMAC-SHA256 over the exact string {timestamp}.{raw body}, lowercase hex |
Compute the HMAC with the signing secret shown once when you registered the endpoint, using the raw request body (not a re-serialised object — any whitespace difference changes the hash), and compare with a constant-time comparison. The timestamp is inside the signed string, so a captured payload cannot be replayed with a fresh one; reject anything older than a few minutes.
Delivery is at-least-once and unordered. Retries mean you may see the same status twice, and two
transitions seconds apart can arrive out of order. Make your handler idempotent and trust
occurredAt over arrival order. Return 2xx promptly — a non-2xx is retried five times, roughly a
minute apart, after which the notification is dropped (your data is still available via
GET /messages/{messageId}).
Not yet available: free-text WhatsApp replies within the 24-hour window, WhatsApp Flows, and inbound SMS replies.
Sends a message to a South African mobile number. What the body must contain depends on
channel:
whatsapp — templateName is required; only approved templates can open a
conversation. Supply bodyParameters in order if the template has {{1}}, {{2}}, ...
placeholders (see GET /templates for each template's expected count).
Sending an OTP? Authentication templates carry a copy-code button that Meta requires a
parameter for; it is filled from your bodyParameters automatically, so send the code once
and nothing else changes. Templates in other categories with a dynamic URL button need
buttonParameters.sms — content is required and carries the message text. SMS has no
template-approval concept, so passing templateName on this channel is rejected rather
than ignored. The response's segments tells you how many segments were billed.Pass an Idempotency-Key header to make retries safe — resending the same key returns
the original result instead of sending a second message. Reusing a key always replays the
first result; it is not re-validated against a changed request body, so treat each key as
single-use per logical send.
| Idempotency-Key | string An opaque string unique to this logical send. Retrying with the same key replays the original result. |
| channel | string Default: "whatsapp" Enum: "whatsapp" "sms" Defaults to |
| to required | string South African mobile number, |
| content | string SMS only — the message text. Billed per 160-character segment (70 if it contains any non-GSM character, such as an emoji); the response's |
| templateName | string WhatsApp only — the approved template to send. |
| language | string Meta language code. Defaults to the template's own language if omitted. |
| phoneNumberId | string <uuid> Required only if you have more than one active WhatsApp number (see |
| bodyParameters | Array of strings Positional values for the template's |
object WhatsApp only, and rarely needed. Values for dynamic URL buttons, keyed by the button's zero-based position as a string — e.g. Authentication templates (OTPs) do not need it. Their copy-code / one-tap button takes the same code as the body, so it is filled from | |
| headerImageUrl | string Overrides the template's stored header image, if any. |
{- "channel": "whatsapp",
- "to": "+27821234567",
- "templateName": "order_ready",
- "language": "en_US",
- "bodyParameters": [
- "Thabo",
- "ORD-1029"
]
}{- "messageId": "8540d774-4863-4d2b-b788-4ecb19412e85",
- "providerMessageId": "string",
- "status": "Sent",
- "conversationId": "ee6e55e8-45fe-4a3e-9bc8-4669f9fdf77a",
- "channel": "whatsapp",
- "segments": 1
}Keyset-paginated list of your outbound messages, newest first. Defaults to the last 30 days if from/to are omitted.
| channel | string Enum: "whatsapp" "sms" Omit to list every channel. |
| status | string Enum: "Queued" "Sent" "Delivered" "Read" "Failed" |
| templateName | string |
| phoneNumberId | string <uuid> |
| from | string <date-time> ISO 8601 datetime; defaults to 30 days ago. |
| to | string <date-time> ISO 8601 datetime; defaults to now. |
| limit | integer [ 1 .. 200 ] Default: 50 |
| cursor | string Opaque token from a prior page's |
{- "items": [
- {
- "messageId": "8540d774-4863-4d2b-b788-4ecb19412e85",
- "to": "string",
- "direction": "Outbound",
- "templateName": "string",
- "status": "Queued",
- "timestamp": "2019-08-24T14:15:22Z",
- "errorDetail": "string",
- "conversationId": "ee6e55e8-45fe-4a3e-9bc8-4669f9fdf77a",
- "channel": "whatsapp",
- "providerMessageId": "string",
- "segments": 0
}
], - "nextCursor": "string"
}| messageId required | string <uuid> |
{- "messageId": "8540d774-4863-4d2b-b788-4ecb19412e85",
- "to": "string",
- "direction": "Outbound",
- "templateName": "string",
- "status": "Queued",
- "timestamp": "2019-08-24T14:15:22Z",
- "errorDetail": "string",
- "conversationId": "ee6e55e8-45fe-4a3e-9bc8-4669f9fdf77a",
- "channel": "whatsapp",
- "providerMessageId": "string",
- "segments": 0
}Includes the derived bodyParameterCount so you know how many bodyParameters a send needs.
{- "items": [
- {
- "name": "string",
- "language": "string",
- "category": "UTILITY",
- "bodyParameterCount": 0,
- "hasHeaderImage": true
}
]
}Only relevant when you have more than one number — with exactly one, POST /messages uses it automatically and phoneNumberId can be omitted.
{- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "displayPhoneNumber": "string",
- "isDefault": true
}
]
}