Register for Webhooks via API

If you haven't already, Sign Up for Sandbox Access to get your client ID and secret to work through this Build Guide!

What are Webhooks?

Webhooks (also called subscriptions or callbacks) are automated HTTP POST messages sent from Rail to your server when specific events occur. Instead of repeatedly polling our API to check for updates, webhooks push real-time notifications directly to you when something happens.

Why Use Webhooks?

  • Real-time Updates - Get notified immediately when events occur
  • Reduced API Calls - No need to poll endpoints repeatedly
  • Better Performance - Lower latency and more efficient than polling
  • Event-Driven Architecture - Build responsive applications that react to events
  • Cost Effective - Reduces API usage and infrastructure costs

Common Use Cases

  • Account Status Changes - Know when accounts are opened, frozen, or ready
  • Transaction Notifications - Get alerted when deposits or withdrawals are processed
  • Application Updates - Track customer onboarding status changes
  • Compliance Events - Receive notifications for RFIs and compliance requirements

Core API Flow Sequence

The standard webhook registration flow follows these key steps:

  1. POST /v1/subscriptions - Register for specific event types
  2. Implement webhook endpoint - Create an endpoint on your server to receive webhooks
  3. Verify webhook signatures - Validate that webhooks are from Rail
  4. Handle webhook events - Process the event data and take appropriate actions
  5. GET /v1/subscriptions - List and manage your subscriptions

Build Guide

This guide should take no longer than 20 minutes and will walk you through setting up webhooks for common scenarios.

Before You Begin: For production environments, ensure you have:

  • Your webhook callback URL ready (must be HTTPS and publicly accessible)
  • Contacted Rail to whitelist your callback URL and public key (if using request signing)
  • Your OAuth2 credentials with subscriptions:read and subscriptions:write scopes

By the end of this guide you will have:

  1. Authenticated your request
  2. Registered webhook subscriptions
  3. Implemented a webhook endpoint
  4. Verified webhook signatures
  5. Handled webhook events
  6. Managed your subscriptions

Let's go!

Authenticating your request

Every request you make to a Rail API endpoint requires an AUTH_TOKEN. We secure our endpoints using standards-based OAuth2 Client Credentials Grant and scopes.

Note: For production environments, if you're implementing request signing in addition to OAuth2, Rail must whitelist your public key before go-live. See Request Signing for more details.

To obtain the AUTH_TOKEN, you will need to authorize your account using your BASE_64_ENCODED_CLIENTID_AND_SECRET with the scopes you require:

Copy
Copied
curl --location --request POST 'https://auth.layer2financial.com/oauth2/ausbdqlx69rH6OjWd696/v1/token?grant_type=client_credentials&scope=subscriptions:read+subscriptions:write' \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Cache-Control: no-cache' \
--header 'Authorization: Basic {BASE_64_ENCODED_CLIENTID_AND_SECRET}'

This gives you a response object containing your AUTH_TOKEN:

Copy
Copied
{
    "token_type": "Bearer",
    "expires_in": 43200,
    "access_token": {AUTH_TOKEN},
    "scope": "subscriptions:read subscriptions:write"
}

The scopes we'll need for this tutorial are:

  • subscriptions:read to list and view subscriptions
  • subscriptions:write to create and delete subscriptions

The full list of scopes are available here.


Step 1: Register Webhook Subscriptions

Register for specific events by calling the /v1/subscriptions endpoint. You'll need to specify:

  • event_type - The type of event you want to receive (e.g., ACCOUNT_OPEN , TRANSACTION_POSTED )
  • callback_url - Your server's endpoint that will receive the webhook

Example: Register for Account Status Changes

Copy
Copied
curl --location --request POST 'https://sandbox.layer2financial.com/api/v1/subscriptions' \
--header 'Authorization: Bearer {AUTH_TOKEN}' \
--header 'Content-Type: application/json' \
--data-raw '{
    "event_type": "ACCOUNT_OPEN",
    "callback_url": "https://your-domain.com/webhooks/account-open"
}'

The response includes a signature_verification_key for validating webhook authenticity:

