Receive webhooks locally
Live events forwarded to localhost, signed, with no tunnel.
Your localhost is not reachable from the internet, which is why testing a webhook handler
usually means deploying it or opening a tunnel. wajub listen does neither. It opens a stream
from your machine to Wajub, and reposts what arrives to a local URL you choose.
wajub listen --forward-to localhost:3000/webhooks/wajubNothing of yours is exposed. There is no public URL, no third party proxy, and 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_c7f543a6a4058db0dfb566dd91e922bc (^C to quit)
6:37:35 AM customer.created evt_test_NGHXbVLyHqPLOhNY3HKoXMca → 200 http://localhost:3000/webhooks/wajub
6:37:35 AM payment.created evt_test_wUpoY1JcMvjoUEs8FUdBkbuw → 200 http://localhost:3000/webhooks/wajub
6:37:36 AM payment.processing evt_test_7wP9Ty2ITqLSzVgWaU1VFH8U → 200 http://localhost:3000/webhooks/wajub
6:37:39 AM payment.succeeded evt_test_HObNwAnDRT54qb28OxW7fwQJ → 200 http://localhost:3000/webhooks/wajub
6:37:39 AM balance.updated evt_test_8pymkrpN71IQnazTsIwSxEVB → 200 http://localhost:3000/webhooks/wajub
6:37:39 AM fee.charged evt_test_u5zOJ37yKP2C0HSQZAKbUtme → 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
The CLI signs what it forwards, with the same scheme production uses, and sends the same 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 | This attempt, whd_ followed by a UUID |
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.
Two fields read differently here than in a production delivery. pending_webhooks is 0 because
the stream is not a registered endpoint, and the User-Agent is wajub-cli rather than
Halo/1.0. If your handler branches on either, it will behave differently locally than it does
deployed, which is a trap worth removing rather than working around.
The secret printed at startup is generated for this run. Export it and your handler verifies without any other change.
export WAJUB_WEBHOOK_SECRET=whsec_test_c7f543a6a4058db0dfb566dd91e922bc
npm run devA new secret on every run gets tedious. Pin one and put it in your .env once.
wajub listen --forward-to localhost:3000/webhooks/wajub --secret whsec_local_dev`--skip-verify` 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.
Flags
| Flag | Effect |
|---|---|
--forward-to | Where to POST. http:// is assumed when the scheme is missing |
--events | Comma separated types, instead of everything |
--secret | Use this signing secret rather than generating one |
--skip-verify | Forward unsigned. See the warning above |
--profile | Which account and environment to stream |
# Only what your handler currently implements
wajub listen \
--forward-to localhost:3000/webhooks/wajub \
--events payment.succeeded,payment.failed,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.
Four things worth knowing
The environment comes from your profile. A sandbox key streams sandbox events, a live key
streams live ones. wajub doctor tells you which you are on, and --profile overrides it for
one run.
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.
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.
Internal events never appear
transaction.* events are checkout telemetry used inside the platform. They are filtered out
before the stream, so they never reach wajub listen and cannot be resent.
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.