Skip to main content
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

Webhook Signature Format

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

Parse Signature Header

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

1

Secret Management

Store webhook secret securely Use environment variables Never commit secret to version control Rotate secrets regularly
2

Validation

Validate all incoming webhooks Check timestamp tolerance Verify signature format Handle validation errors
3

Monitoring

Log validation failures Monitor webhook activity Set up alerts for suspicious activity Track validation metrics
4

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

  1. Security Overview - Complete security architecture
  2. Secondary Wallets - Using dedicated wallets
  3. Best Practices - Security best practices
I