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, which receive incoming messages as they arrive
Webhooks, which receive incoming messages as they arrive

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

Zender only ever delivers phone-number identities. For a direct message, 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);
}