Copy
Copied
{
    "data": {
        "id": "sub_abc123456",
        "event_type": "ACCOUNT_OPEN",
        "callback_url": "https://your-domain.com/webhooks/account-open",
        "signature_verification_key": "whsec_K9p2X5mN8qR3vT6zB4",
        "status": "ACTIVE",
        "created_timestamp": "2023-11-20T14:32:18.123456Z"
    }
}

Important: Save the signature_verification_key - you'll need it to verify that webhooks are genuinely from Rail.

Production Note - Whitelisting Required: Before going live, Rail must whitelist your production webhook callback URL and public key to ensure webhook delivery and signature verification work as expected. Please reach out to your dedicated implementations engineer or account manager to:

  • Whitelist your webhook callback URL(s)
  • Whitelist your public key (if using request signing)

This whitelisting process ensures secure, reliable webhook delivery to your production systems.

Common Webhook Event Types

Here are the most commonly used webhook events organized by use case:

Customer Onboarding Events

Event Type When Triggered Use Case
APPLICATION_SUBMITTED Application submitted for review Track application progress
APPLICATION_CHANGES_REQUESTED Additional information needed Notify customer of required updates
APPLICATION_APPROVED Application approved, customer created Activate customer in your system
APPLICATION_REJECTED Application rejected Notify customer and log reason

Account Management Events

Event Type When Triggered Use Case
ACCOUNT_OPEN Account status changes to OPEN Enable account features in your UI
ACCOUNT_DEPOSIT_INSTRUCTIONS_UPDATE Deposit instructions available/updated Update stored deposit instructions
ACCOUNT_FROZEN Account is frozen Restrict account access
ACCOUNT_CLOSED Account is closed Archive account data

Transaction Events

Event Type When Triggered Use Case
TRANSACTION_PENDING Deposit/withdrawal detected Notify customer of incoming funds
TRANSACTION_POSTED Transaction approved and posted Update customer balance
TRANSACTION_CANCELLED Transaction cancelled Reverse any provisional credits

Money Movement Events

Event Type When Triggered Use Case
DEPOSIT_ACCEPTED Deposit request accepted Confirm deposit initiation
DEPOSIT_COMPLETED Deposit fully processed Update account balance
WITHDRAWAL_REQUESTED Withdrawal initiated Log withdrawal request
WITHDRAWAL_ACCEPTED Withdrawal approved Process withdrawal
WITHDRAWAL_COMPLETED Withdrawal processed Confirm funds sent
EXCHANGE_ACCEPTED Exchange order filled Update balances
EXCHANGE_COMPLETED Exchange settled Confirm exchange completion

For a complete list of available events, see Subscription Events.

Registering for Multiple Events

You need to create a separate subscription for each event type. Here's an example of registering for a complete transaction lifecycle:

Copy
Copied
# Register for pending transactions
curl --location --request POST 'https://sandbox.layer2financial.com/api/v1/subscriptions' \
--header 'Authorization: Bearer {AUTH_TOKEN}' \
--header 'Content-Type: application/json' \
--data-raw '{
    "event_type": "TRANSACTION_PENDING",
    "callback_url": "https://your-domain.com/webhooks/transactions"
}'

# Register for posted transactions
curl --location --request POST 'https://sandbox.layer2financial.com/api/v1/subscriptions' \
--header 'Authorization: Bearer {AUTH_TOKEN}' \
--header 'Content-Type: application/json' \
--data-raw '{
    "event_type": "TRANSACTION_POSTED",
    "callback_url": "https://your-domain.com/webhooks/transactions"
}'

# Register for cancelled transactions
curl --location --request POST 'https://sandbox.layer2financial.com/api/v1/subscriptions' \
--header 'Authorization: Bearer {AUTH_TOKEN}' \
--header 'Content-Type: application/json' \
--data-raw '{
    "event_type": "TRANSACTION_CANCELLED",
    "callback_url": "https://your-domain.com/webhooks/transactions"
}'

Tip: You can use the same callback URL for multiple event types and handle different events within a single endpoint.


Step 2: Implement Your Webhook Endpoint

Your webhook endpoint must be a publicly accessible HTTPS URL that can receive POST requests from Rail.

Endpoint Requirements

  1. HTTPS only - Rail only sends webhooks to HTTPS endpoints
  2. Public accessibility - The endpoint must be reachable from the internet
  3. Return 200-299 status code - Acknowledge receipt within 5 seconds
  4. Handle POST requests - Webhooks are sent as HTTP POST with JSON body
  5. Whitelisted callback URL - For production, your callback URL must be whitelisted by Rail

