Webhook validation is crucial for ensuring the security and authenticity of incoming webhook events from Rise B2B API.
Webhook Security Overview
Webhooks provide real-time notifications but must be validated to ensure they come from Rise and haven’t been tampered with. Our webhook validation uses HMAC-SHA256 signatures for security.
Signature Verification
HMAC-SHA256 signatures
Timestamp validation
Replay attack prevention
Tamper detection
Security Benefits
Authentic source verification
Data integrity assurance
Attack prevention
Compliance requirements
Rise sends webhooks with a signature header in this format:
Rise-Signature: t=1705312200,v1=abc123def456...
Where:
t = Unix timestamp
v1 = HMAC-SHA256 signature
Using the Webhook Validator
Basic Validation
import { WebhookValidator } from '@riseworks/sdk' ;
// Initialize validator with your webhook secret
const validator = new WebhookValidator ({
secret: process . env . WEBHOOK_SECRET
});
// Express.js webhook endpoint
app . post ( '/webhooks/rise' , ( req , res ) => {
try {
// Validate the webhook signature
const isValid = validator . validateEvent (
req . body ,
req . headers [ 'rise-signature' ]
);
if ( isValid ) {
// Process the webhook
console . log ( 'Webhook validated:' , req . body );
res . status ( 200 ). json ({ received: true });
} else {
res . status ( 400 ). json ({ error: 'Invalid signature' });
}
} catch ( error ) {
console . error ( 'Webhook validation error:' , error );
res . status ( 400 ). json ({ error: error . message });
}
});
Safe Validation (Returns Result)
import { WebhookValidator } from '@riseworks/sdk' ;
const validator = new WebhookValidator ({
secret: process . env . WEBHOOK_SECRET
});
app . post ( '/webhooks/rise' , ( req , res ) => {
// Use safe validation that returns a result object
const result = validator . validateEventSafe (
req . body ,
req . headers [ 'rise-signature' ]
);
if ( result . valid ) {
// Process webhook
console . log ( 'Webhook processed:' , req . body );
res . status ( 200 ). json ({ received: true });
} else {
console . error ( 'Webhook validation failed:' , result . error );
res . status ( 400 ). json ({ error: result . error });
}
});
Manual Validation
import { WebhookValidator } from '@riseworks/sdk' ;
const validator = new WebhookValidator ({
secret: process . env . WEBHOOK_SECRET
});
// Parse signature header manually
const signatureHeader = req . headers [ 'rise-signature' ];
const { timestamp , signature } = validator . parseSignatureHeader ( signatureHeader );
console . log ( 'Timestamp:' , timestamp );
console . log ( 'Signature:' , signature );
Validate Timestamp
// Check if webhook is within acceptable time range
const tolerance = 300 ; // 5 minutes
const now = Math . floor ( Date . now () / 1000 );
if ( Math . abs ( now - timestamp ) > tolerance ) {
throw new Error ( 'Webhook timestamp too old' );
}
Compare Signatures
// Generate expected signature
const expectedSignature = validator . generateSignature ( req . body , timestamp );
// Compare signatures securely
const isValid = validator . compareSignatures ( signature , expectedSignature );
if ( isValid ) {
console . log ( 'Signature verified' );
} else {
console . log ( 'Signature verification failed' );
}
Complete Validation Example
import { WebhookValidator } from '@riseworks/sdk' ;
import express from 'express' ;
const app = express ();
app . use ( express . json ());
const validator = new WebhookValidator ({
secret: process . env . WEBHOOK_SECRET ,
tolerance: 300 // 5 minutes
});
app . post ( '/webhooks/rise' , ( req , res ) => {
try {
// Validate webhook
const isValid = validator . validateEvent (
req . body ,
req . headers [ 'rise-signature' ]
);
if ( ! isValid ) {
return res . status ( 400 ). json ({ error: 'Invalid webhook signature' });
}
// Process webhook based on event type
const { event , data } = req . body ;
switch ( event ) {
case 'payment.completed' :
handlePaymentCompleted ( data );
break ;
case 'payment.failed' :
handlePaymentFailed ( data );
break ;
case 'invite.accepted' :
handleInviteAccepted ( data );
break ;
default :
console . log ( 'Unhandled event:' , event );
}
res . status ( 200 ). json ({ received: true });
} catch ( error ) {
console . error ( 'Webhook processing error:' , error );
res . status ( 500 ). json ({ error: 'Internal server error' });
}
});
function handlePaymentCompleted ( data ) {
console . log ( 'Payment completed:' , data . payment_id );
// Update database, send notifications, etc.
}
function handlePaymentFailed ( data ) {
console . log ( 'Payment failed:' , data . payment_id );
// Handle failed payment
}
function handleInviteAccepted ( data ) {
console . log ( 'Invite accepted:' , data . invite_id );
// Update team member status
}
app . listen ( 3000 , () => {
console . log ( 'Webhook server running on port 3000' );
});
Error Handling
Common Validation Errors
try {
const isValid = validator . validateEvent ( req . body , req . headers [ 'rise-signature' ]);
} catch ( error ) {
switch ( error . message ) {
case 'Missing signature header' :
console . error ( 'No Rise-Signature header found' );
break ;
case 'Invalid signature format' :
console . error ( 'Signature header format is invalid' );
break ;
case 'Webhook timestamp too old' :
console . error ( 'Webhook timestamp is outside tolerance window' );
break ;
case 'Invalid signature' :
console . error ( 'Signature verification failed' );
break ;
default :
console . error ( 'Unknown validation error:' , error . message );
}
}
Logging and Monitoring
// Webhook validation metrics
const webhookMetrics = {
total: 0 ,
valid: 0 ,
invalid: 0 ,
errors: []
};
app . post ( '/webhooks/rise' , ( req , res ) => {
webhookMetrics . total ++ ;
try {
const isValid = validator . validateEvent (
req . body ,
req . headers [ 'rise-signature' ]
);
if ( isValid ) {
webhookMetrics . valid ++ ;
// Process webhook
} else {
webhookMetrics . invalid ++ ;
res . status ( 400 ). json ({ error: 'Invalid signature' });
return ;
}
} catch ( error ) {
webhookMetrics . errors . push ({
timestamp: new Date (). toISOString (),
error: error . message
});
res . status ( 400 ). json ({ error: error . message });
return ;
}
res . status ( 200 ). json ({ received: true });
});
// Log metrics periodically
setInterval (() => {
console . log ( 'Webhook metrics:' , webhookMetrics );
}, 60000 ); // Every minute
Security Best Practices
Environment Configuration
# .env file
WEBHOOK_SECRET = your_webhook_secret_here
WEBHOOK_TOLERANCE = 300
Validation Configuration
const validator = new WebhookValidator ({
secret: process . env . WEBHOOK_SECRET ,
tolerance: parseInt ( process . env . WEBHOOK_TOLERANCE || '300' )
});
Security Checklist
Secret Management
Store webhook secret securely
Use environment variables
Never commit secret to version control
Rotate secrets regularly
Validation
Validate all incoming webhooks
Check timestamp tolerance
Verify signature format
Handle validation errors
Monitoring
Log validation failures
Monitor webhook activity
Set up alerts for suspicious activity
Track validation metrics
Error Handling
Return appropriate HTTP status codes
Log detailed error information
Implement retry logic for failures
Monitor error rates
Testing Webhook Validation
Test with Sample Data
// Test webhook validation
const testWebhook = {
event: 'payment.completed' ,
data: {
payment_id: 'pay_123456789' ,
amount: '1000.00' ,
currency: 'USD'
},
timestamp: Math . floor ( Date . now () / 1000 )
};
// Generate test signature
const testSignature = validator . generateSignature (
testWebhook ,
testWebhook . timestamp
);
// Test validation
const isValid = validator . validateEvent ( testWebhook , `t= ${ testWebhook . timestamp } ,v1= ${ testSignature } ` );
console . log ( 'Test validation result:' , isValid );
Unit Tests
import { WebhookValidator } from '@riseworks/sdk' ;
describe ( 'WebhookValidator' , () => {
let validator ;
beforeEach (() => {
validator = new WebhookValidator ({
secret: 'test-secret'
});
});
test ( 'should validate correct signature' , () => {
const payload = { event: 'test' , data: {} };
const timestamp = Math . floor ( Date . now () / 1000 );
const signature = validator . generateSignature ( payload , timestamp );
const signatureHeader = `t= ${ timestamp } ,v1= ${ signature } ` ;
const isValid = validator . validateEvent ( payload , signatureHeader );
expect ( isValid ). toBe ( true );
});
test ( 'should reject invalid signature' , () => {
const payload = { event: 'test' , data: {} };
const signatureHeader = 't=1234567890,v1=invalid-signature' ;
const isValid = validator . validateEvent ( payload , signatureHeader );
expect ( isValid ). toBe ( false );
});
test ( 'should reject old timestamp' , () => {
const payload = { event: 'test' , data: {} };
const timestamp = Math . floor ( Date . now () / 1000 ) - 600 ; // 10 minutes ago
const signature = validator . generateSignature ( payload , timestamp );
const signatureHeader = `t= ${ timestamp } ,v1= ${ signature } ` ;
const isValid = validator . validateEvent ( payload , signatureHeader );
expect ( isValid ). toBe ( false );
});
});
Next Steps
Security Overview - Complete security architecture
Secondary Wallets - Using dedicated wallets
Best Practices - Security best practices