CallMePing

How to set up webhooks to get leads and messages on your server

A webhook sends a JSON POST to your URL the moment something happens in your account, such as a lead being captured. You get one webhook URL per account and choose which events it receives.

Set up the webhook in the app

  1. Open Developer API in the sidebar.
  2. In the Webhook card, enter your URL, for example https://example.com/hooks/whatsapp.
  3. Under Events to send, tick the events you want. None ticked means you receive every event.
  4. Press Save webhook.
  5. Copy the signing secret that appears, then press Done. It is shown only once.
  6. Press Send test to send a webhook.test call to your URL.

Only the account owner can set up the webhook. Use Rotate secret if the secret leaks, and Remove to stop all calls.

Events you can receive

To push every captured lead into your CRM, tick automation.completed. The Example payload dropdown in the Webhook card shows a real sample of each event.

  • Messages: message (inbound message received), message.any (sent or received), message.ack (delivered or read), message.reaction.
  • AI Automations: automation.question_asked, automation.lead_collected, automation.completed.
  • Account and groups: session.status (WhatsApp connection status), group.v2.join, group.v2.leave, group.v2.update, group.v2.participants.

Set it up with the API instead

Save the URL and events (leave events empty for all)
curl -X PUT "https://callmeping.com/api/v1/webhook" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks/whatsapp","events":["automation.completed"]}'
Send a test call
curl -X POST "https://callmeping.com/api/v1/webhook/test" \
  -H "Authorization: Bearer YOUR_API_KEY"
Rotate the signing secret
curl -X PUT "https://callmeping.com/api/v1/webhook" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"rotateSecret":true}'
Remove the webhook
curl -X DELETE "https://callmeping.com/api/v1/webhook" \
  -H "Authorization: Bearer YOUR_API_KEY"

The secret is returned only when the webhook is first created or the secret is rotated. GET /webhook never shows it.

What each call looks like

Each call is a JSON POST with the headers X-Waha-Event (the event name), X-Waha-Session and X-Webhook-Signature. The body has the event, the session and a payload.

automation.completed body
{
  "event": "automation.completed",
  "session": "acct_ab12cd34ef56",
  "timestamp": "2026-09-30T10:15:00.000Z",
  "payload": {
    "automationId": "0b6f2c1e-...",
    "automationName": "Demo booking",
    "submissionId": "5d1a9e73-...",
    "chatId": "c83f10aa-...",
    "contactName": "Priya",
    "phoneNumber": "919876543210",
    "startedAt": "2026-09-30T10:12:41.000Z",
    "completedAt": "2026-09-30T10:15:00.000Z",
    "collected": { "Name": "Priya", "Email": "[email protected]", "Preferred date": "Friday 4 PM" }
  }
}

Verify the signature

X-Webhook-Signature is the HMAC-SHA256 hex digest of the raw request body, using your signing secret. Compute it over the exact bytes you received, before parsing the JSON.

Node (Express)
import crypto from "crypto";
import express from "express";

const app = express();
const SECRET = process.env.WEBHOOK_SECRET;

app.post("/hooks/whatsapp", express.raw({ type: "application/json" }), (req, res) => {
  const expected = crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
  const given = req.get("X-Webhook-Signature") || "";
  const ok = given.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
  if (!ok) return res.sendStatus(401);

  const { event, payload } = JSON.parse(req.body.toString());
  if (event === "automation.completed") saveLeadToCrm(payload);
  res.sendStatus(200);
});
Python (Flask)
import hashlib, hmac, os
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["WEBHOOK_SECRET"].encode()

@app.post("/hooks/whatsapp")
def hook():
    raw = request.get_data()
    expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(request.headers.get("X-Webhook-Signature", ""), expected):
        abort(401)
    data = request.get_json()
    if data["event"] == "automation.completed":
        save_lead_to_crm(data["payload"])
    return "", 200

Delivery rules

To fetch leads captured before your webhook existed, or any you missed, call GET /automations/{automationId}/collected.

  • Answer with any 2xx status within 10 seconds. Do heavy work after you answer.
  • Failed calls are retried, about 2 and 8 seconds apart. After that the event is dropped.
  • Delivery is at least once, so a call can arrive twice. For automation events, remove duplicates using payload.submissionId.
  • Order is not guaranteed across events.
  • Webhook calls are not rate limited.
Was this article helpful?

Common questions

Send test says Set a webhook URL first.

No webhook is saved yet. Enter your URL, press Save webhook, then press Send test.

Why is Send test greyed out?

You have unsaved changes. Press Save changes first.

My server rejects every call with a bad signature.

Check that you sign the raw body, not re-serialized JSON, and that you use the latest secret. If you pressed Rotate secret, update your server with the new one.

Does the test call follow my event filter?

No. The test call (event webhook.test) is always sent, whatever events you ticked.

I lost my signing secret.

It cannot be shown again. Press Rotate secret to get a new one and update your server.

Related articles