# Register for Webhooks via API *If you haven't already, [Sign Up for Sandbox Access](https://rail.io/contact-us) 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](#authenticating-your-request) 2. [Registered webhook subscriptions](#step-1-register-webhook-subscriptions) 3. [Implemented a webhook endpoint](#step-2-implement-your-webhook-endpoint) 4. [Verified webhook signatures](#step-3-verify-webhook-signatures) 5. [Handled webhook events](#step-4-handle-webhook-events) 6. [Managed your subscriptions](#step-5-manage-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](/guides/requestsigning) 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: ```bash 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`: ```javascript { "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](/guides/authentication). ## 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 ```bash 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: ```javascript { "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](/guides/subscriptions_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: ```bash # 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) ```javascript 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: ```javascript { "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) ```javascript 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) ```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 ```javascript 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: ```javascript 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: ```javascript 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: ```bash curl --location --request GET 'https://sandbox.layer2financial.com/api/v1/subscriptions' \ --header 'Authorization: Bearer {AUTH_TOKEN}' \ --header 'Content-Type: application/json' ``` Response: ```javascript { "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: ```bash 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: ```bash curl --location --request DELETE 'https://sandbox.layer2financial.com/api/v1/subscriptions/sub_abc123456' \ --header 'Authorization: Bearer {AUTH_TOKEN}' \ --header 'Content-Type: application/json' ``` Response: ```javascript { "data": { "id": "sub_abc123456", "status": "DELETED" } } ``` ### View Webhook Delivery History You can view historical webhook events and their delivery status via the [Management UI](https://management-sandbox.layer2financial.com/). 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. ```javascript 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: ```javascript 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: ```javascript 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: ```javascript 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: ```javascript 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: ```javascript 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 ```bash # 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: ```bash # 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: ```bash # 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: ```bash # 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](/guides/subscriptions_events). To dive deeper into what you can do on the Rail platform, head to our [API documentation](/api-docs/openapi/rail-spec).