Payments & Transfers
Beneficiary Management
Add and manage beneficiaries (external bank accounts) for payouts and conversions in supported currencies (EUR, GBP, USD, NGN).
Note: Available in Both APIs: Beneficiary endpoints exist in both Accounts API and Stablecoin API for backwards compatibility. Use Accounts API for same-currency transfers, Stablecoin API for crypto-based cross-currency flows.
Two Use Cases for ACH Pulls
ACH Pulls support two distinct scenarios:
Onramp Transactions
Converting to Crypto
- Pull funds from bank → Convert to crypto → Send to crypto address
- Requires: Crypto address configured
- Use case: User wants to buy crypto with bank funds
Bank Transfers
Keep Funds in Account
- Pull funds from bank → Keep in user's account balance
- Does NOT require: Crypto address
- Use case: User wants to deposit funds to their account
Info: Important: Throughout this guide, sections marked as "Onramp Only" apply only to transactions that convert to crypto. Bank transfers (keeping funds in account) do not require crypto addresses and are not subject to transaction path restrictions.
Create Beneficiary
POST /user/bank-account/remote
Add an external bank account with details for payouts. Works for both domestic and international destinations.
List Beneficiaries
GET /user/bank-account/remote
View all saved beneficiaries.
Get Beneficiary Details
GET /user/bank-account/remote/{uuid}
View specific beneficiary information.
Update
PATCH /user/bank-account/remote
Set a specific beneficiary as the default destination for payouts.
Delete
DELETE /user/bank-account/remote/{uuid}
Remove a saved beneficiary.
Payments & Transfers
Send and receive money:
Transaction History
GET /user/ledger/transactions
View all payment and transfer history for a user, including deposits and payouts.
Open Banking (Plaid)
POST /user/open-banking/payment
ACH pull via Plaid integration - instant account linking.
Info: Note: Sending money to beneficiaries happens when you use payment or remittance endpoints. Direct "send" endpoint coming in future release.
Money OUT (Payouts)
Domestic Payout
Send to same-currency beneficiary:
// 1. Create domestic beneficiary
const beneficiary = await bakkt.payments.createRemoteBankAccount(sessionId, {
first_name: 'John',
last_name: 'Smith',
beneficiary_country: 'US',
account_details: {
currency: 'USD',
account_number: '12365498',
bank_code: '026009593', // ABA routing
bank_name: 'Bank of America'
}
});
// 2. User sends payment
// (Via UI that debits their Bakkt account)
// Funds arrive via ACH in 2-3 business daysInfo: International Currencies: To send money to other currencies like PKR, INR, or TRY, use the Stablecoin API third-party off-ramp feature.
Beneficiary Types
The Accounts API handles all types of beneficiaries:
Domestic Beneficiaries
Same currency, same country - fast settlement:
- US (USD → USD): ACH push, 2-3 business days
- Europe (EUR → EUR): SEPA, same day to 1 business day
- UK (GBP → GBP): FPS, same day
Supported Beneficiary Currencies
Beneficiaries can be created in the same currencies as Virtual IBANs:
- EUR (SEPA - Europe-wide)
- GBP (FPS - United Kingdom)
- USD (ACH - United States)
- NGN (Local - Nigeria)
Info: Other Currencies: Beneficiaries are also available in the Stablecoin API for crypto-to-fiat off-ramps to PKR, INR, TRY, and other currencies.
ACH Operations
ACH Push (via Beneficiary)
Credit beneficiary's external bank account:
// 1. Add beneficiary (one-time setup)
const beneficiary = await bakkt.payments.createRemoteBankAccount(sessionId, {
first_name: 'Jane',
last_name: 'Doe',
beneficiary_country: 'US',
account_details: {
currency: 'USD',
account_number: '987654321',
bank_code: '026009593'
}
});
// 2. User initiates payment to this beneficiary
// (Debit their Bakkt account, credit beneficiary)
// Settlement in 2-3 business daysACH Pull via Plaid
Overview
This section explains how to enable ACH Pulls via Plaid with Bakkt for an individual user. It covers prerequisites, the end‑to‑end flow, API calls, error handling, and troubleshooting.
Prerequisites
Before you begin, ensure the following requirements are met:
- The user must be in status
FULL_USER - User is fully created and eligible for ACH pulls in your system
- Feature flags and environment variables for Plaid and Bakkt are configured in your backend
- You have the user identifier needed by the APIs below
- This functionality works only for individual users. Joint or business users are not supported
Warning: Individual Users Only: ACH Pull functionality is currently only supported for individual users. Joint or business users cannot use this feature.
High‑level Flow
The ACH Pull process follows these key steps:
Step 1: Ensure User Eligibility
Create or ensure the user exists and is in a state eligible for bank linking
Step 2: Get Plaid Link URL
Call the link endpoint to receive a Plaid link_url for the user
Step 3: User Completes Plaid Flow
Direct the user to the link_url where they complete the Plaid authentication and select their bank account
Step 4: Verify the Link
After Plaid completion, verify the link status via the linked bank account endpoint
Step 5: Initiate ACH Pull
Once verified, initiate ACH Pulls to debit the linked external account
Step 1 — Ensure User is Ready
Make sure the user is fully provisioned in your system. If your workflow requires any KYC or profile completion, complete that first.
Create or Ensure User Endpoint
Use this endpoint to create or verify the user profile before proceeding with bank linking.
Step 2 — Get Plaid Link URL
Call the link endpoint to receive a Plaid link_url that the user will use to complete the bank account linking process.
Link Bank Account
Use this endpoint to initiate the bank account linking process and receive a Plaid link URL.
Example Request:
PUT /user/linked-bank-account/linkExample Response:
{
"link_url": "https://plaid.com/link?token=link-sandbox-abc123...",
"status": "pending"
}Info: The
link_urlis a unique URL that directs the user to Plaid's secure authentication flow.
Step 3 — User Completes Plaid Flow
Direct the user to the link_url received in Step 2. The user will:
- Authenticate with Plaid - Securely log into their bank
- Select their bank account - Choose the account for ACH Pulls
- Grant permission - Authorize ACH debits from the selected account
What Happens During Plaid Flow:
Bank Authentication
User securely logs into their bank through Plaid's interface
Account Selection
User selects which checking or savings account to link
Permission Grant
User authorizes ACH debits from the selected account
Account Verification
Plaid verifies the account details and ownership
Warning: User Action Required: The user must complete the entire Plaid flow for the account to be successfully linked. If they close the window or fail authentication, the link will not complete.
Step 4 — Verify the Link / Check Link Status
Use the linked bank account read endpoint to confirm the account is linked and active.
Read Linked Bank Account
Check the status and details of the linked bank account.
What to Check:
Account Status
status: should indicate linked/active- Verify the account is ready for ACH pulls
Error Validation
lastError: should be empty or null- Check for any error messages if linking failed
Account Details
- Verify account details are present (last 4 digits of account, bank name, etc.)
- Confirm the linked account is the one the user selected in Plaid
Info: Single Account Limitation: Only one linked external bank account is supported per user at a time. If you need to change it, you must unlink the current account first.
Unlink Bank Account
Use this endpoint to unlink an external bank account before linking a new one.
Step 5 — Initiate ACH Pull
Once the external account is linked and verified, create the ACH pull request.
Initiate ACH Pull
Create an ACH pull request to debit the user's external bank account.
Example Payload:
{
"amount": 25000 // currency is always USD
}Best Practice: Use a unique
idempotencyKeyper attempt to prevent duplicate transfers in network or retry scenarios.
Code Example:
// Initiate ACH Pull
const achPull = await bakkt.linkedBankAccount.createPull(sessionId, {
amount: 25000,
idempotencyKey: generateUniqueKey() // Generate unique key per request
});
console.log('ACH Pull initiated:', achPull.uuid);
console.log('Status:', achPull.status);Webhooks and Status Updates
After initiating an ACH Pull, monitor payment and ACH transfer webhooks from the provider to track processing states.
Webhook Events to Monitor:
ach_pull.initiated- ACH pull has been createdach_pull.pending- ACH pull is being processedach_pull.completed- ACH pull has settled successfullyach_pull.failed- ACH pull has failedach_pull.returned- ACH pull was returned by the bank
Info: Update your ledger or transaction store from these webhook events to maintain accurate records.
Typical Lifecycle:
Success Path
created → pending → settled
This is the normal flow when everything processes successfully. Funds typically arrive in 2-3 business days.
Failure Path
created → pending → failed/returned
Failed or returned transactions include reason codes that indicate why the ACH pull was unsuccessful.
Error Handling and Common Failure Reasons
Common Issues:
Plaid Flow Not Completed
Problem: The user closed the Plaid window, failed authentication, or didn't complete the linking process.
Solution: Check the link status via the linked bank account endpoint. If status is not ACTIVE, generate a new link URL and have the user retry the Plaid flow.
Account Not Eligible
Problem: Some accounts may not accept ACH debits.
Solution: Verify with the user that their bank account supports ACH debits. Some accounts (like savings accounts) may have restrictions.
Insufficient Permissions
Problem: Feature flag disabled or server configuration issues.
Solution: Ensure server configuration enables Plaid linking and ACH pulls. Check that all required environment variables are set.
Compliance Checks Failed
Problem: Pulls may be blocked if the user fails internal rules or KYC.
Solution: Verify user is in FULL_USER status and has completed all required KYC checks. Review compliance rules that may be blocking the transaction.
Merchant Limit Exceeded
Problem: For unverified merchants, transaction limits may apply and be exceeded.
Solution: Complete merchant verification to increase limits, or ensure individual transactions stay within the allocated limits. Monitor your remaining transaction capacity.
Recommended Approach:
Always Verify
Always read back link status before initiating pulls to ensure the account is properly linked
Clear Error Messages
Surface clear errors to the client and offer unlink → relink flow when appropriate
Constraints and Limits
Single Account
Only one linked external account per user. Linking a new account requires unlinking the current one first using the unlink endpoint.
Individual Users Only
Only supported for individual users. Joint or business users cannot use ACH Pull functionality.
Minimum Amount
ACH pulls must be at least $10.00 USD. Transfers below this minimum will be automatically rejected and refunded.
Currency
ACH Pulls are only available for USD transactions.
Crypto Address Required (Onramp Only)
For onramp transactions only: Users must have a valid target crypto address configured before ACH pulls that convert to crypto can be processed. Set this up during onboarding.
For bank transfers: Crypto address is not required - funds remain in the user's account.
Transaction Restrictions (Onramp Only)
Certain transaction paths may be temporarily restricted due to maintenance or regulatory requirements. This applies only to onramp transactions (converting to crypto). Bank transfers are not affected.
Troubleshooting
| Issue | Solution |
|---|---|
| Link says successful but pulls fail | Verify the account supports ACH debits and that name matching or other compliance checks pass |
| User didn't complete Plaid flow | Check link status. If not ACTIVE, generate a new link URL and have the user retry |
| Transfer rejected - below minimum | Ensure transfer amount is at least $10.00 USD. Update your UI to prevent amounts below this limit |
| Transfer rejected - missing crypto address (onramp only) | For onramp transactions, configure user's target crypto address via onboarding endpoints before initiating pulls. Bank transfers do not require crypto addresses |
| Transfer rejected - path restricted | Certain currency/token paths may be temporarily unavailable. Check system status or contact support |
| 409/duplicate on pull | Reuse of idempotencyKey. Generate a new key for a fresh attempt |
| Need to switch accounts | Call unlink, then repeat link flow with the new account |
| Merchant limit exceeded | Complete merchant verification or wait for limit reset. Monitor transaction capacity |
When troubleshooting, always check the
lastErrorfield in the linked bank account status response for detailed error information.
Sandbox vs. Production
Sandbox
Development Environment
- Use sandbox credentials and test accounts for development
- Validate the full webhook lifecycle in sandbox before moving to production
- Test all error scenarios and edge cases
- ACH pulls typically complete instantly or within minutes in sandbox
Production
Live Environment
- Monitor webhook delivery and set alerts for failures and returns
- ACH pulls take 2-3 business days to settle
- Implement proper retry logic for failed webhooks
- Set up monitoring and alerting for all ACH operations
Warning: Always Test First: Thoroughly test your integration in the sandbox environment before deploying to production.
Security Considerations
Sensitive Data
Treat account and routing numbers as sensitive data. Do not log them in plaintext.
Encryption
Use TLS everywhere, and store secrets in a secure vault (e.g., AWS Secrets Manager, HashiCorp Vault).
Idempotency
Enforce idempotency for transfer creation to avoid duplicates. Always use unique idempotency keys.
Access Control
Implement proper authentication and authorization for all API calls.
Warning: PCI DSS Compliance: If you're handling financial data, ensure your infrastructure meets PCI DSS compliance requirements.
Complete Integration Example
Here's a complete example of the ACH Pull flow from start to finish with proper validation:
// Step 1: Ensure user is ready (FULL_USER status)
const user = await bakkt.getUser(userUuid);
if (user.status !== 'FULL_USER') {
throw new Error('User must complete KYC before linking bank account');
}
// Step 1a: Verify user has crypto address configured (ONLY for onramp transactions)
// Skip this step if you're doing a bank transfer (keeping funds in account)
const isOnrampTransaction = true; // Set to false for bank transfers
if (isOnrampTransaction) {
const userCryptoAddress = await bakkt.getCryptoAddress(userUuid);
if (!userCryptoAddress || !userCryptoAddress.targetAddress) {
throw new Error('User must configure crypto address before onramp ACH pulls. Complete onboarding first.');
}
console.log('User crypto address verified:', userCryptoAddress.targetAddress);
}
// Step 2: Get Plaid link URL
const linkResponse = await bakkt.linkedBankAccount.link(sessionId);
console.log('Plaid link URL:', linkResponse.link_url);
console.log('Status:', linkResponse.status); // "pending"
// Step 3: Direct user to Plaid flow
// In your frontend, redirect or open the link_url
window.location.href = linkResponse.link_url;
// OR use Plaid's modal/iframe integration:
// plaidHandler.open(linkResponse.link_url);
// After user completes Plaid flow...
// (You'll receive a webhook or can poll the status)
// Step 4: Verify the link
const accountStatus = await bakkt.linkedBankAccount.get(sessionId);
if (accountStatus.status !== 'ACTIVE') {
console.error('Link failed:', accountStatus.lastError);
throw new Error('Bank account linking failed');
}
console.log('Link verified successfully');
console.log('Account linked');
// Step 5: Validate amount against minimum requirements
const MINIMUM_USD_AMOUNT = 10;
const requestedAmount = 250;
if (requestedAmount = MINIMUM_USD;
const allValid = Object.values(validations).every(v => v);
if (!allValid) {
console.error('Validation failed:', validations);
const transactionType = isOnrampTransaction ? 'onramp' : 'bank transfer';
throw new Error(`Pre-flight validation failed for ${transactionType}. Check user setup.`);
}
return true;
}
// Use before initiating pull
// For onramp transaction (converting to crypto):
await validateBeforeACHPull(userUuid, 25000, true);
// For bank transfer (keeping funds in account):
await validateBeforeACHPull(userUuid, 25000, false);Quick Reference
| Action | Endpoint |
|---|---|
| Create user profile | POST /user/linked-bank-account/profile |
| Link external bank account | PUT /user/linked-bank-account/link |
| Read linked bank account | GET /user/linked-bank-account |
| Unlink external bank account | DELETE /user/linked-bank-account/unlink |
| Create ACH Pull | POST /user/linked-bank-account/pull |
Payment Rails Supported
United States
Money IN:
- Bank transfers (wire)
- ACH pull (Plaid)
Money OUT:
- ACH push (2-3 days)
- Domestic wire (same day, higher fee)
Europe
Money IN:
- SEPA transfers
- Local bank transfers
Money OUT:
- SEPA (1-2 business days)
- SEPA Instant (minutes, where available)
United Kingdom
Money IN:
- Faster Payments (FPS)
- BACS
- CHAPS
Money OUT:
- FPS (same day)
- BACS (3 business days)
Note: International Transfers: For sending money to currencies beyond EUR/GBP/USD/NGN, use the Stablecoin API for crypto-based cross-border transfers.
Transaction Monitoring
Get Payment History
View all payment and transfer transactions:
// Get user's fiat transaction history (ledger)
const response = await fetch('https://api.bakkt.com/user/ledger/transactions', {
headers: {
'Authorization': 'your_api_key',
'unblock-session-id': sessionId
}
});
const transactions = await response.json();
transactions.forEach(txn => {
console.log(`${txn.type}: ${txn.amount} ${txn.currency}`);
console.log(` Status: ${txn.status}`);
console.log(` Date: ${txn.timestamp}`);
});Get Transaction History
Warning: TODO - New Feature: The
/ledger/transactionsendpoint is currently under development. This section describes the planned functionality.
Use the ledger endpoints to view deposits and withdrawals:
GET /user/ledger/transactions [TODO]
Get all fiat transactions for user's Virtual IBANs
GET /corporate/{uuid}/ledger/transactions [TODO]
GET /corporate/{uuid}/ledger/transactions [TODO]Get all fiat transactions for corporate's Virtual IBANs
GET /user/ledger/transactions?limit=50&offset=0Response:
[
{
"transaction_id": "txn-123",
"account_uuid": "account-uuid",
"type": "CREDIT",
"amount": 1000.00,
"currency": "EUR",
"timestamp": "2024-01-15T10:30:00Z",
"status": "COMPLETED",
"description": "Bank transfer from external account"
}
]Next Steps
Bank Accounts
Learn about creating Virtual IBANs and account management
Advanced Features
Exchange rates, webhooks, compliance, and testing
Stablecoin API
Link bank accounts to crypto for conversions
API Reference
Explore all endpoints with interactive examples
Updated about 2 months ago
