Sonar API and webhooks
Connect your own software to WhatsApp through Sonar: send messages and templates, add and update contacts with their consent, and get every incoming message, reply from the WhatsApp Business app and delivery update at your own address. Sonar sends through Meta's official WhatsApp Cloud API.
Before you start
- A connected number. Sign up, then connect a WhatsApp number under WhatsApp numbers. A number that's already in the WhatsApp Business app can stay there: see Sonar with the WhatsApp Business app.
- An API key. Each number has its own, under WhatsApp numbers → Advanced (for developers). Send it on every request as
Authorization: Bearer YOUR_API_KEY. It sends from that number, so keep it on your server, never in a browser or app. - Base address:
https://sonar.whats-hub.co.za. Requests and responses are JSON. - On every plan. The API and webhooks come with every Sonar plan, the free trial too, at no extra cost.
- Full reference: every call, field, response and error is in the API reference, and the OpenAPI spec is at /openapi.json to import into Postman or generate a client.
- Phone numbers are in international format, with or without the
+:27821234567or+27 82 123 4567.
Send a message
Free text, for replying to someone who has messaged the number in the last 24 hours. Outside that window WhatsApp only accepts an approved template.
POST https://sonar.whats-hub.co.za/send
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{"recipient": "27821234567", "message": "Hi Sam, your room is ready from 14:00."}
200 OK
{"message_id": "wamid.HBgL...", "status": "sent"}
curl -X POST https://sonar.whats-hub.co.za/send \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"recipient":"27821234567","message":"Hello!"}'
Keep the message_id: it's how you look up delivery, and how webhook updates refer to the message.
recipient is a number in international format, or the WhatsApp user ID of a customer who hides their number (see Customers who use a WhatsApp username).
Links in a message can be tapped. To show a preview of the first link, add "preview_url": true to the request. Leave it out for one-time links, like sign-in links. WhatsApp opens the page to build the preview.
{"recipient": "27821234567", "message": "Your booking: https://example.com/b/123", "preview_url": true}
Send a template
Approved templates can be sent at any time. Create them in Sonar under Templates; WhatsApp usually approves them within minutes.
body_parametersfill the template's{{1}},{{2}}and so on, in order. If the template uses named fill-ins like{{first_name}}, give the values in the order those fill-ins first appear in the template.buttonsare for buttons that need a value when you send:index(its position among the template's buttons, from 0),kind(urlfor a link ending in a variable,copy_codefor a coupon,quick_reply) andpayload(the value).header_documentfor a PDF header:filename, plusbase64or an httpslink, up to 16 MB.header_imagefor an image header:linkorbase64, JPEG or PNG up to 5 MB.reference(optional) is your own id for the message, up to 100 characters. Send the samereferenceagain from the same number within 24 hours, for example after a timeout, and Sonar doesn't send a second message: you get the first one's answer with"duplicate": true. While the first is still being sent you get409, and if WhatsApp didn't answer the first in time (so it may have gone) a retry with thatreferencegets409too. Thereferencecomes back inmessage.statusand/message-status.- Opt-outs. A marketing template (or one whose category Sonar doesn't know) isn't sent to someone who opted out of your business, or of this number only. You get
403with the reason, for example{"detail": "Sam opted out of marketing from this number."}. Utility and authentication templates, like order and booking updates, still send.
POST https://sonar.whats-hub.co.za/send-template
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{"recipient": "27821234567", "template_name": "booking_confirmed", "language_code": "en_US",
"body_parameters": ["Sam", "Kruger Safari Lodge", "12 May", "15 May"],
"buttons": [{"index": 0, "kind": "url", "payload": "P-881"}], "reference": "booking-881"}
200 OK
{"message_id": "wamid.HBgL...", "status": "sent", "reference": "booking-881"}
Check delivery
WhatsApp reports delivery a moment after it accepts a message, so a sent message can still fail. Send up to 100 message_ids from this number to get each one's latest status: sent, delivered, read or failed. The message.status webhook tells you the same as it happens.
POST https://sonar.whats-hub.co.za/message-status
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{"wamids": ["wamid.HBgL..."]}
Or look them up by the references you gave /send-template: {"references": ["booking-881"]}. Times are UTC.
200 OK
{"statuses": {"wamid.HBgL...": {"status": "read", "error_code": null, "error_message": null,
"updated_at": "2026-10-25T12:01:09", "reference": "booking-881"}}}
Create a template
Templates can also be made by API, on the WhatsApp account of the key's number. WhatsApp reviews each one, usually within minutes and at most 24 hours; /list-templates and the template.updated webhook tell you the outcome.
category:UTILITY,MARKETINGorAUTHENTICATION. WhatsApp can approve a utility template as marketing if it reads like one, so checkcategoryin the answer.body_textuses{{1}},{{2}}and so on, with a sample for each inbody_example. WhatsApp rejects text that starts or ends with a fill-in.buttons:{"type": "URL", "text": ..., "url": ...}for a link. A link ending in{{1}}takes its end from the send'sbuttons, with a sample inexample. AlsoQUICK_REPLYandPHONE_NUMBER.
POST https://sonar.whats-hub.co.za/create-template
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{"name": "booking_confirmed", "category": "UTILITY", "language_code": "en_US",
"body_text": "Hi {{1}}, your stay at {{2}} from {{3}} to {{4}} is confirmed.",
"body_example": ["Sam", "Kruger Safari Lodge", "12 May", "15 May"],
"buttons": [{"type": "URL", "text": "View booking", "url": "https://example.com/b/{{1}}", "example": "P-881"}]}
200 OK
{"id": "1234567890123456", "status": "PENDING", "category": "UTILITY", "name": "booking_confirmed"}
List your templates
The templates on this number's WhatsApp account, with their approval status, straight from Meta.
GET https://sonar.whats-hub.co.za/list-templates
Authorization: Bearer YOUR_API_KEY
200 OK
{"templates": [{"name": "booking_confirmed", "status": "APPROVED", "category": "UTILITY", "language": "en_US"}]}
Add or update a contact
Matched on the phone number: a new number is added, an existing contact is updated. Use it to keep Sonar in step with your own system, for example tagging a guest "Hold placed" so a flow sends a reminder.
tagsare added to the ones the contact has, andremove_tagsare taken off. Either can be a list or one comma-separated string.customsets your own contact fields by their key, likearrival_date. Add fields under Contacts → Custom fields.opt_in: truerecords marketing consent, withopt_in_sourcesaying where it came from.opted_out: trueunsubscribes them. Sonar keeps the consent record POPIA asks for.- For a customer whose number is hidden, send their
whatsapp_user_idasphone. It updates the contact Sonar has for them. It never adds a new one, so an ID Sonar doesn't know gets a 400.
POST https://sonar.whats-hub.co.za/contacts/upsert
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{"phone": "+27 82 123 4567", "name": "Sam Scop", "tags": ["Hold placed"],
"remove_tags": ["Enquiry"], "custom": {"arrival_date": "2026-11-14"},
"opt_in": true, "opt_in_source": "booking form"}
200 OK
{"id": 123, "wa_id": "27821234567", "name": "Sam Scop", "created": true, "opted_out": false,
"tags": ["Hold placed"], "custom": {"arrival_date": "2026-11-14"}, "opt_in_at": "2026-10-25T14:00:00Z"}
Webhooks
Sonar can post what happens to your own address as it happens. Set it up under Settings → Connected apps → Your own system (webhook): give the https address, choose the events, and copy the signing secret, which is shown once. One webhook covers all of a business's numbers.
- Format: a JSON
POSTwithid(unique per update),type(the event) andsent_at, plus the event's own fields. The headers includeX-Sonar-Event(the event) andX-Sonar-Delivery(the sameid). - Signature:
X-Sonar-Signature: sha256=...is the HMAC-SHA256 of the raw request body with your secret. Check it before trusting an update. - Delivery: answer with any 2xx within 10 seconds. If your address doesn't, Sonar tries twice more over about half a minute. Use
idto ignore an update you've already handled. - Addresses inside a private network aren't allowed.
# Python: check the signature on the raw body, before parsing it
import hashlib, hmac
def is_from_sonar(secret: str, raw_body: bytes, signature: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or "")
Webhook events
| Event | When |
|---|---|
message.received | Someone messages one of your numbers. |
message.sent | Someone on your team replies from the WhatsApp Business app on their phone (Meta's message echoes, on numbers connected with the app). |
message.status | A message sent through Sonar or the API is delivered, read or fails. |
template.updated | WhatsApp approves, rejects, pauses or switches off a template, gives notice that it will move one to another category (about 24 hours ahead), or moves it. |
contact.opted_in, contact.opted_out | Someone opts in or out, by keyword, button or the API. |
rsvp.updated | An event RSVP changes (going, not going, waitlist). |
number.connected | A property's owner connects their number through a partner connect link (see Building a platform). Always sent to partners. |
{"id": "4f1c...", "type": "message.received", "sent_at": "2026-10-25T12:00:02Z",
"message": {"id": "wamid.HBgL...", "type": "text", "text": "Do you have a room on Friday?",
"received_at": "2026-10-25T12:00:01Z", "phone_number_id": "1234567890",
"ad_headline": null, "media": null},
"contact": {"whatsapp": "+27821234567", "whatsapp_user_id": "ZA.1349...", "name": "Sam", "tags": ["Guest"], "opted_out": false}}
{"id": "9b2e...", "type": "message.sent", "sent_at": "2026-10-25T12:03:40Z",
"message": {"id": "wamid.HBgL...", "type": "text", "text": "Yes, the Main Suite is free.",
"to": "+27821234567", "to_user_id": "ZA.1349...", "sent_at": "2026-10-25T12:03:39Z",
"phone_number_id": "1234567890", "sent_from": "whatsapp_business_app", "media": null},
"contact": {"whatsapp": "+27821234567", "whatsapp_user_id": "ZA.1349...", "name": "Sam", "tags": ["Guest"], "opted_out": false}}
{"id": "c07a...", "type": "message.status", "sent_at": "2026-10-25T12:05:11Z",
"message": {"id": "wamid.HBgL...", "status": "read", "previous_status": "delivered",
"to": "+27821234567", "to_user_id": "ZA.1349...", "template": "booking_confirmed", "broadcast_id": null,
"reference": "booking-881", "phone_number_id": "1234567890", "error": null, "at": "2026-10-25T12:05:10Z"}}
{"id": "7d3a...", "type": "template.updated", "sent_at": "2026-10-25T12:07:00Z",
"template": {"name": "booking_confirmed", "language": "en_US", "status": "APPROVED", "category": "UTILITY",
"reason": null, "waba_id": "1029...", "change": "category_notice",
"coming_category": "MARKETING", "coming_at": "2026-10-26T12:00:00Z"}}
change is status (with previous_status), category_notice (with coming_category and coming_at) or category (with previous_category). reason is filled for a rejection.
{"id": "e51d...", "type": "contact.opted_out", "sent_at": "2026-10-25T12:09:00Z",
"contact": {"whatsapp": "+27821234567", "whatsapp_user_id": "ZA.1349...", "name": "Sam", "tags": ["Guest"], "opted_out": true},
"consent": {"source": "keyword", "keyword": "STOP", "detail": "STOP"}}
Customers who use a WhatsApp username
Some customers use a WhatsApp username and don't share their number. For them, "whatsapp" is null and their WhatsApp user ID is in "whatsapp_user_id". In message.sent and message.status, "to" is null for them and the ID is in "to_user_id". To message them, send that ID as "recipient" to /send or /send-template, exactly as you got it.
- A customer whose number Sonar already has usually keeps it: Sonar sends to their number, as WhatsApp recommends. If Sonar keeps their user ID as a separate contact, a send to the ID goes to the ID.
- Sign-in code (authentication) templates need a number. WhatsApp refuses them to a user ID.
Files
When a message has a document, photo, voice note or video, its update includes message.media with an id, mime, filename and a url. Fetch the file from that address with any of the business's API keys:
GET https://sonar.whats-hub.co.za/media/MEDIA_ID
Authorization: Bearer YOUR_API_KEY
Sonar keeps each file for 12 months, so save the ones you need in your own system. Voice notes come as the original audio file (OGG).
Numbers on the WhatsApp Business app
A number connected with the WhatsApp Business app (what Meta calls coexistence) works with the API like any other, and also keeps working in the app on the owner's phone. South African +27 numbers included.
- What you send shows in the app. Messages sent through
/sendor/send-templateappear in that chat in the owner's WhatsApp Business app. - What they send from the app reaches you. A reply typed in the app arrives as
message.sent, with"sent_from": "whatsapp_business_app". Messages sent from the app stay free. - Incoming messages go to both. They show in the app and in Sonar, and reach you as
message.received. - The API keeps WhatsApp's rules. Free text through
/sendonly works within 24 hours of the customer's last message; after that, send an approved template. The owner carries on in the app as they always have. - Past chats aren't replayed. When the number connects, Meta can copy up to 6 months of past chats into Sonar's inbox. They aren't sent to your webhook, which starts from the moment it's set up. Group chats stay in the app only.
- The app has to stay in use. If the WhatsApp Business app isn't opened for about 14 days, Meta disconnects the number and it needs connecting again.
Errors and limits
401: the API key is missing or wrong.400: something in the request isn't right, like a missing field or a number that isn't in international format. The response'sdetailsays what. WhatsApp's own refusals come back here too, for example a free-text/sendto someone outside their 24-hour window.402: the message is past the plan's included messages and the account's balance can't cover it. Top up or upgrade to carry on.- If WhatsApp is pacing your number, wait a little and try again. Broadcasts sent from Sonar are paced for you.
- WhatsApp's own charge for each message is billed by Meta to the card on the business's WhatsApp account. See WhatsApp pricing in South Africa.
Building a platform
If you run software for many businesses, like a booking or property management system, the partner API lets each of your customers connect their own WhatsApp number from inside your product, with no Sonar login. Each customer becomes its own business in Sonar (a property here), with its own numbers, contacts and opt-outs, and its number stays in the owner's own Meta business portfolio.
- Add the property. Sonar returns a
connect_urlfor its owner. It works once, for 30 days. - Send the owner to it. They sign in with Facebook and confirm on their phone in the WhatsApp Business app, which keeps working as before.
- They come back to you. Sonar sends them to your
return_urlwithproperty_idandphone_number_idadded, and postsnumber.connectedto your webhook. - Get the number's API key from the property, and use it for everything above.
POST https://sonar.whats-hub.co.za/partner/properties
Authorization: Bearer YOUR_PARTNER_KEY
Content-Type: application/json
{"name": "Riverside Guest House", "return_url": "https://yourapp.example/whatsapp/connected"}
{"id": 42, "name": "Riverside Guest House",
"connect_url": "https://sonar.whats-hub.co.za/connect/3kQ9...", "connect_url_expires_at": "2026-11-25T09:00:00Z",
"numbers": []}
Once the owner has connected, GET /partner/properties/42 lists the number with its api_key. GET /partner/properties lists every property, and POST /partner/properties/42/connect-link makes a new link when one has expired or the owner needs to connect again.
{"id": "a81f...", "type": "number.connected", "sent_at": "2026-10-26T09:14:03Z",
"number": {"phone_number_id": "1234567890", "display_phone": "+27 82 123 4567",
"name": "Riverside Guest House", "whatsapp_business_app": true},
"property": {"id": 42, "name": "Riverside Guest House"}}
The webhook in your own Sonar business (see Webhooks) gets every property's updates, each with a property field, so one address and one secret cover them all.
Email support@whats-hub.co.za to become a partner. Once we've switched it on, your account owner makes the partner key under Settings → Connected apps → Partner API. It's shown once and never sent by email. Partners' owners and admins can read the partner API reference once signed in.
More guides
- WhatsApp pricing in South Africa
- POPIA and WhatsApp marketing
- Event invites with one-tap RSVPs
- AI Agent and Copilot: a WhatsApp chatbot in your voice
- Use Sonar with the WhatsApp Business app
- Import your contacts
- Order and booking updates from your other systems
- Which number to use for WhatsApp
- Connect a client's WhatsApp number
- WhatsApp API providers in South Africa, compared
- Switching to Sonar
- WhatsApp RSVPs for events
- Competitions and prize draws
Need a hand?
Email support@whats-hub.co.za with what you're building and we'll help you connect it. Uptime and updates are on status.whats-hub.co.za.