Example Webhook Endpoint (Node.js/Express)

Copy
Copied
const express = require('express');
const crypto = require('crypto');

const app = express();
app.use(express.json());

app.post('/webhooks/transactions', (req, res) => {
    // Step 1: Get the webhook payload and signature
    const payload = JSON.stringify(req.body);
    const signature = req.headers['x-rail-signature'];
    const timestamp = req.headers['x-rail-timestamp'];
    
    // Step 2: Verify the webhook signature (see Step 3)
    const isValid = verifyWebhookSignature(payload, signature, timestamp);
    
    if (!isValid) {
        console.error('Invalid webhook signature');
        return res.status(401).send('Invalid signature');
    }
    
    // Step 3: Process the webhook event
    const event = req.body;
    console.log('Received webhook:', event.event_type);
    
    try {
        handleWebhookEvent(event);
        
        // Step 4: Acknowledge receipt
        res.status(200).send('Webhook received');
    } catch (error) {
        console.error('Error processing webhook:', error);
        res.status(500).send('Error processing webhook');
    }
});

app.listen(3000, () => {
    console.log('Webhook server running on port 3000');
});

Webhook Payload Structure

All Rail webhooks share the same basic structure:

Copy
Copied
{
    "event_id": "evt_abc123456789",        // Unique event ID (idempotent)
    "event_date": "2023-11-20T14:32:18.123456Z",  // When event occurred
    "event_type": "TRANSACTION_POSTED",    // Type of event
    "event_data": {                        // Event-specific data
        "transaction_id": "txn_xyz789",
        "account_id": "CUSTOMER_USD_001",
        "amount": 1000.00,
        "asset_type_id": "FIAT_MAINNET_USD",
        "category": "DEPOSIT",
        "status": "POSTED"
    }
}

Note: Webhooks are purposefully lightweight and contain only critical information for decision-making. If you need extended information, call the appropriate GET endpoint using the IDs provided in the event.


Step 3: Verify Webhook Signatures

Always verify that webhooks are genuinely from Rail by validating the signature. Rail signs each webhook with your signature_verification_key.

How Webhook Signing Works

Rail includes these headers with each webhook:

  • x-rail-signature - HMAC signature of the payload
  • x-rail-timestamp - Unix timestamp when webhook was sent

Signature Verification Algorithm

  1. Concatenate the timestamp and payload: {timestamp}.{payload}
  2. Compute HMAC-SHA256 hash using your signature_verification_key
  3. Compare the computed signature with the x-rail-signature header

Example Verification (Node.js)

Copy
Copied
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, timestamp) {
    // Your signature verification key from subscription response
    const SIGNATURE_KEY = 'whsec_K9p2X5mN8qR3vT6zB4';
    
    // Prevent replay attacks - reject old webhooks (older than 5 minutes)
    const currentTime = Math.floor(Date.now() / 1000);
    const webhookAge = currentTime - parseInt(timestamp);
    
    if (webhookAge > 300) {  // 5 minutes
        console.error('Webhook is too old');
        return false;
    }
    
    // Create the signed payload
    const signedPayload = `${timestamp}.${payload}`;
    
    // Compute expected signature
    const expectedSignature = crypto
        .createHmac('sha256', SIGNATURE_KEY)
        .update(signedPayload)
        .digest('hex');
    
    // Compare signatures securely (timing-safe comparison)
    return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expectedSignature)
    );
}

Example Verification (Python)

Copy
Copied
import hmac
import hashlib
import time

