# Withdrawals 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 Withdrawals? Withdrawals allow customers to send funds from their Rail accounts to external bank accounts or crypto wallets. The withdrawal process involves creating a counterparty (the external destination), initiating the withdrawal, and accepting it for execution. Rail supports withdrawals via multiple rails: - **FIAT** - Bank transfers (ACH, Wire, SWIFT, etc.) - **CRYPTO** - Blockchain withdrawals to external wallets ### Auto-RFI (Request for Information) Rail's Auto-RFI functionality automatically triggers document requests when withdrawals exceed certain thresholds. This ensures compliance requirements are met before processing large transactions. **How Auto-RFI Works:** - **Below Threshold** - Withdrawal proceeds normally without additional documents - **Above Threshold** - Withdrawal enters `CHANGES_REQUESTED` status and requires documents (e.g., invoices) before it can be accepted ## Core API Flow Sequence The standard withdrawal flow follows these key steps: 1. **POST** [`/v1/counterparties`](/api-docs/openapi/rail-spec#operation/createCounterparty) - Create a counterparty (external destination) 2. **POST** [`/v1/withdrawals`](/api-docs/openapi/rail-spec#operation/createWithdrawal) - Initiate the withdrawal 3. **GET** [`/v1/withdrawals/{id}/status`](/api-docs/openapi/rail-spec#operation/getWithdrawalStatus) - Check withdrawal status (if Auto-RFI triggered) 4. **POST** [`/v1/documents/{document_id}`](/api-docs/openapi/rail-spec#operation/uploadDocument) - Upload required documents (if needed) 5. **POST** [`/v1/withdrawals/{id}/accept`](/api-docs/openapi/rail-spec#operation/acceptWithdrawal) - Accept the withdrawal for execution ## Build Guide This guide covers two scenarios: 1. [Simple Withdrawal (Below Threshold)](#scenario-1-simple-withdrawal-below-threshold) 2. [Withdrawal with Auto-RFI (Above Threshold)](#scenario-2-withdrawal-with-auto-rfi-above-threshold) By the end of this guide you will have: 1. [Authenticated your request](#authenticating-your-request) 2. [Created a counterparty](#step-1-create-a-counterparty) 3. [Initiated a withdrawal](#step-2-create-a-withdrawal) 4. [Handled Auto-RFI document requests (if triggered)](#scenario-2-withdrawal-with-auto-rfi-above-threshold) 5. [Accepted the withdrawal](#step-4-accept-the-withdrawal) 6. [Monitored withdrawal status](#step-5-monitor-withdrawal-status) 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. 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=counterparties:read+counterparties:write+withdrawals:read+withdrawals:write+accounts:read' \ --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": "counterparties:read counterparties:write withdrawals:read withdrawals:write accounts:read" } ``` The scopes we'll need for this tutorial are: - `counterparties:read` and `counterparties:write` to create and manage counterparties - `withdrawals:read` and `withdrawals:write` to create and manage withdrawals - `accounts:read` to verify account balances The full list of scopes are available [here](/guides/authentication). ## Scenario 1: Simple Withdrawal (Below Threshold) This scenario covers a straightforward withdrawal that doesn't trigger Auto-RFI. ### Step 1: Create a Counterparty A counterparty represents the external destination where funds will be sent. You must create a counterparty before initiating a withdrawal. #### Creating a Fiat Counterparty (Bank Account) Create a counterparty for a US bank account using the [`/v1/counterparties`](/api-docs/openapi/rail-spec#operation/createCounterparty) endpoint: ```bash curl --location --request POST 'https://sandbox.layer2financial.com/api/v1/counterparties' \ --header 'Authorization: Bearer {AUTH_TOKEN}' \ --header 'Content-Type: application/json' \ --data-raw '{ "customer_id": "INDIVIDUAL_CUSTOMER_001", "description": "John'\''s Chase Checking Account", "counterparty_type": "FIAT_US", "is_international": false, "supported_rails": ["ACH", "FEDWIRE"], "profile": { "name": "John Doe", "address": { "address_line1": "456 Bank Street", "city": "New York", "state": "NY", "postal_code": "10001", "country_code": "US" }, "relationship_to_customer": "SELF" }, "account_information": { "asset_type_id": "FIAT_TESTNET_USD", "account_number": "1234567890", "routing_number": "021000021", "account_type": "CHECKING" } }' ``` The response includes the `counterparty_id` you'll need for the withdrawal: ```javascript { "data": { "id": "cpty_abc123456", "status": "ACTIVE" } } ``` Important Certain counterparty types require specific asset types in the `account_information.asset_type_id` field. For example, `FIAT_CN` counterparties must use Chinese Yuan asset types. See the [Counterparties Guide](/guides/counterparties) for details. #### Creating a Crypto Counterparty (Wallet Address) Create a counterparty for a crypto wallet: ```bash curl --location --request POST 'https://sandbox.layer2financial.com/api/v1/counterparties' \ --header 'Authorization: Bearer {AUTH_TOKEN}' \ --header 'Content-Type: application/json' \ --data-raw '{ "customer_id": "INDIVIDUAL_CUSTOMER_001", "description": "Customer'\''s Personal USDC Wallet", "counterparty_type": "CRYPTO", "is_international": false, "supported_rails": ["CRYPTO"], "profile": { "name": "John Doe", "address": { "country_code": "US" }, "relationship_to_customer": "SELF" }, "wallet_information": { "asset_type_id": "ETHEREUM_GOERLI_USDC", "blockchain_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", "wallet_type": "OTHER", "institution_name": "Personal Wallet", "institution_address": { "country_code": "US" } } }' ``` Response: ```javascript { "data": { "id": "cpty_xyz789012", "status": "ACTIVE" } } ``` ### Step 2: Create a Withdrawal Initiate a withdrawal using the [`/v1/withdrawals`](/api-docs/openapi/rail-spec#operation/createWithdrawal) endpoint: ```bash curl --location --request POST 'https://sandbox.layer2financial.com/api/v1/withdrawals' \ --header 'Authorization: Bearer {AUTH_TOKEN}' \ --header 'Content-Type: application/json' \ --data-raw '{ "withdrawal_rail": "ACH", "description": "Payment to vendor", "source_account_id": "CUSTOMER_USD_001", "amount": 500.00, "destination_counterparty_id": "cpty_abc123456", "memo": "Invoice #12345" }' ``` > **Important:** Use `destination_counterparty_id` (not just `counterparty_id`) and ensure `withdrawal_rail` is specified as it's a mandatory field. The response includes the `withdrawal_id` and initial status: ```javascript { "data": { "id": "withdrawal_abc123", "status": "REQUESTED", "created_timestamp": "2023-11-20T14:32:18.123456Z", "source_details": { "source_account_id": "CUSTOMER_USD_001", "asset_type_id": "FIAT_TESTNET_USD", "amount_to_debit": 500.00 }, "destination_details": { "destination_counterparty_id": "cpty_abc123456", "withdrawal_rail": "ACH" } } } ``` ### Step 3: Check Withdrawal Status For withdrawals below the Auto-RFI threshold, the status will be `REQUESTED`. Check the status using the [`/v1/withdrawals/{id}/status`](/api-docs/openapi/rail-spec#operation/getWithdrawalStatus) endpoint: ```bash curl --location --request GET 'https://sandbox.layer2financial.com/api/v1/withdrawals/withdrawal_abc123/status' \ --header 'Authorization: Bearer {AUTH_TOKEN}' \ --header 'Content-Type: application/json' ``` Response for a withdrawal below threshold: ```javascript { "data": { "id": "withdrawal_abc123", "status": "REQUESTED", "created_timestamp": "2023-11-20T14:32:18.123456Z", "withdrawal_validation_errors": [], "withdrawal_document_errors": [] } } ``` Since the status is `REQUESTED` and there are no validation or document errors, you can proceed to accept the withdrawal. ### Step 4: Accept the Withdrawal Accept the withdrawal to queue it for execution: ```bash curl --location --request POST 'https://sandbox.layer2financial.com/api/v1/withdrawals/withdrawal_abc123/accept' \ --header 'Authorization: Bearer {AUTH_TOKEN}' \ --header 'Content-Type: application/json' ``` Response: ```javascript { "data": { "id": "withdrawal_abc123", "status": "ACCEPTED" } } ``` ### Step 5: Monitor Withdrawal Status The withdrawal will progress through various statuses: ```bash curl --location --request GET 'https://sandbox.layer2financial.com/api/v1/withdrawals/withdrawal_abc123' \ --header 'Authorization: Bearer {AUTH_TOKEN}' \ --header 'Content-Type: application/json' ``` Response showing progression: ```javascript { "data": { "id": "withdrawal_abc123", "status": "PROCESSING", // Then moves to COMPLETED or FAILED "created_timestamp": "2023-11-20T14:32:18.123456Z", "accepted_timestamp": "2023-11-20T14:33:00.000000Z", "source_details": { "source_account_id": "CUSTOMER_USD_001", "asset_type_id": "FIAT_TESTNET_USD", "amount_to_debit": 500.00 }, "destination_details": { "destination_counterparty_id": "cpty_abc123456", "withdrawal_rail": "ACH" } } } ``` ## Scenario 2: Withdrawal with Auto-RFI (Above Threshold) This scenario covers withdrawals that exceed the Auto-RFI threshold and require document uploads before acceptance. ### Step 1: Create a Counterparty Create a counterparty the same way as in Scenario 1: ```bash curl --location --request POST 'https://sandbox.layer2financial.com/api/v1/counterparties' \ --header 'Authorization: Bearer {AUTH_TOKEN}' \ --header 'Content-Type: application/json' \ --data-raw '{ "customer_id": "INDIVIDUAL_CUSTOMER_001", "description": "Vendor Payment Account", "counterparty_type": "FIAT_US", "is_international": false, "supported_rails": ["FEDWIRE"], "profile": { "name": "ABC Corp", "address": { "address_line1": "789 Business Ave", "city": "San Francisco", "state": "CA", "postal_code": "94105", "country_code": "US" }, "relationship_to_customer": "VENDOR" }, "account_information": { "asset_type_id": "FIAT_TESTNET_USD", "account_number": "9876543210", "routing_number": "121000248", "account_type": "BUSINESS_CHECKING" } }' ``` Response: ```javascript { "data": { "id": "cpty_vendor_123", "status": "ACTIVE" } } ``` ### Step 2: Create a Withdrawal (Above Threshold) Create a withdrawal for an amount that exceeds the Auto-RFI threshold: ```bash curl --location --request POST 'https://sandbox.layer2financial.com/api/v1/withdrawals' \ --header 'Authorization: Bearer {AUTH_TOKEN}' \ --header 'Content-Type: application/json' \ --data-raw '{ "withdrawal_rail": "FEDWIRE", "description": "Large vendor payment", "source_account_id": "CUSTOMER_USD_001", "amount": 25000.00, "destination_counterparty_id": "cpty_vendor_123", "memo": "Invoice #INV-2023-1234" }' ``` Response indicating Auto-RFI triggered: ```javascript { "data": { "id": "withdrawal_large_456", "status": "CHANGES_REQUESTED", "created_timestamp": "2023-11-20T15:00:00.123456Z", "source_details": { "source_account_id": "CUSTOMER_USD_001", "asset_type_id": "FIAT_TESTNET_USD", "amount_to_debit": 25000.00 }, "destination_details": { "destination_counterparty_id": "cpty_vendor_123", "withdrawal_rail": "FEDWIRE" } } } ``` > **Notice:** The status is `CHANGES_REQUESTED` instead of `REQUESTED`. This indicates that Auto-RFI has been triggered and documents are required. ### Step 3: Get Document Requirements Similar to the application onboarding process, check the withdrawal status to see which documents are required: ```bash curl --location --request GET 'https://sandbox.layer2financial.com/api/v1/withdrawals/withdrawal_large_456/status' \ --header 'Authorization: Bearer {AUTH_TOKEN}' \ --header 'Content-Type: application/json' ``` Response showing required documents: ```javascript { "data": { "id": "withdrawal_large_456", "status": "CHANGES_REQUESTED", "created_timestamp": "2023-11-20T15:00:00.123456Z", "withdrawal_validation_errors": [], "withdrawal_document_errors": [ { "document_id": "doc_withdrawal_001", "document_type": "INVOICE", "status": "MISSING", "description": "Invoice or receipt for this transaction" }, { "document_id": "doc_withdrawal_002", "document_type": "CONTRACT", "status": "MISSING", "description": "Contract or agreement with vendor" } ] } } ``` The `withdrawal_document_errors` array contains the `document_id`s you need for uploading documents. > **Important:** This is the same status endpoint used throughout the withdrawal lifecycle. It serves a dual purpose - providing status updates and identifying required documents (similar to how the application status endpoint works in customer onboarding). ### Step 4: Upload Required Documents Upload the required documents using the `document_id`s from the status response: ```bash # Upload invoice curl --location --request POST 'https://sandbox.layer2financial.com/api/v1/documents/doc_withdrawal_001' \ --header 'Authorization: Bearer {AUTH_TOKEN}' \ --form 'file=@"/path/to/invoice.pdf"' # Upload contract curl --location --request POST 'https://sandbox.layer2financial.com/api/v1/documents/doc_withdrawal_002' \ --header 'Authorization: Bearer {AUTH_TOKEN}' \ --form 'file=@"/path/to/contract.pdf"' ``` Each successful upload returns: ```javascript { "data": { "id": "doc_withdrawal_001", "status": "UPLOADED" } } ``` ### Step 5: Verify Withdrawal is Ready After uploading documents, check the withdrawal status again: ```bash curl --location --request GET 'https://sandbox.layer2financial.com/api/v1/withdrawals/withdrawal_large_456/status' \ --header 'Authorization: Bearer {AUTH_TOKEN}' \ --header 'Content-Type: application/json' ``` Once all documents are uploaded, the status returns to `REQUESTED`: ```javascript { "data": { "id": "withdrawal_large_456", "status": "REQUESTED", "created_timestamp": "2023-11-20T15:00:00.123456Z", "withdrawal_validation_errors": [], "withdrawal_document_errors": [] } } ``` ### Step 6: Accept the Withdrawal Now that the status is `REQUESTED` and all documents are uploaded, you can accept the withdrawal: ```bash curl --location --request POST 'https://sandbox.layer2financial.com/api/v1/withdrawals/withdrawal_large_456/accept' \ --header 'Authorization: Bearer {AUTH_TOKEN}' \ --header 'Content-Type: application/json' ``` Response: ```javascript { "data": { "id": "withdrawal_large_456", "status": "ACCEPTED" } } ``` The withdrawal is now queued for execution and will be processed by Rail's banking partners. ## Withdrawal Status Flow Withdrawals progress through different statuses during their lifecycle: ### Without Auto-RFI (Below Threshold) **REQUESTED** → **ACCEPTED** → **PROCESSING** → **COMPLETED** / **FAILED** ### With Auto-RFI (Above Threshold) **CHANGES_REQUESTED** → *(upload documents)* → **REQUESTED** → **ACCEPTED** → **PROCESSING** → **COMPLETED** / **FAILED** ### Status Meanings | Status | Description | Action Required | | --- | --- | --- | | `REQUESTED` | Withdrawal created and ready to accept | Call `/accept` endpoint | | `CHANGES_REQUESTED` | Auto-RFI triggered, documents required | Upload required documents via status endpoint | | `ACCEPTED` | Withdrawal accepted and queued for execution | Monitor status for completion | | `PROCESSING` | Withdrawal being processed by banking partners | Wait for completion | | `COMPLETED` | Withdrawal successfully processed | None - funds sent | | `FAILED` | Withdrawal failed | Review error details, may need to retry | | `CANCELLED` | Withdrawal cancelled | None - no funds sent | > **Important:** You cannot accept a withdrawal while it's in `CHANGES_REQUESTED` status. The withdrawal must return to `REQUESTED` status after all required documents are uploaded. ## Managing Counterparties ### Retrieve All Counterparties List all counterparties for a customer: ```bash curl --location --request GET 'https://sandbox.layer2financial.com/api/v1/counterparties?customer_id=INDIVIDUAL_CUSTOMER_001' \ --header 'Authorization: Bearer {AUTH_TOKEN}' \ --header 'Content-Type: application/json' ``` Response: ```javascript { "data": { "counterparties": [ { "id": "cpty_abc123456", "customer_id": "INDIVIDUAL_CUSTOMER_001", "description": "John's Chase Checking Account", "counterparty_type": "FIAT_US", "status": "ACTIVE", "supported_rails": ["ACH", "FEDWIRE"] }, { "id": "cpty_xyz789012", "customer_id": "INDIVIDUAL_CUSTOMER_001", "description": "Customer's Personal USDC Wallet", "counterparty_type": "CRYPTO", "status": "ACTIVE", "supported_rails": ["CRYPTO"] } ] } } ``` ### Retrieve a Specific Counterparty Get details for a single counterparty: ```bash curl --location --request GET 'https://sandbox.layer2financial.com/api/v1/counterparties/cpty_abc123456' \ --header 'Authorization: Bearer {AUTH_TOKEN}' \ --header 'Content-Type: application/json' ``` ### Update a Counterparty Update counterparty details: ```bash curl --location --request PUT 'https://sandbox.layer2financial.com/api/v1/counterparties/cpty_abc123456' \ --header 'Authorization: Bearer {AUTH_TOKEN}' \ --header 'Content-Type: application/json' \ --data-raw '{ "description": "John'\''s Updated Checking Account", "profile": { "name": "John Doe", "address": { "address_line1": "789 New Address St", "city": "New York", "state": "NY", "postal_code": "10002", "country_code": "US" } } }' ``` ### Delete a Counterparty Remove a counterparty: ```bash curl --location --request DELETE 'https://sandbox.layer2financial.com/api/v1/counterparties/cpty_abc123456' \ --header 'Authorization: Bearer {AUTH_TOKEN}' \ --header 'Content-Type: application/json' ``` Response: ```javascript { "data": { "id": "cpty_abc123456", "status": "DELETED" } } ``` ## Withdrawal Best Practices ### 1. Verify Account Balance Before Creating Withdrawals Always check that the source account has sufficient available balance: ```bash curl --location --request GET 'https://sandbox.layer2financial.com/api/v1/accounts/deposits/CUSTOMER_USD_001' \ --header 'Authorization: Bearer {AUTH_TOKEN}' ``` Ensure `available_balance` is greater than or equal to the withdrawal amount. ### 2. Use the Status Endpoint Throughout the Lifecycle The `/v1/withdrawals/{id}/status` endpoint serves multiple purposes: - Check if Auto-RFI was triggered - Get document IDs for required documents - Verify all requirements are met before accepting - Monitor withdrawal progress ```bash # Always check status before attempting to accept GET /v1/withdrawals/{id}/status # If status is "REQUESTED" and no errors, proceed to accept POST /v1/withdrawals/{id}/accept ``` ### 3. Handle Auto-RFI Similar to Application Onboarding The Auto-RFI process mirrors the application onboarding flow: 1. Check status endpoint for required documents 2. Upload documents using the provided `document_id`s 3. Re-check status to verify all requirements are met 4. Only then can you accept the withdrawal ### 4. Provide Meaningful Descriptions and Memos Include clear descriptions and memos for audit trails and customer communication: ```javascript { "description": "Payment for Invoice #INV-2023-1234", "memo": "Q4 2023 Services - Net 30" } ``` ### 5. Match Asset Types Between Accounts and Counterparties Ensure the `asset_type_id` of the source account matches the counterparty's expected asset type: - Fiat withdrawals: Account and counterparty must use the same fiat currency - Crypto withdrawals: Account and wallet must use the same blockchain and token ### 6. Set Up Webhook Subscriptions for Withdrawal Events Monitor withdrawal lifecycle events in real-time: ```bash # Subscribe to withdrawal 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": "WITHDRAWAL_COMPLETED", "callback_url": "https://your-domain.com/webhooks/withdrawals" }' ``` Recommended withdrawal webhooks: - `WITHDRAWAL_REQUESTED` - Withdrawal initiated - `WITHDRAWAL_ACCEPTED` - Withdrawal accepted for processing - `WITHDRAWAL_PROCESSING` - Withdrawal being processed - `WITHDRAWAL_COMPLETED` - Withdrawal successfully sent - `WITHDRAWAL_FAILED` - Withdrawal failed - `WITHDRAWAL_CANCELLED` - Withdrawal cancelled ### 7. Implement Proper Error Handling Handle common withdrawal errors: ```javascript try { const withdrawal = await createWithdrawal({ amount: 25000, destination_counterparty_id: counterpartyId }); const status = await getWithdrawalStatus(withdrawal.id); if (status.status === 'CHANGES_REQUESTED') { // Auto-RFI triggered - handle document upload await handleDocumentRequirements(status.withdrawal_document_errors); } else if (status.status === 'REQUESTED') { // Ready to accept await acceptWithdrawal(withdrawal.id); } } catch (error) { if (error.code === 'INSUFFICIENT_BALANCE') { // Handle insufficient funds } else if (error.code === 'INVALID_COUNTERPARTY') { // Handle invalid counterparty } // Log and notify } ``` ### 8. Store Counterparties for Repeat Withdrawals Save frequently used counterparties to simplify repeat withdrawals: ```javascript // First time - create and store counterparty const counterparty = await createCounterparty({...}); await database.saveCounterparty(customerId, counterparty.id); // Subsequent withdrawals - reuse counterparty const storedCounterpartyId = await database.getCounterparty(customerId); await createWithdrawal({ destination_counterparty_id: storedCounterpartyId, amount: 1000 }); ``` ## Summary Let's review the withdrawal process: ### Simple Withdrawal (Below Threshold) 1. **Authenticate** - Get your `AUTH_TOKEN` using OAuth2 2. **Create Counterparty** - POST to [`/v1/counterparties`](/api-docs/openapi/rail-spec#operation/createCounterparty) with destination details 3. **Create Withdrawal** - POST to [`/v1/withdrawals`](/api-docs/openapi/rail-spec#operation/createWithdrawal) with `destination_counterparty_id` and `withdrawal_rail` 4. **Check Status** - GET [`/v1/withdrawals/{id}/status`](/api-docs/openapi/rail-spec#operation/getWithdrawalStatus) to verify status is `REQUESTED` 5. **Accept Withdrawal** - POST to [`/v1/withdrawals/{id}/accept`](/api-docs/openapi/rail-spec#operation/acceptWithdrawal) to queue for execution 6. **Monitor Progress** - Track withdrawal through `PROCESSING` to `COMPLETED` ### Withdrawal with Auto-RFI (Above Threshold) 1. **Authenticate** - Get your `AUTH_TOKEN` using OAuth2 2. **Create Counterparty** - POST to [`/v1/counterparties`](/api-docs/openapi/rail-spec#operation/createCounterparty) 3. **Create Withdrawal** - POST to [`/v1/withdrawals`](/api-docs/openapi/rail-spec#operation/createWithdrawal) 4. **Check Status** - GET [`/v1/withdrawals/{id}/status`](/api-docs/openapi/rail-spec#operation/getWithdrawalStatus) - status will be `CHANGES_REQUESTED` 5. **Upload Documents** - POST to [`/v1/documents/{document_id}`](/api-docs/openapi/rail-spec#operation/uploadDocument) for each required document 6. **Re-check Status** - Verify status has returned to `REQUESTED` 7. **Accept Withdrawal** - POST to [`/v1/withdrawals/{id}/accept`](/api-docs/openapi/rail-spec#operation/acceptWithdrawal) 8. **Monitor Progress** - Track withdrawal through `PROCESSING` to `COMPLETED` ### Key Takeaways - ✅ Create counterparties before initiating withdrawals - ✅ Use `destination_counterparty_id` (not `counterparty_id`) in withdrawal requests - ✅ Always specify `withdrawal_rail` as it's mandatory - ✅ Match asset types between accounts and counterparties - ✅ Use the status endpoint to check for Auto-RFI document requirements - ✅ Handle `CHANGES_REQUESTED` status similar to application onboarding - ✅ Upload all required documents before attempting to accept - ✅ Set up webhooks to monitor withdrawal progress - ✅ Verify account balance before creating withdrawals ### API Endpoints Used | Endpoint | Purpose | Documentation | | --- | --- | --- | | `/v1/counterparties` | Create and manage external destinations | [Create Counterparty](/api-docs/openapi/rail-spec#operation/createCounterparty), [Counterparties Guide](/guides/counterparties) | | `/v1/withdrawals` | Create withdrawal requests | [Create Withdrawal](/api-docs/openapi/rail-spec#operation/createWithdrawal) | | `/v1/withdrawals/{id}/status` | Check withdrawal status and document requirements | [Get Withdrawal Status](/api-docs/openapi/rail-spec#operation/getWithdrawalStatus) | | `/v1/withdrawals/{id}/accept` | Accept withdrawal for execution | [Accept Withdrawal](/api-docs/openapi/rail-spec#operation/acceptWithdrawal) | | `/v1/documents/{document_id}` | Upload required documents | [Upload Document](/api-docs/openapi/rail-spec#operation/uploadDocument) | To dive deeper into what you can do on the Rail platform, head to our [API documentation](/api-docs/openapi/rail-spec).