Overview
The WebhooksClient provides methods for verifying inbound webhooks from Revstack Cloud. It uses HMAC-SHA256 with constant-time comparison to prevent timing attacks, plus timestamp validation for replay protection.
Methods
constructEvent()
Verify the signature of an incoming webhook and parse the event payload.
const event = revstack . webhooks . constructEvent (
payload ,
signature ,
secret ,
tolerance
);
Raw request body. Must NOT be parsed JSON. Use express.raw({ type: "application/json" }) or equivalent to preserve the raw body.
Value of the revstack-signature HTTP header. Format: t=<timestamp>,v1=<signature>
Webhook signing secret from the Revstack Dashboard. Example: "whsec_..."
Maximum age of the webhook in seconds. Default: 300 (5 minutes)Set to 0 to disable replay protection (not recommended).
The verified and parsed webhook event. Event type. Examples: "subscription.created", "invoice.paid", "customer.updated"
Event payload containing the relevant resource data.
ISO 8601 timestamp of when the event occurred.
Throws:
SignatureVerificationError - If the signature is invalid, the header is malformed, or the timestamp exceeds the tolerance.
Usage Examples
Express.js
import express from "express" ;
import { Revstack , SignatureVerificationError } from "@revstackhq/node" ;
const app = express ();
const revstack = new Revstack ({ secretKey: process . env . REVSTACK_SECRET_KEY ! });
// Use express.raw() to preserve the raw body
app . post (
"/webhooks/revstack" ,
express . raw ({ type: "application/json" }),
( req , res ) => {
try {
const event = revstack . webhooks . constructEvent (
req . body ,
req . headers [ "revstack-signature" ] as string ,
process . env . REVSTACK_WEBHOOK_SECRET !
);
console . log ( `Received event: ${ event . type } ` );
// Handle the event
switch ( event . type ) {
case "subscription.created" :
await handleSubscriptionCreated ( event . data );
break ;
case "invoice.paid" :
await handleInvoicePaid ( event . data );
break ;
default :
console . log ( `Unhandled event type: ${ event . type } ` );
}
res . sendStatus ( 200 );
} catch ( error ) {
if ( error instanceof SignatureVerificationError ) {
console . error ( "Invalid signature:" , error . message );
res . status ( 400 ). send ( "Invalid signature" );
} else {
console . error ( "Webhook error:" , error );
res . status ( 500 ). send ( "Webhook processing failed" );
}
}
}
);
Next.js API Route
// pages/api/webhooks/revstack.ts
import { NextApiRequest , NextApiResponse } from "next" ;
import { Revstack , SignatureVerificationError } from "@revstackhq/node" ;
import { buffer } from "micro" ;
const revstack = new Revstack ({ secretKey: process . env . REVSTACK_SECRET_KEY ! });
// Disable Next.js body parsing
export const config = {
api: {
bodyParser: false ,
},
};
export default async function handler (
req : NextApiRequest ,
res : NextApiResponse
) {
if ( req . method !== "POST" ) {
return res . status ( 405 ). send ( "Method not allowed" );
}
try {
const rawBody = await buffer ( req );
const signature = req . headers [ "revstack-signature" ] as string ;
const event = revstack . webhooks . constructEvent (
rawBody ,
signature ,
process . env . REVSTACK_WEBHOOK_SECRET !
);
// Handle the event
await handleWebhookEvent ( event );
res . status ( 200 ). json ({ received: true });
} catch ( error ) {
if ( error instanceof SignatureVerificationError ) {
return res . status ( 400 ). send ( "Invalid signature" );
}
console . error ( "Webhook error:" , error );
res . status ( 500 ). send ( "Internal server error" );
}
}
Fastify
import Fastify from "fastify" ;
import { Revstack , SignatureVerificationError } from "@revstackhq/node" ;
const fastify = Fastify ();
const revstack = new Revstack ({ secretKey: process . env . REVSTACK_SECRET_KEY ! });
fastify . post (
"/webhooks/revstack" ,
{
config: {
rawBody: true , // Preserve raw body
},
},
async ( request , reply ) => {
try {
const event = revstack . webhooks . constructEvent (
request . rawBody ! ,
request . headers [ "revstack-signature" ] as string ,
process . env . REVSTACK_WEBHOOK_SECRET !
);
console . log ( `Event: ${ event . type } ` );
// Process the event
await processWebhook ( event );
reply . code ( 200 ). send ({ ok: true });
} catch ( error ) {
if ( error instanceof SignatureVerificationError ) {
reply . code ( 400 ). send ({ error: "Invalid signature" });
} else {
reply . code ( 500 ). send ({ error: "Internal error" });
}
}
}
);
Event Types
Revstack sends the following webhook events:
Customer Events
customer.created - New customer created
customer.updated - Customer information updated
customer.deleted - Customer deleted
Subscription Events
subscription.created - New subscription started
subscription.updated - Subscription modified (plan change, etc.)
subscription.canceled - Subscription canceled
subscription.trial_ending - Trial period ending soon
Invoice Events
invoice.created - New invoice generated
invoice.paid - Invoice successfully paid
invoice.payment_failed - Payment failed
invoice.voided - Invoice voided
Entitlement Events
entitlement.limit_reached - Customer reached their usage limit
entitlement.limit_exceeded - Customer exceeded their usage limit
Best Practices
1. Always Verify Signatures
Never process webhooks without verification:
// ❌ Bad: Processing unverified webhooks
app . post ( "/webhooks" , ( req , res ) => {
const event = req . body ; // Unverified!
processEvent ( event );
});
// ✅ Good: Always verify
app . post ( "/webhooks" , express . raw ({ type: "application/json" }), ( req , res ) => {
const event = revstack . webhooks . constructEvent (
req . body ,
req . headers [ "revstack-signature" ],
secret
);
processEvent ( event );
});
2. Use Raw Body
The signature is computed over the raw request body:
// ❌ Bad: Body already parsed
app . use ( express . json ()); // Parses all routes
app . post ( "/webhooks" , ( req , res ) => {
const event = revstack . webhooks . constructEvent (
req . body , // Already parsed, verification will fail!
...
);
});
// ✅ Good: Preserve raw body for webhook route
app . post (
"/webhooks" ,
express . raw ({ type: "application/json" }),
( req , res ) => {
const event = revstack . webhooks . constructEvent (
req . body , // Raw Buffer
...
);
}
);
3. Handle Errors Gracefully
try {
const event = revstack . webhooks . constructEvent ( ... );
await processEvent ( event );
res . sendStatus ( 200 );
} catch ( error ) {
if ( error instanceof SignatureVerificationError ) {
// Invalid signature - reject the request
console . error ( "Webhook signature verification failed:" , error . message );
return res . status ( 400 ). send ( "Invalid signature" );
}
// Other errors - still acknowledge receipt to avoid retries
console . error ( "Webhook processing error:" , error );
res . sendStatus ( 500 );
}
4. Implement Idempotency
Webhooks may be delivered multiple times:
const processedEvents = new Set < string >();
app . post ( "/webhooks" , async ( req , res ) => {
const event = revstack . webhooks . constructEvent ( ... );
// Check if already processed
if ( processedEvents . has ( event . id )) {
return res . sendStatus ( 200 ); // Already handled
}
await processEvent ( event );
processedEvents . add ( event . id );
res . sendStatus ( 200 );
});
5. Respond Quickly
Return 200 quickly and process asynchronously:
app . post ( "/webhooks" , async ( req , res ) => {
const event = revstack . webhooks . constructEvent ( ... );
// Queue for async processing
await queue . add ( "webhook" , { event });
// Respond immediately
res . sendStatus ( 200 );
});