Automation & API
Webhooks
Get notified the moment an SMS, WhatsApp message, or USSD response comes in, without having to poll the API for it.
Webhooks push data out of Zender the moment something happens. They're managed under Tools → Webhooks (visible when your subscription includes them). Add webhook takes a name, a destination URL, and which event(s) it should fire for; a secret is generated for you and shown from the table's copy button, the same way API key secrets are (see the REST API page).
The identity contract
phone is bare international digits (5–15 characters). For a group,
phone is a full …@g.us identifier. Anything else is
rejected before it reaches your endpoint — there's no code path that hands your
webhook a phone number in any other shape.
This is enforced before anything — webhooks, auto-replies, flows — gets a chance to
run on an inbound WhatsApp message: unless the sender's identity is either a group ID
ending in @g.us or a bare string of 5–15 digits, the message is
dropped before it reaches any of them.
What actually lands in your payload's data.phone is normalized right
after that check: a group ID is passed through as-is, and a direct number always gets
a leading + added if it doesn't already have one — so in practice every
direct-message phone you receive looks like +639760713666,
never a bare digit string with no +.
Events and payloads
Three events exist, each fired the moment the corresponding message is received and
logged. All three are POSTed as an HTML form body (secret,
type, and a nested data array) — not JSON.
sms
Fired right after an inbound SMS is saved:
secret=WEBHOOK_SECRET
type=sms
data[id]=2
data[rid]=10593
data[sim]=1
data[device]=00000000-0000-0000-d57d-f30cb6a89289
data[phone]=+639760713666
data[message]=Hello World!
data[timestamp]=1645684231
id is the received row's ID, rid is the
Android device's own message ID, device is your device UUID, and
timestamp is a Unix timestamp of the receive time.
whatsapp
Fired right after an inbound chat is saved:
secret=WEBHOOK_SECRET
type=whatsapp
data[id]=2
data[wid]=+639760713666
data[phone]=+639760666713
data[message]=Hello World!
data[attachment]=yoursite.com/uploads/whatsapp/received/.../file.jpg
data[timestamp]=1645684231
wid is the receiving WhatsApp account's own number (with a leading
+); phone is the sender — a direct number or a
…@g.us group ID per the contract above. attachment is
false when the message had no media.
ussd
Fired once a USSD session response comes back from the device (see the REST API page for how to send a USSD request in the first place):
secret=WEBHOOK_SECRET
type=ussd
data[id]=98
data[sim]=1
data[device]=00000000-0000-0000-d57d-f30cb6a89289
data[code]=*143#
data[response]=Sorry! You are not allowed to use this service.
data[timestamp]=1645684231
Testing a webhook
Every row in the Webhooks table has a Simulate button next to its URL. It prompts you to pick an event type (SMS, WhatsApp, or USSD) and POSTs a realistic-but-fake payload — a sample local number, "Hello World!" as the message, random IDs — to your configured URL using your webhook's real secret, exactly the shape shown above. It's the fastest way to confirm your endpoint is reachable and checks the secret correctly before waiting on a real message.
Expected response
Your endpoint must respond with HTTP 200. Zender checks the
response status code from every call: on 200 it logs a "Webhook
Triggered!" entry; on anything else — including no response at all — it logs
"Webhook Failed!" instead, both visible from Tools → Logger. The
request itself uses a 5-second connect and total timeout and does
not retry, so a slow endpoint is indistinguishable from a failed one, and a failed
delivery does not queue or resend later.
A minimal receiving endpoint, matching the one shown in the dashboard's own webhook docs:
<?php
$request = $_REQUEST;
$secret = "WEBHOOK_SECRET"; // from Tools → Webhooks
if (isset($request["secret"]) && $request["secret"] === $secret) {
$type = $request["type"]; // "sms" | "whatsapp" | "ussd"
$data = $request["data"]; // array, shape depends on $type
// ...handle it...
http_response_code(200);
} else {
http_response_code(403);
}