Testing Webhooks
Tools and strategies for testing webhook integrations locally.
Testing webhooks requires exposing a local endpoint to the internet. Here are the recommended approaches.
Using ngrok
ngrok creates a public URL that tunnels to your local server.
# Start your local webhook handler
node server.js # listening on port 3000
# In another terminal, start ngrok
ngrok http 3000ngrok will display a public URL like https://abc123.ngrok.io. Use this as your webhook URL:
const webhook = await renta.webhooks.create({
url: 'https://abc123.ngrok.io/webhooks/renta',
events: ['booking.created', 'payment.received'],
});Use test API keys (renta_sk_test_...) when testing webhooks. Test events don't affect real data.
Using Cloudflare Tunnel
# Install cloudflared
brew install cloudflare/cloudflare/cloudflared
# Start a tunnel
cloudflared tunnel --url http://localhost:3000Manual Testing with cURL
Test your webhook handler locally by simulating a webhook payload:
# Generate a test signature
TIMESTAMP=$(date +%s)
BODY='{"id":"evt_test","type":"booking.created","data":{"id":"bk_test","status":"pending"},"tenant_id":"test","created_at":"2026-01-01T00:00:00Z"}'
SIGNATURE=$(echo -n "${TIMESTAMP}.${BODY}" | openssl dgst -sha256 -hmac "whsec_your_test_secret" | cut -d' ' -f2)
# Send the test webhook
curl -X POST http://localhost:3000/webhooks/renta \
-H "Content-Type: application/json" \
-H "renta-signature: t=${TIMESTAMP},v1=${SIGNATURE}" \
-d "${BODY}"Test Helper Function
Create a helper for generating test webhook payloads in your test suite:
import { createHmac } from 'crypto';
export function createTestWebhookPayload(
type: string,
data: Record<string, unknown>,
secret: string,
) {
const timestamp = Math.floor(Date.now() / 1000);
const body = JSON.stringify({
id: `evt_test_${Date.now()}`,
type,
data,
tenant_id: 'test_tenant',
created_at: new Date().toISOString(),
});
const signature = createHmac('sha256', secret)
.update(`${timestamp}.${body}`)
.digest('hex');
return {
body,
headers: {
'content-type': 'application/json',
'renta-signature': `t=${timestamp},v1=${signature}`,
},
};
}
// Usage in tests
const { body, headers } = createTestWebhookPayload(
'booking.created',
{ id: 'bk_test', status: 'pending', total: 10800 },
process.env.RENTA_WEBHOOK_SECRET!,
);
const response = await fetch('http://localhost:3000/webhooks/renta', {
method: 'POST',
headers,
body,
});
expect(response.status).toBe(200);Testing Checklist
- Webhook handler responds with 200 for valid signatures
- Webhook handler responds with 400 for invalid signatures
- Webhook handler responds with 400 for expired timestamps
- Events are processed idempotently (duplicate
event.idis safe) - Handler responds within 30 seconds (use async processing for slow operations)
- All subscribed event types are handled (even if just logged)
- Unrecognized event types don't crash the handler
Debugging Tips
- Log everything — Log the full event payload and processing result during development
- Check the signature header — Ensure you're reading
renta-signature(notx-renta-signature) - Raw body — Make sure your framework isn't parsing the JSON body before you verify the signature
- Timezone — Timestamps are Unix seconds (UTC). Make sure your clock is accurate.
- Secret rotation — If verification suddenly fails, check if the webhook secret was rotated