Renta Docs

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 3000

ngrok 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:3000

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

test/helpers/webhook.ts
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.id is 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

  1. Log everything — Log the full event payload and processing result during development
  2. Check the signature header — Ensure you're reading renta-signature (not x-renta-signature)
  3. Raw body — Make sure your framework isn't parsing the JSON body before you verify the signature
  4. Timezone — Timestamps are Unix seconds (UTC). Make sure your clock is accurate.
  5. Secret rotation — If verification suddenly fails, check if the webhook secret was rotated