Skip to the page

Webhooks

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:

WayWhat reaches your handlerBest for
Send a test eventA signed example of any event type, posted to a webhook's real addressChecking a deployed handler, and every event type, without sending a text
bluebot listenYour account's real events, forwarded to localhost and signed with a local secretWriting a handler on your laptop, with no tunnel and no webhook to set up
A tunnelReal deliveries from a real webhook, with its retries and its secretThe 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:

POST/v1/webhooks/wh_1Hq2LxW9cVbN3mKpRtYs0a/testSend a test event →
curl -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:

Response200
{
  "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 gotIt meansDo next
state: "delivered" and a 2xx in last_statusYour handler verified the post and answered.Try the other types your handler takes.
state: "pending" with last_status: 400Your handler rejected it, usually the signature.Check the secret and the raw body. When a check fails
state: "pending" with last_status: 500Your 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 setBlueBot couldn't reach the address or got no answer in 10 seconds.Check the address is public and quick to answer.
state: "pending", attempts: 0The webhook is paused, so nothing was sent.Resume it, then test again.

What to know about test events:

  • The body is the event catalog'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 gets 404 not_found. Handle that the way you would in production.
  • The body says it is a test: it has "test": true beside type, 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 shows test: true in 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

Terminal
npm install -g @usebluebot/cli
bluebot login

bluebot 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. CLI

Start listening

Terminal
bluebot listen --forward-to http://localhost:3000/bluebot/webhooks
Output
Listening for events on dana@groomers.example. Press Ctrl+C to stop.
Forwarding to http://localhost:3000/bluebot/webhooks
Signing secret for local testing: whsec_LcL9pQ2wErT7yUiO1aSdF3gH5jK8zX0c

Set 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:

Terminal
bluebot send +15555550123 "Testing my handler"

Each event is forwarded as it comes, and the CLI prints what your handler answered:

Output
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:

OptionWhat 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.
--jsonPrint 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:

Terminal
ngrok http 3000

Or with Cloudflare's cloudflared, which needs no account:

Terminal
cloudflared tunnel --url http://localhost:3000

Each prints an https:// address, such as https://example-words.trycloudflare.com.

Make a webhook to it

POST/v1/webhooksMake a webhook →
curl -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

DELETE/v1/webhooks/wh_2Bf8QzD4kJmA6nXwEuTr1cDelete a webhook →
curl -X DELETE "https://usebluebot.com/api/v1/webhooks/wh_2Bf8QzD4kJmA6nXwEuTr1c" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY"

Next