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:
-
POST
/v1/subscriptions- Register for specific event types - Implement webhook endpoint - Create an endpoint on your server to receive webhooks
- Verify webhook signatures - Validate that webhooks are from Rail
- Handle webhook events - Process the event data and take appropriate actions
-
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:readandsubscriptions:writescopes
By the end of this guide you will have:
- Authenticated your request
- Registered webhook subscriptions
- Implemented a webhook endpoint
- Verified webhook signatures
- Handled webhook events
- 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:readto list and view subscriptions -
subscriptions:writeto 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 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:
# 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
- HTTPS only - Rail only sends webhooks to HTTPS endpoints
- Public accessibility - The endpoint must be reachable from the internet
- Return 200-299 status code - Acknowledge receipt within 5 seconds
- Handle POST requests - Webhooks are sent as HTTP POST with JSON body
- 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
GETendpoint 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
-
Concatenate the timestamp and payload:
{timestamp}.{payload} -
Compute HMAC-SHA256 hash using your
signature_verification_key -
Compare the computed signature with the
x-rail-signatureheader
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_idis 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:
-
Test Account Events
- Create an account and wait for
ACCOUNT_OPENwebhook -
Test Transaction Events
- Simulate a deposit to trigger
TRANSACTION_PENDINGandTRANSACTION_POSTED - 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/transactionsCommon 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\"
}"
doneScenario 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\"
}"
doneSummary
Let's review the webhook registration and management process:
Core Workflow
-
Authenticate
- Get your
AUTH_TOKENusing OAuth2 -
Register Subscriptions
- POST to
/v1/subscriptionsfor each event type you want to receive - Implement Endpoint - Create an HTTPS endpoint to receive webhook POST requests
-
Verify Signatures
- Always validate the
x-rail-signatureheader using your verification key -
Handle Events
- Process event data based on
event_typeand useevent_idfor idempotency - 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.