def verify_webhook_signature(payload, signature, timestamp):
    # Your signature verification key from subscription response
    SIGNATURE_KEY = 'whsec_K9p2X5mN8qR3vT6zB4'
    
    # Prevent replay attacks
    current_time = int(time.time())
    webhook_age = current_time - int(timestamp)
    
    if webhook_age > 300:  # 5 minutes
        print('Webhook is too old')
        return False
    
    # Create the signed payload
    signed_payload = f"{timestamp}.{payload}"
    
    # Compute expected signature
    expected_signature = hmac.new(
        SIGNATURE_KEY.encode('utf-8'),
        signed_payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    # Compare signatures securely
    return hmac.compare_digest(signature, expected_signature)

Security Best Practice: Always verify webhook signatures to prevent malicious actors from sending fake webhooks to your endpoint.


Step 4: Handle Webhook Events

Once you've verified the webhook signature, process the event data based on the event_type.

Example Event Handler

Copy
Copied
function handleWebhookEvent(event) {
    switch (event.event_type) {
        case 'ACCOUNT_OPEN':
            handleAccountOpen(event.event_data);
            break;
            
        case 'TRANSACTION_PENDING':
            handleTransactionPending(event.event_data);
            break;
            
        case 'TRANSACTION_POSTED':
            handleTransactionPosted(event.event_data);
            break;
            
        case 'WITHDRAWAL_COMPLETED':
            handleWithdrawalCompleted(event.event_data);
            break;
            
        case 'APPLICATION_APPROVED':
            handleApplicationApproved(event.event_data);
            break;
            
        default:
            console.log('Unhandled event type:', event.event_type);
    }
}

function handleAccountOpen(data) {
    console.log('Account opened:', data.account_id);
    
    // Update your database
    // Enable account features in your UI
    // Send notification to customer
}

function handleTransactionPosted(data) {
    console.log('Transaction posted:', data.transaction_id);
    
    // Update account balance
    // Send confirmation to customer
    // Trigger any dependent workflows
}

Handling Duplicate Events (Idempotency)

Webhooks may occasionally be delivered more than once. Use the event_id to ensure idempotent processing:

Copy
Copied
const processedEvents = new Set();

function handleWebhookEvent(event) {
    // Check if we've already processed this event
    if (processedEvents.has(event.event_id)) {
        console.log('Duplicate event, skipping:', event.event_id);
        return;
    }
    
    // Process the event
    processEvent(event);
    
    // Mark as processed
    processedEvents.add(event.event_id);
    
    // In production, store processed event IDs in a database
}

Important: The event_id is idempotent and remains the same even if an event is replayed. Always check for duplicate events before processing.

Enriching Webhook Data

Webhooks contain minimal data. To get more details, call the appropriate API endpoint:

Copy
Copied
async function handleTransactionPosted(data) {
    // Webhook provides basic info
    const { transaction_id, account_id } = data;
    
    // Fetch full transaction details if needed
    const transaction = await fetch(
        `https://sandbox.layer2financial.com/api/v1/accounts/deposits/${account_id}/transactions/${transaction_id}`,
        {
            headers: { 'Authorization': `Bearer ${AUTH_TOKEN}` }
        }
    );
    
    const fullData = await transaction.json();
    
    // Now you have complete transaction details
    processFullTransaction(fullData);
}

Step 5: Manage Subscriptions

List All Subscriptions

View all your registered webhook subscriptions:

Copy
Copied
curl --location --request GET 'https://sandbox.layer2financial.com/api/v1/subscriptions' \
--header 'Authorization: Bearer {AUTH_TOKEN}' \
--header 'Content-Type: application/json'

Response:

Copy
Copied
{
    "data": {
        "subscriptions": [
            {
                "id": "sub_abc123456",
                "event_type": "ACCOUNT_OPEN",
                "callback_url": "https://your-domain.com/webhooks/account-open",
                "status": "ACTIVE",
                "created_timestamp": "2023-11-20T14:32:18.123456Z"
            },
            {
                "id": "sub_def789012",
                "event_type": "TRANSACTION_POSTED",
                "callback_url": "https://your-domain.com/webhooks/transactions",
                "status": "ACTIVE",
                "created_timestamp": "2023-11-20T14:35:22.456789Z"
            }
        ]
    }
}

Get a Specific Subscription

Retrieve details for a single subscription:

Copy
Copied
curl --location --request GET 'https://sandbox.layer2financial.com/api/v1/subscriptions/sub_abc123456' \
--header 'Authorization: Bearer {AUTH_TOKEN}' \
--header 'Content-Type: application/json'

Delete a Subscription

Remove a webhook subscription when you no longer need it:

Copy
Copied
curl --location --request DELETE 'https://sandbox.layer2financial.com/api/v1/subscriptions/sub_abc123456' \
--header 'Authorization: Bearer {AUTH_TOKEN}' \
--header 'Content-Type: application/json'

Response:

Copy
Copied
{
    "data": {
        "id": "sub_abc123456",
        "status": "DELETED"
    }
}

View Webhook Delivery History

You can view historical webhook events and their delivery status via the Management UI. This helps with debugging webhook delivery issues.


Webhook Best Practices

1. Always Verify Signatures

Never trust webhook data without verifying the signature. This prevents malicious actors from sending fake events to your endpoint.

Copy
Copied
if (!verifyWebhookSignature(payload, signature, timestamp)) {
    return res.status(401).send('Invalid signature');
}

2. Respond Quickly (Within 5 Seconds)

Acknowledge webhook receipt immediately and process events asynchronously:

Copy
Copied
app.post('/webhooks/transactions', async (req, res) => {
    // Verify signature
    if (!verifySignature(req)) {
        return res.status(401).send('Invalid');
    }
    
    // Respond immediately
    res.status(200).send('Received');
    
    // Process asynchronously
    processWebhookAsync(req.body).catch(console.error);
});

3. Handle Duplicate Events Idempotently

Use the event_id to detect and skip duplicate events:

Copy
Copied
if (await isEventProcessed(event.event_id)) {
    return; // Skip duplicate
}

await processEvent(event);
await markEventProcessed(event.event_id);

4. Implement Retry Logic for Failed Processing

If your processing fails, the event may be lost. Implement retry mechanisms:

Copy
Copied
async function processWebhookWithRetry(event, maxRetries = 3) {
    for (let attempt = 0; attempt < maxRetries; attempt++) {
        try {
            await processEvent(event);
            return; // Success
        } catch (error) {
            console.error(`Attempt ${attempt + 1} failed:`, error);
            if (attempt === maxRetries - 1) throw error;
            await sleep(1000 * Math.pow(2, attempt)); // Exponential backoff
        }
    }
}

5. Monitor Webhook Delivery Failures

Track webhook delivery failures in your logs and set up alerts:

Copy
Copied
app.post('/webhooks/transactions', async (req, res) => {
    try {
        await processWebhook(req.body);
        res.status(200).send('OK');
        
        // Log success
        logger.info('Webhook processed', { event_id: req.body.event_id });
    } catch (error) {
        res.status(500).send('Error');
        
        // Alert on failure
        logger.error('Webhook processing failed', {
            event_id: req.body.event_id,
            error: error.message
        });
        
        // Send alert to monitoring system
        alerting.notify('Webhook processing failed', error);
    }
});

6. Use HTTPS and Keep Endpoints Secure

  • Always use HTTPS endpoints (Rail won't send to HTTP)
  • Implement rate limiting to prevent abuse
  • Keep your signature verification keys secure
  • Rotate keys periodically

7. Test Webhook Endpoints Before Going Live

Test your webhook endpoint thoroughly:

  • Verify signature validation works
  • Test handling of different event types
  • Simulate duplicate events
  • Test timeout scenarios
  • Verify error handling

8. Store Webhook Events for Audit Trail

Keep a record of all received webhooks for debugging and compliance:

Copy
Copied
async function storeWebhookEvent(event) {
    await database.webhookEvents.insert({
        event_id: event.event_id,
        event_type: event.event_type,
        event_date: event.event_date,
        event_data: event.event_data,
        received_at: new Date(),
        processed: true
    });
}

Testing Webhooks

Testing in Sandbox

In sandbox, you can trigger webhooks by performing actions that generate events:

  1. Test Account Events - Create an account and wait for ACCOUNT_OPEN webhook
  2. Test Transaction Events - Simulate a deposit to trigger TRANSACTION_PENDING and TRANSACTION_POSTED
  3. Test Application Events - Submit an application to receive application status webhooks

Using Webhook Testing Tools

For local development, use tools like:

  • ngrok - Expose your local server to the internet
  • webhook.site - Test webhook delivery without writing code
  • RequestBin - Inspect webhook payloads

Example with ngrok

Copy
Copied
# Start your local webhook server
node webhook-server.js

# In another terminal, expose it with ngrok
ngrok http 3000

# Use the ngrok URL when registering subscriptions
# Example: https://abc123.ngrok.io/webhooks/transactions

Common Webhook Scenarios

Scenario 1: Customer Onboarding Flow

Register for all application-related events to track customer onboarding:

Copy
Copied
# Register for application events
for event in APPLICATION_SUBMITTED APPLICATION_CHANGES_REQUESTED APPLICATION_APPROVED APPLICATION_REJECTED; do
    curl --location --request POST 'https://sandbox.layer2financial.com/api/v1/subscriptions' \
    --header 'Authorization: Bearer {AUTH_TOKEN}' \
    --header 'Content-Type: application/json' \
    --data-raw "{
        \"event_type\": \"$event\",
        \"callback_url\": \"https://your-domain.com/webhooks/applications\"
    }"
done

Scenario 2: Real-Time Balance Updates

Track all transactions to maintain up-to-date balances:

Copy
Copied
# Register for transaction events
curl --location --request POST 'https://sandbox.layer2financial.com/api/v1/subscriptions' \
--header 'Authorization: Bearer {AUTH_TOKEN}' \
--header 'Content-Type: application/json' \
--data-raw '{
    "event_type": "TRANSACTION_POSTED",
    "callback_url": "https://your-domain.com/webhooks/balance-updates"
}'

