Test webhooks
Three ways to check your webhook handler before you go live. Send a signed test event to your address, forward your account's real events to localhost with bluebot listen, or open a tunnel to your laptop.
There are three ways to try a handler. Each suits a different stage of building one:
| Way | What reaches your handler | Best for |
|---|---|---|
| A signed example of any event type, posted to a webhook's real address | Checking a deployed handler, and every event type, without sending a text | |
Your account's real events, forwarded to localhost and signed with a local secret | Writing a handler on your laptop, with no tunnel and no webhook to set up | |
| Real deliveries from a real webhook, with its retries and its secret | The last check before you deploy, end to end |
None of them needs a stranger's phone. When an event needs a real text, text yourself.
Send a test event
Every webhook has Send a test event in Developers → Webhooks: pick a type and BlueBot posts an example of it to the webhook's address, signed with the webhook's secret like any delivery. The same thing through the API:
/v1/webhooks/wh_1Hq2LxW9cVbN3mKpRtYs0a/testcurl -X POST "https://usebluebot.com/api/v1/webhooks/wh_1Hq2LxW9cVbN3mKpRtYs0a/test" \
-H "Authorization: Bearer $BLUEBOT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "message.received"
}'BlueBot answers once it has tried, with the delivery, so you see what your handler said in the same request:
{
"id": "dlv_3Lp3VsC7hGqE5tZyKbWn2d",
"object": "webhook_delivery",
"webhook_id": "wh_1Hq2LxW9cVbN3mKpRtYs0a",
"event_id": "evt_3Lp3VsC7hGqE5tZyKbWn2d",
"event_type": "message.received",
"test": true,
"state": "delivered",
"attempts": 1,
"last_status": 204,
"last_error": null,
"last_duration_ms": 87,
"next_attempt_at": null,
"delivered_at": "2026-11-13T19:41:00.000Z",
"replay_of": null,
"created_at": "2026-11-13T19:41:00.000Z"
}| You got | It means | Do next |
|---|---|---|
state: "delivered" and a 2xx in last_status | Your handler verified the post and answered. | Try the other types your handler takes. |
state: "pending" with last_status: 400 | Your handler rejected it, usually the signature. | Check the secret and the raw body. |
state: "pending" with last_status: 500 | Your handler threw. | Read your logs. The test is retried like any delivery, so once you fix the handler it arrives. |
state: "pending" and last_error set | BlueBot couldn't reach the address or got no answer in 10 seconds. | Check the address is public and quick to answer. |
state: "pending", attempts: 0 | The webhook is paused, so nothing was sent. | Resume it, then test again. |
What to know about test events:
- The body is the 's example for that type inside the same envelope a real event has. Its ids are the docs' example ids (
msg_1Hq2LxW9cVbN3mKpRtYs0a,ct_1Hq2LxW9cVbN3mKpRtYs0a), so a handler that looks the record up gets404 not_found. Handle that the way you would in production. - The body says it is a test: it has
"test": truebesidetype, which a real event never has. Skip writing anything when you see it, or let it run through in a staging copy of your handler. The delivery also showstest: truein the deliveries log, and the event never appears in the event feed. - A test goes to the one webhook you chose, even if it doesn't subscribe to that type.
To try every type your handler takes in one go:
import BlueBot from '@usebluebot/sdk';
const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });
const webhookId = 'wh_1Hq2LxW9cVbN3mKpRtYs0a';
for (const type of ['message.received', 'message.sent', 'message.failed', 'contact.stopped'] as const) {
const delivery = await client.webhooks.test(webhookId, { type });
console.log(type.padEnd(18), delivery.state); // "delivered", or "pending" while it's retried
}Forward events to localhost with bluebot listen
bluebot listen lets you write a handler on your laptop with no tunnel and no webhook. It reads your account's event feed every couple of seconds and posts each new event to the address you give it, signed with a secret of its own, exactly as a webhook delivery is.
Install the CLI and sign in
npm install -g @usebluebot/cli
bluebot loginbluebot login asks for an API key. A key with messages:read is enough to listen, and messages:send lets you text yourself from the CLI too.
Start listening
bluebot listen --forward-to http://localhost:3000/bluebot/webhooksListening for events on dana@groomers.example. Press Ctrl+C to stop.
Forwarding to http://localhost:3000/bluebot/webhooks
Signing secret for local testing: whsec_LcL9pQ2wErT7yUiO1aSdF3gH5jK8zX0cSet BLUEBOT_WEBHOOK_SECRET in your local environment to the secret it printed. It stays the same each time you run bluebot listen on this computer, so you set it once.
Make something happen
Text your BlueBot number from your phone, or text yourself from the CLI in another terminal:
bluebot send +15555550123 "Testing my handler"Each event is forwarded as it comes, and the CLI prints what your handler answered:
19:41:02 message.queued evt_6Tn2xL9mQ0cR4bW8kV1sHd → 204 (12 ms)
19:41:05 message.sent evt_7Uo3yM0nR1dS5cX9lW2tIe → 204 (9 ms)
19:41:31 message.received evt_8Vp4zN1oS2eT6dY0mX3uJf → 500 (41 ms)Options:
| Option | What it does |
|---|---|
--forward-to <url> | Where to post each event. Any address your computer can reach, http://localhost included. |
--events <types> | Only these types, comma-separated: --events message.received,message.failed. |
--json | Print each event as one line of JSON instead of the summary. |
What it doesn't do:
- It forwards only events that happen after it starts.
- It doesn't retry. When your handler errors, it prints the status and moves on; send the event again by making it happen again.
- It doesn't forward test events from Send a test event, which never reach the event feed.
- Its secret is for your laptop only. Your deployed handler verifies with the webhook's own secret.
Use a tunnel
A tunnel gives your laptop a public https:// address, so a real webhook can deliver to it. Use it for the last check before you deploy, with real deliveries and retries and the webhook's own secret.
Open a tunnel to your local port
With :
ngrok http 3000Or with Cloudflare's , which needs no account:
cloudflared tunnel --url http://localhost:3000Each prints an https:// address, such as https://example-words.trycloudflare.com.
Make a webhook to it
/v1/webhookscurl -X POST "https://usebluebot.com/api/v1/webhooks" \
-H "Authorization: Bearer $BLUEBOT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: dev-tunnel-2026-11-13" \
-d '{
"url": "https://example-words.trycloudflare.com/bluebot/webhooks",
"events": [
"message.received",
"message.sent",
"message.failed"
],
"description": "Dev tunnel (delete when done)"
}'Put the secret from the answer in your local BLUEBOT_WEBHOOK_SECRET.
Send a test event, then a real text
Send a test event to the new webhook, then text your BlueBot number from your phone and watch the real message.received arrive.
Delete the webhook when you're done
/v1/webhooks/wh_2Bf8QzD4kJmA6nXwEuTr1ccurl -X DELETE "https://usebluebot.com/api/v1/webhooks/wh_2Bf8QzD4kJmA6nXwEuTr1c" \
-H "Authorization: Bearer $BLUEBOT_API_KEY"