Testing Webhooks
Validate your webhook receiving endpoint, signature verification, and simulate error scenarios with Konsole.
Testing webhooks locally is essential: they are how you will be notified of every state change. Here is how to validate your implementation end to end.
Expose your local endpoint
Your local server is not accessible from the internet. Use a tunnel so that Wajub can send webhooks to it:
# With ngrok
ngrok http 3000
# With localhost.run
ssh -R 80:localhost:3000 nokey@localhost.run
# With Cloudflare Tunnel
cloudflared tunnel --url http://localhost:3000Copy the generated public URL and configure it in Konsole > Webhooks (Sandbox environment).
Signature verification
Your endpoint must always verify the signature before processing the event. The signature
is computed over {timestamp}.{raw_json_body}, not the body alone — see
signature verification for the full mechanics. Here is
a complete test endpoint:
const express = require('express');
const crypto = require('crypto');
const app = express();
const SECRET = process.env.WAJUB_WEBHOOK_SECRET;
function verify(rawBody, timestamp, signatureHeader) {
const expected = 'v1=' + crypto.createHmac('sha256', SECRET)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
app.post('/webhooks/wajub',
express.raw({ type: '*/*' }),
(req, res) => {
const signature = req.headers['x-wajub-signature'];
const timestamp = req.headers['x-wajub-timestamp'];
const rawBody = req.body.toString('utf8');
if (!signature || !timestamp || !verify(rawBody, timestamp, signature)) {
console.error('Invalid signature');
return res.status(400).send('Signature verification failed');
}
const event = JSON.parse(rawBody);
// Acknowledge immediately
res.sendStatus(200);
// Process the event (async recommended)
console.log('Event received:', event.type, event.id);
switch (event.type) {
case 'payment.succeeded':
handlePaymentComplete(event.data);
break;
case 'payment.failed':
handlePaymentFailed(event.data);
break;
case 'transfer.succeeded':
handleTransferComplete(event.data);
break;
}
}
);Never skip verification
Without signature verification, anyone can send fake webhooks to your endpoint. This is the first vulnerability exploited in production.
Test with Konsole
There is no arbitrary event simulator — you cannot type an event type and an endpoint URL and have Konsole invent a payload for it. What you actually have:
- Trigger a real sandbox event (e.g. initialize a test payment with a test number) so a real delivery is attempted against your configured endpoint.
- Open Konsole > Webhooks → Deliveries to see the attempt, its response code, body, and duration.
- If it failed, fix your endpoint and click Retry — this resends the exact original payload with a freshly-computed signature, so you can confirm the fix without waiting for another real event.
- To just check reachability of a brand-new endpoint (no real event needed), use its Test
action under Endpoints — it sends one fixed
webhook.testpayload, signed the same way.
Webhook test scenarios
| Test to validate | How to simulate |
|---|---|
| Valid signature accepted | Trigger a real sandbox event, or use an endpoint's Test action |
Invalid signature rejected (400) | Change the secret in your .env, or recompute the HMAC without the {timestamp}. prefix |
Missing signature rejected (400) | Send a POST without the x-wajub-signature header |
Idempotency on event.id / X-Wajub-Delivery-Id | Use Konsole's Retry on a delivery your endpoint already processed → your handler should no-op |
| Asynchronous processing | Simulate a processing delay > 10s → the 200 ACK must remain fast, since 10s is the delivery timeout |
| Unknown event | Send a payload with type: 'payment.reviewed' (a made-up type) → no crash |
Automated endpoint test
const crypto = require('crypto');
const request = require('supertest');
const SECRET = 'whsec_test_abc123';
function sign(payload, timestamp) {
const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
const hmac = crypto.createHmac('sha256', SECRET);
hmac.update(signedPayload);
return 'v1=' + hmac.digest('hex');
}
it('rejects an invalid signature', async () => {
const payload = {
type: 'payment.succeeded',
id: 'evt.123',
data: { reference: 'trx.PVrU8x2kQ1', status: 'succeeded' }
};
const timestamp = String(Math.floor(Date.now() / 1000));
await request(app)
.post('/webhooks/wajub')
.set('x-wajub-signature', 'v1=fake_signature')
.set('x-wajub-timestamp', timestamp)
.send(payload)
.expect(400);
});
it('accepts a valid signature', async () => {
const payload = {
type: 'payment.succeeded',
id: 'evt.456',
data: { reference: 'trx.PVrU8x2kQ1', status: 'succeeded' }
};
const timestamp = String(Math.floor(Date.now() / 1000));
await request(app)
.post('/webhooks/wajub')
.set('x-wajub-signature', sign(payload, timestamp))
.set('x-wajub-timestamp', timestamp)
.send(payload)
.expect(200);
});Automate in your CI
Integrate these tests into your CI pipeline. Use a dedicated test secret and verify that each deployment keeps signature validation functional.
Related pages