Scenario 3: Money Movement Notifications

Track the complete lifecycle of withdrawals:

Copy
Copied
# Register for withdrawal events
for event in WITHDRAWAL_REQUESTED WITHDRAWAL_ACCEPTED WITHDRAWAL_COMPLETED WITHDRAWAL_CANCELLED; do
    curl --location --request POST 'https://sandbox.layer2financial.com/api/v1/subscriptions' \
    --header 'Authorization: Bearer {AUTH_TOKEN}' \
    --header 'Content-Type: application/json' \
    --data-raw "{
        \"event_type\": \"$event\",
        \"callback_url\": \"https://your-domain.com/webhooks/withdrawals\"
    }"
done

Summary

Let's review the webhook registration and management process:

Core Workflow

  1. Authenticate - Get your AUTH_TOKEN using OAuth2
  2. Register Subscriptions - POST to /v1/subscriptions for each event type you want to receive
  3. Implement Endpoint - Create an HTTPS endpoint to receive webhook POST requests
  4. Verify Signatures - Always validate the x-rail-signature header using your verification key
  5. Handle Events - Process event data based on event_type and use event_id for idempotency
  6. Manage Subscriptions - List, view, and delete subscriptions as needed

Key Takeaways

  • ✅ Always use HTTPS endpoints for webhooks
  • ✅ Verify webhook signatures to ensure authenticity
  • ✅ Respond within 5 seconds with a 200-299 status code
  • ✅ Handle duplicate events using the idempotent event_id
  • ✅ Process events asynchronously after acknowledging receipt
  • ✅ Implement retry logic for failed processing
  • ✅ Store webhook events for audit trail and debugging
  • Production URLs and public keys must be whitelisted by Rail before go-live

