Skip to main content

Documentation Index

Fetch the complete documentation index at: https://v2-docs.riseworks.io/llms.txt

Use this file to discover all available pages before exploring further.

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