Skip to content

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:

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:

{
    "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

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:

{
    "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 TypeWhen TriggeredUse Case
APPLICATION_SUBMITTEDApplication submitted for reviewTrack application progress
APPLICATION_CHANGES_REQUESTEDAdditional information neededNotify customer of required updates
APPLICATION_APPROVEDApplication approved, customer createdActivate customer in your system
APPLICATION_REJECTEDApplication rejectedNotify customer and log reason

Account Management Events

Event TypeWhen TriggeredUse Case
ACCOUNT_OPENAccount status changes to OPENEnable account features in your UI
ACCOUNT_DEPOSIT_INSTRUCTIONS_UPDATEDeposit instructions available/updatedUpdate stored deposit instructions
ACCOUNT_FROZENAccount is frozenRestrict account access
ACCOUNT_CLOSEDAccount is closedArchive account data

Transaction Events

Event TypeWhen TriggeredUse Case
TRANSACTION_PENDINGDeposit/withdrawal detectedNotify customer of incoming funds
TRANSACTION_POSTEDTransaction approved and postedUpdate customer balance
TRANSACTION_CANCELLEDTransaction cancelledReverse any provisional credits

Money Movement Events

Event TypeWhen TriggeredUse Case
DEPOSIT_ACCEPTEDDeposit request acceptedConfirm deposit initiation
DEPOSIT_COMPLETEDDeposit fully processedUpdate account balance
WITHDRAWAL_REQUESTEDWithdrawal initiatedLog withdrawal request
WITHDRAWAL_ACCEPTEDWithdrawal approvedProcess withdrawal
WITHDRAWAL_COMPLETEDWithdrawal processedConfirm funds sent
EXCHANGE_ACCEPTEDExchange order filledUpdate balances
EXCHANGE_COMPLETEDExchange settledConfirm 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:

# 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)

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:

{
    "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)

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)

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

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:

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:

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:

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

Response:

{
    "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:

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:

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

Response:

{
    "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.

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:

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:

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:

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:

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:

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

# 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:

# 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:

# 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:

# 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

CategoryKey Events
AccountsACCOUNT_OPEN, ACCOUNT_DEPOSIT_INSTRUCTIONS_UPDATE, ACCOUNT_FROZEN
TransactionsTRANSACTION_PENDING, TRANSACTION_POSTED, TRANSACTION_CANCELLED
ApplicationsAPPLICATION_SUBMITTED, APPLICATION_APPROVED, APPLICATION_CHANGES_REQUESTED
WithdrawalsWITHDRAWAL_REQUESTED, WITHDRAWAL_COMPLETED, WITHDRAWAL_CANCELLED
DepositsDEPOSIT_ACCEPTED, DEPOSIT_COMPLETED
ExchangesEXCHANGE_ACCEPTED, EXCHANGE_COMPLETED

API Endpoints Used

EndpointPurpose
POST /v1/subscriptionsRegister for webhook events
GET /v1/subscriptionsList 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.