Common Webhook Events Quick Reference

Category Key Events
Accounts ACCOUNT_OPEN, ACCOUNT_DEPOSIT_INSTRUCTIONS_UPDATE, ACCOUNT_FROZEN
Transactions TRANSACTION_PENDING, TRANSACTION_POSTED, TRANSACTION_CANCELLED
Applications APPLICATION_SUBMITTED, APPLICATION_APPROVED, APPLICATION_CHANGES_REQUESTED
Withdrawals WITHDRAWAL_REQUESTED, WITHDRAWAL_COMPLETED, WITHDRAWAL_CANCELLED
Deposits DEPOSIT_ACCEPTED, DEPOSIT_COMPLETED
Exchanges EXCHANGE_ACCEPTED, EXCHANGE_COMPLETED

API Endpoints Used

Endpoint Purpose
POST /v1/subscriptions Register for webhook events
GET /v1/subscriptions List all subscriptions
GET /v1/subscriptions/{id} Get specific subscription details
DELETE /v1/subscriptions/{id} Remove a subscription

For a complete list of available webhook events, see Subscription Events.

To dive deeper into what you can do on the Rail platform, head to our API documentation.

Contact Us - We'd love to hear your thoughts, and you can contact the team via slack, website or email support@layer2financial.com.

© 2024 Rail. All Rights Reserved.