Renta Docs

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

  1. Renta generates a timestamp and computes HMAC-SHA256(timestamp + "." + body, signing_secret)
  2. The renta-signature header contains: t=<timestamp>,v1=<signature>
  3. Your server recomputes the signature and compares

The SDK provides a built-in verification method:

webhooks/renta.ts
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);
app/api/webhooks/renta/route.ts
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 });
}
webhooks/renta.ts
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:

manual-verify.ts
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
ComponentDescription
tUnix timestamp (seconds) when the event was sent
v1HMAC-SHA256 hex digest of {timestamp}.{body}

Security Best Practices

  1. Always verify — Never process webhook events without signature verification
  2. Use raw body — Parse the body only after verification
  3. Check timestamp — Reject events older than 5 minutes to prevent replay attacks
  4. Use timing-safe comparison — Prevent timing attacks with timingSafeEqual
  5. Return 200 quickly — Process events asynchronously if they take > 5 seconds
  6. Handle idempotently — Events may be delivered more than once; use event.id for deduplication