Receive webhooks locally
Live events forwarded to localhost, signed by Wajub, with no tunnel.
wajub listen forwards your account's events to a local URL you choose, signed like production
deliveries, without deploying your handler or opening a tunnel.
wajub listen --forward-to localhost:3000/webhooks/wajubNothing of yours is exposed: no public URL, no inbound port.
What you see
$ wajub listen --forward-to localhost:3000/webhooks/wajub
Ready! Streaming sandbox events for your account.
Your webhook signing secret is whsec_test_xxxxxxxxxxxxxxxx… (^C to quit)
It stays the same between runs: set WAJUB_WEBHOOK_SECRET=whsec_test_xxxxxxxxxxxxxxxx… in your app's .env once.
Forwarding to http://localhost:3000/webhooks/wajub
subscribed — waiting for events…
6:37:35 AM customer.created evt_test_Example00000000000000001 → 200 http://localhost:3000/webhooks/wajub
6:37:35 AM payment.created evt_test_Example00000000000000002 → 200 http://localhost:3000/webhooks/wajub
6:37:36 AM payment.processing evt_test_Example00000000000000003 → 200 http://localhost:3000/webhooks/wajub
6:37:39 AM payment.succeeded evt_test_Example00000000000000004 → 200 http://localhost:3000/webhooks/wajub
6:37:39 AM balance.updated evt_test_Example00000000000000005 → 200 http://localhost:3000/webhooks/wajub
6:37:39 AM fee.charged evt_test_Example00000000000000006 → 200 http://localhost:3000/webhooks/wajubThe number on the right is your own endpoint's answer, so a 500 there is your handler failing,
not the CLI. The run above is a single wajub trigger payment.succeeded: one command produced six
events, which is what a real payment produces too.
That is the fastest way to discover how many types your handler is quietly ignoring.
The forwarded request is a real webhook
Each local delivery is signed the way a production one is, with the same body and headers.
| Header | Value |
|---|---|
X-Wajub-Signature | v1= followed by the HMAC |
X-Wajub-Timestamp | Unix seconds, part of the signed string |
X-Wajub-Event | The event type |
X-Wajub-Delivery-Id | whd_ followed by a UUID, the same for every delivery of an event to this listener |
So your verification code runs for real on your laptop, instead of being skipped behind an
if (process.env.NODE_ENV === 'development') that hides a bug until production.
Because the body is the one production sends, a handler that verifies the signature over the raw body passes locally exactly when it will pass deployed.
The signing secret is the same every time you run wajub listen for this account and environment,
one for sandbox and one for live. Put it in your .env once and your handler verifies without any
other change. It is not a registered endpoint's secret: each endpoint keeps its own.
export WAJUB_WEBHOOK_SECRET=$(wajub listen --print-secret)
npm run dev--print-secret prints the secret alone and exits, so it also works in a script. With a live key,
add --live (wajub listen --live --print-secret): without it the command fails, and $(…) then
captures an empty secret.
`--unsigned` removes the signature entirely
The flag exists for wedging a payload into a handler that cannot verify yet. It forwards events unsigned, so your endpoint has no way to tell Wajub apart from anyone else who can reach it. Never leave it in a script, and never use it against an endpoint that is also deployed.
If your app answers on an https:// address with a self-signed or local-CA certificate, such as a
local site with a self-signed certificate, add --insecure-tls. It skips the certificate check for
the forward to your handler only; the CLI's calls to the Wajub API stay verified. Pointing
NODE_EXTRA_CA_CERTS at your local CA works too.
Flags
| Flag | Effect |
|---|---|
--forward-to | Where to POST. http:// is assumed when the scheme is missing |
--events | Comma separated types, wildcards allowed (payment.*), instead of everything |
--live | Required with a live key: real customers' events reach this machine |
--print-secret | Print the signing secret and exit |
--unsigned | Forward without the signature. See the warning above |
--insecure-tls | Accept a self-signed or local-CA certificate on an https:// forward URL |
--profile | Which account and environment to stream |
# Only what your handler currently implements
wajub listen \
--forward-to localhost:3000/webhooks/wajub \
--events "payment.*,refund.succeeded"
# Watch without forwarding anywhere
wajub listenWith no --forward-to, it prints events and posts nothing. That is the fastest way to find out
whether an event is being emitted at all, before you blame your handler. A malformed --events
value stops the command before anything starts, and Wajub only sends the types you asked for.
Things worth knowing
The environment comes from your profile. A sandbox key streams sandbox events. A live key
streams live ones, and only with --live: real customers' data then reaches your machine.
wajub doctor tells you which you are on, and --profile overrides it for one run.
It needs a secret key, or a restricted key with event.read. The key wajub login creates has
full access unless you restrict it. A publishable key cannot listen, and a key revoked by
wajub logout stops a running stream.
You do not need a registered endpoint. The stream is independent of POST /webhooks. An
account with no endpoint at all still streams everything to wajub listen.
It does not replace your registered endpoints. If you also have a live endpoint subscribed to the same events, it keeps receiving them. Listening does not divert anything.
Your listeners show up in the Dashboard. Konsole › Webhooks lists each running wajub listen
for the current environment, with what your handler answered to the latest events.
It survives a sleeping laptop. It reconnects on its own, nothing to do.
Twenty at a time. An account can have 20 wajub listen sessions open per environment; the
twenty-first is refused with 429. Stopping a command with ^C frees its place at once.
A 503 means the stream could not be opened. The command stops before anything starts. Try again in a moment; if it persists, send support the request id the
CLI prints. Every other command keeps working.
Nothing is buffered. Events produced while the command is not running are not replayed when
you start it. To recover those, read wajub events list and
resend the ones you want.
The loop
Two terminals, and you never touch a phone.
# 1: forward everything to your handler
wajub listen --forward-to localhost:3000/webhooks/wajub
# 2: make something happen
wajub trigger payment.succeeded
wajub trigger payment.failed --amount 2500Write the handler, save, trigger again. The loop is a few seconds long, which is the point.