If you haven't already, Sign Up for Sandbox Access to get your client ID and secret to work through this Build Guide!
Account & Funds Management encompasses the complete lifecycle of opening accounts for your customers, obtaining deposit instructions, monitoring account status changes, and tracking incoming funds. This guide will walk you through best practices for managing accounts and funds programmatically via the Rail API.
Rail supports multiple account types and currencies, allowing you to:
- Open deposit accounts in various fiat and crypto currencies
- Monitor account status changes through webhooks
- Generate deposit instructions for customers to send funds
- Track incoming deposits and transactions in real-time
The standard API account management flow follows these key steps:
- POST
/v1/accounts/deposits- Open a new account - GET
/v1/accounts/deposits/{id}- Check account status - POST
/v1/subscriptions- Subscribe to account webhooks - POST
/v1/deposits- Get deposit instructions - GET
/v1/accounts/deposits/{id}/transactions- Track incoming deposits
This guide should take no longer than 15 minutes and will allow you to open an account, set up monitoring, and track incoming funds in Sandbox.
By the end of this guide you will have:
- Authenticated your request
- Opened a new account for a customer
- Verified the account is ready
- Set up webhook subscriptions for monitoring
- Retrieved deposit instructions
- Tracked incoming deposits
Let's go!
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.
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=accounts:read+accounts:write+deposits:read+deposits:write+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": "accounts:read accounts:write deposits:read deposits:write subscriptions:read subscriptions:write"
}The scopes we'll need for this tutorial are:
accounts:readandaccounts:writeto open and query accountsdeposits:readanddeposits:writeto get deposit instructions and track depositssubscriptions:readandsubscriptions:writeto set up webhooks for monitoring
The full list of scopes are available here.
Open a new account for a customer by calling the /v1/accounts/deposits endpoint.
You'll need:
- The
customer_idfrom your onboarded customer (see Customer Onboarding) - An
account_idthat will be the unique identifier for this account (e.g.,CUSTOMERNAME_USD_001) - The
product_id: The type of account to open. UseDEPOSIT_BASICfor standard deposit accounts - The
asset_type_id: The currency/asset type for the account (e.g.,FIAT_TESTNET_USDfor USD in sandbox)
curl --location --request POST 'https://sandbox.layer2financial.com/api/v1/accounts/deposits' \
--header 'Authorization: Bearer {AUTH_TOKEN}' \
--header 'Content-Type: application/json' \
--data-raw '{
"customer_id": "INDIVIDUAL_CUSTOMER_001",
"account_to_open": {
"account_id": "CUSTOMER_USD_001",
"product_id": "DEPOSIT_BASIC",
"asset_type_id": "FIAT_TESTNET_USD"
}
}'The response contains the account_id you provided:
{
"data": {
"id": "CUSTOMER_USD_001"
}
}Name your accounts appropriately for your business needs (e.g., CustomerName_USD_001, CustomerName_EUR_002) to make them easy to identify and manage.
Before you can request deposit instructions, the account must reach OPEN status. Check the account status by calling the /v1/accounts/deposits/{id} endpoint:
curl --location --request GET 'https://sandbox.layer2financial.com/api/v1/accounts/deposits/CUSTOMER_USD_001' \
--header 'Authorization: Bearer {AUTH_TOKEN}' \
--header 'Content-Type: application/json'The response shows the account status and balance information:
{
"data": {
"id": "CUSTOMER_USD_001",
"status": "OPEN",
"asset_type_id": "FIAT_TESTNET_USD",
"product_type": "DEPOSIT",
"product_id": "DEPOSIT_BASIC",
"customer_id": "INDIVIDUAL_CUSTOMER_001",
"current_balance": 0,
"available_balance": 0
}
}Accounts progress through different statuses during their lifecycle:
- PENDING - Account is being set up and not yet ready for transactions
- OPEN - Account is fully ready and can receive deposit instructions
- FROZEN - Account has been temporarily locked for all actions (deposits, transfers, exchanges, withdrawals)
- EDD - Account requires enhanced due diligence. The account will operate as if frozen
- CLOSED - Account is permanently closed, no further actions can be performed
- DORMANT - No activity seen in the last 12 months. Account is still available for use
Deposit instructions will only be available when the account status is OPEN - not when it's in PENDING or any other status. Always verify the account status before attempting to get deposit instructions.
Scenario: You want to display an account card or enable account features in your UI only when the account is truly ready to receive funds.
The Most Reliable Approach: Don't rely solely on the account status being OPEN. Instead, actually request deposit instructions and confirm they are available. Here's why:
There may be scenarios where an account shows OPEN status, but the underlying banking provider has not yet completed their approval process. In these edge cases:
- The account status will be
OPENin Rail's system - However, deposit instructions may not yet be available
- Attempting to get deposit instructions will return an error or empty response
Recommended Implementation:
# Step 1: Check account status
GET /v1/accounts/deposits/{account_id}
# Step 2: If status is OPEN, attempt to get deposit instructions
POST /v1/deposits
# Step 3: Only show account as "ready" if deposit instructions are successfully returnedThis two-step verification ensures that:
- The account is marked as
OPENby Rail - The underlying provider has approved and deposit instructions are actually available
- Your customer can immediately use the account without errors
Example Flow:
// Pseudo-code for checking account readiness
async function isAccountReady(accountId) {
// Check account status
const account = await getAccount(accountId);
if (account.status !== 'OPEN') {
return false;
}
// Verify deposit instructions are available
try {
const deposit = await getDepositInstructions(accountId);
return deposit.deposit_instructions && deposit.deposit_instructions.length > 0;
} catch (error) {
// Instructions not yet available
return false;
}
}
// Only show account card when truly ready
if (await isAccountReady(accountId)) {
displayAccountCard();
}This approach provides the best user experience and prevents showing accounts as "ready" when they cannot actually receive funds yet.
You can also retrieve all accounts for a customer using the /v1/accounts/deposits endpoint with query parameters:
curl --location --request GET 'https://sandbox.layer2financial.com/api/v1/accounts/deposits?customer_id=INDIVIDUAL_CUSTOMER_001&status=OPEN' \
--header 'Authorization: Bearer {AUTH_TOKEN}' \
--header 'Content-Type: application/json'This returns a paginated list of accounts:
{
"data": {
"accounts": [
{
"id": "CUSTOMER_USD_001",
"status": "OPEN",
"asset_type_id": "FIAT_TESTNET_USD",
"product_type": "DEPOSIT",
"product_id": "DEPOSIT_BASIC",
"customer_id": "INDIVIDUAL_CUSTOMER_001",
"current_balance": 0,
"available_balance": 0
}
]
},
"links": {
"self": "/v1/accounts/deposits?customer_id=INDIVIDUAL_CUSTOMER_001&status=OPEN&page=0&page_size=20",
"first": "/v1/accounts/deposits?customer_id=INDIVIDUAL_CUSTOMER_001&status=OPEN&page=0&page_size=20",
"prev": null,
"next": null,
"last": "/v1/accounts/deposits?customer_id=INDIVIDUAL_CUSTOMER_001&status=OPEN&page=0&page_size=20"
}
}Instead of constantly polling for account status changes, set up webhook subscriptions to receive real-time notifications. This is the recommended approach for production systems.
Create a subscription for account status changes using the /v1/subscriptions endpoint:
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 payloads:
{
"data": {
"id": "sub_123456789",
"event_type": "ACCOUNT_OPEN",
"callback_url": "https://your-domain.com/webhooks/account-open",
"signature_verification_key": "whsec_abc123..."
}
}Subscribe to the ACCOUNT_DEPOSIT_INSTRUCTIONS_UPDATE webhook to know when deposit instructions become available or are updated:
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_DEPOSIT_INSTRUCTIONS_UPDATE",
"callback_url": "https://your-domain.com/webhooks/deposit-instructions"
}'Deposit instructions on an account may be updated over time. Account numbers and banking details can change, so listening to this webhook ensures you always have current information.
Set up subscriptions for tracking incoming deposits:
# Subscribe to 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/transaction-pending"
}'
# Subscribe to 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/transaction-posted"
}'| Event Type | When Triggered | Use Case |
|---|---|---|
ACCOUNT_OPEN | Account status changes to OPEN | Know when account is ready for deposit instructions |
ACCOUNT_DEPOSIT_INSTRUCTIONS_UPDATE | Deposit instructions become available or are updated | Get notified of deposit instruction changes |
ACCOUNT_FROZEN | Account is frozen | Alert customer of account restrictions |
ACCOUNT_EDD | Account requires enhanced due diligence | Take appropriate compliance actions |
TRANSACTION_PENDING | Deposit detected but not yet posted | Notify customer of incoming funds |
TRANSACTION_POSTED | Deposit approved and posted to account | Update customer balance and confirm receipt |
For more details on webhooks, see the Subscriptions Guide.
Once the account status is OPEN, request deposit instructions using the /v1/deposits endpoint.
Different currencies support different payment rails. For USD accounts, you can receive instructions for:
- ACH - Automated Clearing House (domestic US transfers)
- FEDWIRE - Federal Reserve Wire Network (domestic US transfers)
- SWIFT - Society for Worldwide Interbank Financial Telecommunication (international transfers)
For the complete list of supported payment rails for each currency, see the Assets Guide.
Create a PUSH deposit to get the instructions customers need to send funds:
curl --location --request POST 'https://sandbox.layer2financial.com/api/v1/deposits' \
--header 'Authorization: Bearer {AUTH_TOKEN}' \
--header 'Content-Type: application/json' \
--data-raw '{
"deposit_type": "PUSH",
"deposit_destination": {
"destination_account_id": "CUSTOMER_USD_001"
},
"customer_name": "Jane Doe"
}'For FIAT_TESTNET_USD accounts, you'll receive ACH, FEDWIRE, and SWIFT instruction types (production accounts for FIAT_MAINNET_USD support all three rails):
{
"data": {
"id": "deposit_987654321",
"status": "REQUESTED",
"created_timestamp": "2023-11-20T14:32:18.123456Z",
"deposit_type": "PUSH",
"deposit_destination": {
"destination_account_id": "CUSTOMER_USD_001",
"asset_type_id": "FIAT_TESTNET_USD"
},
"customer_name": "Jane Doe",
"deposit_source": {
"deposit_instructions": [
{
"instruction_type": "ACH",
"account_holder_name": "Jane Doe",
"account_number": "9695859732",
"account_routing_number": "715960587",
"description": "Deposit to CUSTOMER_USD_001",
"asset_type_id": "FIAT_TESTNET_USD"
},
{
"instruction_type": "FEDWIRE",
"account_holder_name": "Jane Doe",
"account_number": "9695859732",
"account_routing_number": "715960587",
"account_holder_address": {
"address_line1": "123 Main Street",
"city": "Boston",
"state": "MA",
"postal_code": "02101",
"country_code": "US"
},
"institution_name": "Rail Banking Services",
"institution_address": {
"address_line1": "1 Financial Place",
"city": "Boston",
"state": "MA",
"postal_code": "02101",
"country_code": "US"
},
"memo": "ea947e78-50ba-40b0-8e6e-32345eeb2cab",
"asset_type_id": "FIAT_TESTNET_USD"
},
{
"instruction_type": "SWIFT",
"account_holder_name": "Jane Doe",
"account_number": "9695859732",
"swift_code": "RAILUSXX",
"account_holder_address": {
"address_line1": "123 Main Street",
"city": "Boston",
"state": "MA",
"postal_code": "02101",
"country_code": "US"
},
"institution_name": "Rail Banking Services",
"institution_address": {
"address_line1": "1 Financial Place",
"city": "Boston",
"state": "MA",
"postal_code": "02101",
"country_code": "US"
},
"reference": "CUSTOMER_USD_001",
"asset_type_id": "FIAT_TESTNET_USD"
}
]
}
}
}For crypto accounts, you'll receive blockchain addresses:
curl --location --request POST 'https://sandbox.layer2financial.com/api/v1/deposits' \
--header 'Authorization: Bearer {AUTH_TOKEN}' \
--header 'Content-Type: application/json' \
--data-raw '{
"deposit_type": "PUSH",
"deposit_destination": {
"destination_account_id": "CUSTOMER_USDC_001"
},
"customer_name": "Jane Doe"
}'Response for crypto account:
{
"data": {
"id": "deposit_crypto_123",
"status": "REQUESTED",
"created_timestamp": "2023-11-20T14:35:22.456789Z",
"deposit_type": "PUSH",
"deposit_destination": {
"destination_account_id": "CUSTOMER_USDC_001",
"asset_type_id": "ETHEREUM_GOERLI_USDC"
},
"customer_name": "Jane Doe",
"deposit_source": {
"deposit_instructions": [
{
"instruction_type": "CRYPTO",
"address": "0x9ED9961A87ba49511Cf522059df52e2a94eF188b",
"blockchain": "ethereum",
"network": "goerli",
"asset_type_id": "ETHEREUM_GOERLI_USDC"
}
]
}
}
}The /v1/deposits endpoint is the primary and most reliable method for getting deposit instructions. When the account status is OPEN, this endpoint will consistently return the appropriate instruction types for the account's asset type. For USD accounts, this includes ACH, FEDWIRE, and SWIFT instructions. See the Assets Guide for supported payment rails by currency.
Once deposit instructions are shared with customers and they send funds, you need to track when those funds arrive.
- Deposit - The request for deposit instructions (the
idreturned from/v1/deposits) - Transaction - The actual movement of funds into the account (linked via
category_id)
When a customer sends funds using the deposit instructions, Rail creates a transaction linked to the deposit.
A transaction appearing in the Rail system means the deposit has reached our platform. Rail does not have the ability to track the status of deposits before they hit our platform (e.g., we cannot see that a wire transfer has been initiated but not yet received). Transaction notifications only occur once funds have actually arrived at Rail's banking partners.
When a deposit is received, you'll receive webhooks in this sequence:
TRANSACTION_PENDING- Sent when Rail detects the incoming depositTRANSACTION_POSTED- Sent once monitoring is completed and the transaction is approved
Both webhooks include the transaction details with the category_id matching your deposit id.
Example TRANSACTION_PENDING webhook payload:
{
"event_type": "TRANSACTION_PENDING",
"data": {
"transaction_id": "txn_abc123",
"account_id": "CUSTOMER_USD_001",
"amount": 1000.00,
"asset_type_id": "FIAT_TESTNET_USD",
"category": "DEPOSIT",
"category_id": "deposit_987654321",
"status": "PENDING",
"created_timestamp": "2023-11-20T15:00:00.000000Z"
}
}Alternatively, you can poll for transactions using the account's transaction endpoint:
curl --location --request GET 'https://sandbox.layer2financial.com/api/v1/accounts/deposits/CUSTOMER_USD_001/transactions' \
--header 'Authorization: Bearer {AUTH_TOKEN}' \
--header 'Content-Type: application/json'Response with transaction details:
{
"data": {
"transactions": [
{
"id": "txn_abc123",
"account_id": "CUSTOMER_USD_001",
"status": "POSTED",
"amount": 1000.00,
"asset_type_id": "FIAT_TESTNET_USD",
"category": "DEPOSIT",
"category_id": "deposit_987654321",
"created_timestamp": "2023-11-20T15:00:00.000000Z",
"posted_timestamp": "2023-11-20T15:05:00.000000Z"
}
]
},
"links": {
"self": "/v1/accounts/deposits/CUSTOMER_USD_001/transactions?page=0&page_size=20",
"first": "/v1/accounts/deposits/CUSTOMER_USD_001/transactions?page=0&page_size=20",
"prev": null,
"next": null,
"last": "/v1/accounts/deposits/CUSTOMER_USD_001/transactions?page=0&page_size=20"
}
}To find transactions for a specific deposit, filter by category and category_id:
curl --location --request GET 'https://sandbox.layer2financial.com/api/v1/accounts/deposits/CUSTOMER_USD_001/transactions?category=DEPOSIT&category_id=deposit_987654321' \
--header 'Authorization: Bearer {AUTH_TOKEN}' \
--header 'Content-Type: application/json'After a transaction is posted, verify the account balance was updated:
curl --location --request GET 'https://sandbox.layer2financial.com/api/v1/accounts/deposits/CUSTOMER_USD_001' \
--header 'Authorization: Bearer {AUTH_TOKEN}' \
--header 'Content-Type: application/json'The response shows the updated balances:
{
"data": {
"id": "CUSTOMER_USD_001",
"status": "OPEN",
"asset_type_id": "FIAT_TESTNET_USD",
"product_type": "DEPOSIT",
"product_id": "DEPOSIT_BASIC",
"customer_id": "INDIVIDUAL_CUSTOMER_001",
"current_balance": 1000.00,
"available_balance": 1000.00
}
}Deposit instructions will only be available when the account status is OPEN. Attempting to get instructions for accounts in PENDING or other statuses will fail.
Recommended Flow:
# 1. Check account status
GET /v1/accounts/deposits/{account_id}
# 2. Only proceed if status is "OPEN"
if status == "OPEN":
POST /v1/depositsSet up webhook subscriptions for real-time notifications rather than continuously polling endpoints:
ACCOUNT_OPEN- Know immediately when account is readyACCOUNT_DEPOSIT_INSTRUCTIONS_UPDATE- Get notified of instruction changesTRANSACTION_PENDINGandTRANSACTION_POSTED- Track incoming funds in real-time
Benefits:
- Reduces API load and costs
- Faster response times
- More reliable than polling intervals
- Scales better for high-volume operations
If webhooks don't work for your setup (e.g., firewall restrictions, infrastructure limitations, or compliance requirements), polling can be a reliable replacement. Implement polling with appropriate intervals (e.g., every 30-60 seconds for account status, every 2-5 minutes for transactions) and proper exponential backoff for rate limiting.
Account numbers and banking details can change over time. Always:
- Subscribe to
ACCOUNT_DEPOSIT_INSTRUCTIONS_UPDATEwebhooks - Periodically refresh deposit instructions for long-lived accounts
- Update your UI/systems when instructions change
Reserve accounts are internal system accounts and won't work for customer deposits. Always:
- Create new accounts specifically for customer transactions
- Use meaningful naming conventions (e.g.,
CustomerName_Currency_SequenceNumber) - Verify the
product_typeisDEPOSIT, notCLIENT
Transactions progress through states:
PENDING- Detected but not yet approved (don't credit customer yet)POSTED- Approved and settled (safe to credit customer)CANCELLED- Transaction was cancelled (reverse any credits)
Always wait for TRANSACTION_POSTED before considering funds available to the customer.
When querying transactions, use the deposit id as the category_id filter:
GET /v1/accounts/deposits/{account_id}/transactions?category=DEPOSIT&category_id={deposit_id}This ensures you're tracking the correct transactions for specific deposit requests.
Common scenarios to handle:
- Account not yet
OPENwhen requesting deposit instructions - Network timeouts when polling for status
- Webhook delivery failures (implement retry logic)
- Duplicate transaction notifications (use idempotency)
Let's review the account and funds management process:
- Authenticate - Get your
AUTH_TOKENusing OAuth2 - Open Account - POST to
/v1/accounts/depositswith customer details and asset type - Verify Status - GET
/v1/accounts/deposits/{id}until status isOPEN - Set Up Webhooks - POST to
/v1/subscriptionsfor account and transaction events - Get Deposit Instructions - POST to
/v1/depositswithdeposit_type: "PUSH" - Track Deposits - Listen for
TRANSACTION_PENDINGandTRANSACTION_POSTEDwebhooks, or poll transactions endpoint
- ✅ Always verify account status is
OPENbefore requesting deposit instructions - ✅ Use webhooks for real-time monitoring instead of polling
- ✅ Subscribe to
ACCOUNT_DEPOSIT_INSTRUCTIONS_UPDATEto catch instruction changes - ✅ Link transactions to deposits using
category_id - ✅ Only credit customers after
TRANSACTION_POSTEDstatus - ✅ Never attempt deposits on reserve accounts
- ✅ Implement proper error handling and retry logic
| Endpoint | Purpose | Documentation |
|---|---|---|
/v1/accounts/deposits | Open new accounts and retrieve account lists | Open Account, Get Accounts |
/v1/accounts/deposits/{id} | Check individual account status and balance | Get Account |
/v1/subscriptions | Set up webhook subscriptions | Create Subscription |
/v1/deposits | Get deposit instructions | Create Deposit |
/v1/accounts/deposits/{id}/transactions | Track incoming deposits and transactions | Get Transactions |
To dive deeper into what you can do on the Rail platform, head to our API documentation.