When a customer messages one of your numbers, WaConnect sends it to your server straight away with a webhook: an HTTP POST with a JSON body. No polling needed.
Build an endpoint#
Your server needs a public https URL that accepts POST requests with a JSON body and answers with any 2xx status within 10 seconds.
import express from "express"
const app = express()
// Keep the raw body: you need it to verify the signature.
app.post("/webhooks/waconnect", express.raw({ type: "application/json" }), (req, res) => {
const event = JSON.parse(req.body.toString("utf8"))
if (event.type === "message.received") {
const { contact, text } = event.data.message
console.log(`${contact.name ?? contact.phone}: ${text}`)
}
res.sendStatus(200) // answer fast; do slow work afterwards
})
app.listen(3000)Add it in WaConnect#
In Settings → Developer settings → Webhooks, click Add webhook, paste your URL, choose the events you want, and choose its numbers: All numbers, or only the numbers this app handles. You'll see a signing secret (whsec_…) — store it on your server.
Send a test event#
Open the webhook's menu and choose Send test event. Your server receives a webhook.test event, and the dashboard shows what your server answered.
Verify every request#
Check the WaConnect-Signature header before trusting a request — see Verify webhooks.
What you receive#
{
"id": "evt_9f2c1b7e4a6d4c0f8e3b2a1d5c6f7e80",
"type": "message.received",
"createdAt": "2026-10-01T10:15:02.000Z",
"data": {
"message": {
"id": "01a0f4d1-1c2b-7e3a-8f4d-5a6b7c8d9e0f",
"numberId": "01a0f44d-c969-75b1-955d-3841d817f711",
"direction": "INBOUND",
"contact": { "phone": "+14155550123", "name": "Emily Carter" },
"kind": "TEXT",
"text": "Hi, is my order ready?",
"status": "DELIVERED",
"error": null,
"sentVia": null,
"sentAt": "2026-10-01T10:15:01.000Z"
}
}
}Photos, voice notes and other media arrive with their kind (IMAGE, AUDIO, DOCUMENT…) and their caption in text. See every event in Events & payloads.
Answer fast, then work#
Reply 200 as soon as you've stored the event, and do slow work (database writes, calling other APIs) afterwards. If your server takes longer than 10 seconds or returns an error, we retry — for about 24 hours.
Ignore duplicates
A retry delivers the same event again with the same id. Store the ids you've handled and skip ones you've already seen.
Testing on your laptop#
http://localhost URLs are refused in production. To test locally, expose your laptop with a tunnel such as ngrok http 3000 and use the https URL it gives you.