Webhook Verification
Verify webhook signatures using HMAC-SHA256 to ensure requests come from Renta.
Every webhook from Renta includes a renta-signature header containing an HMAC-SHA256 signature. Always verify this signature before processing the event.
How Signatures Work
- Renta generates a timestamp and computes
HMAC-SHA256(timestamp + "." + body, signing_secret) - The
renta-signatureheader contains:t=<timestamp>,v1=<signature> - Your server recomputes the signature and compares
Using the SDK (Recommended)
The SDK provides a built-in verification method:
import express from 'express';
import { Renta, type WebhookEvent } from '@renta/sdk';
const app = express();
app.post(
'/webhooks/renta',
express.raw({ type: 'application/json' }),
async (req, res) => {
const signature = req.headers['renta-signature'] as string;
let event: WebhookEvent;
try {
event = await Renta.webhooks.verify(
req.body, // raw body (Buffer or string)
signature, // renta-signature header
process.env.RENTA_WEBHOOK_SECRET!,
);
} catch (err) {
console.error('Webhook verification failed:', err);
return res.status(400).send('Invalid signature');
}
// Process the verified event
switch (event.type) {
case 'booking.created':
await handleNewBooking(event.data);
break;
case 'booking.cancelled':
await handleCancellation(event.data);
break;
case 'payment.received':
await handlePayment(event.data);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.status(200).json({ received: true });
},
);
app.listen(3000);import { Renta, type WebhookEvent } from '@renta/sdk';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const body = await req.text();
const signature = req.headers.get('renta-signature')!;
let event: WebhookEvent;
try {
event = await Renta.webhooks.verify(
body,
signature,
process.env.RENTA_WEBHOOK_SECRET!,
);
} catch {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 400 },
);
}
// Process the verified event
switch (event.type) {
case 'booking.created':
console.log('New booking:', event.data);
break;
case 'payment.received':
console.log('Payment:', event.data);
break;
}
return NextResponse.json({ received: true });
}import Fastify from 'fastify';
import { Renta, type WebhookEvent } from '@renta/sdk';
const fastify = Fastify();
// Disable JSON parsing for this route (we need raw body)
fastify.addContentTypeParser(
'application/json',
{ parseAs: 'string' },
(req, body, done) => done(null, body),
);
fastify.post('/webhooks/renta', async (request, reply) => {
const signature = request.headers['renta-signature'] as string;
const body = request.body as string;
let event: WebhookEvent;
try {
event = await Renta.webhooks.verify(
body,
signature,
process.env.RENTA_WEBHOOK_SECRET!,
);
} catch {
return reply.status(400).send({ error: 'Invalid signature' });
}
console.log(`Received ${event.type}:`, event.data);
return { received: true };
});
fastify.listen({ port: 3000 });You must access the raw request body (not parsed JSON) for signature verification. Parsing the JSON first may alter whitespace and break the signature check.
Manual Verification
If you're not using the SDK, here's how to verify manually:
import { createHmac, timingSafeEqual } from 'crypto';
function verifyWebhookSignature(
body: string,
signatureHeader: string,
secret: string,
toleranceSeconds = 300, // 5-minute tolerance
): boolean {
// Parse the signature header
const parts = signatureHeader.split(',');
const timestamp = parts
.find(p => p.startsWith('t='))
?.slice(2);
const signature = parts
.find(p => p.startsWith('v1='))
?.slice(3);
if (!timestamp || !signature) {
throw new Error('Invalid signature header format');
}
// Check timestamp tolerance (prevent replay attacks)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > toleranceSeconds) {
throw new Error('Timestamp outside tolerance window');
}
// Compute expected signature
const payload = `${timestamp}.${body}`;
const expected = createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Timing-safe comparison
const sigBuffer = Buffer.from(signature, 'hex');
const expBuffer = Buffer.from(expected, 'hex');
if (sigBuffer.length !== expBuffer.length) {
return false;
}
return timingSafeEqual(sigBuffer, expBuffer);
}Signature Header Format
renta-signature: t=1711843200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd| Component | Description |
|---|---|
t | Unix timestamp (seconds) when the event was sent |
v1 | HMAC-SHA256 hex digest of {timestamp}.{body} |
Security Best Practices
- Always verify — Never process webhook events without signature verification
- Use raw body — Parse the body only after verification
- Check timestamp — Reject events older than 5 minutes to prevent replay attacks
- Use timing-safe comparison — Prevent timing attacks with
timingSafeEqual - Return 200 quickly — Process events asynchronously if they take > 5 seconds
- Handle idempotently — Events may be delivered more than once; use
event.idfor deduplication