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 days

Info: 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 days

ACH 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/link

Example Response:

{
  "link_url": "https://plaid.com/link?token=link-sandbox-abc123...",
  "status": "pending"
}

Info: The link_url is 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:

  1. Authenticate with Plaid - Securely log into their bank
  2. Select their bank account - Choose the account for ACH Pulls
  3. 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 idempotencyKey per 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 created
  • ach_pull.pending - ACH pull is being processed
  • ach_pull.completed - ACH pull has settled successfully
  • ach_pull.failed - ACH pull has failed
  • ach_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

IssueSolution
Link says successful but pulls failVerify the account supports ACH debits and that name matching or other compliance checks pass
User didn't complete Plaid flowCheck link status. If not ACTIVE, generate a new link URL and have the user retry
Transfer rejected - below minimumEnsure 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 restrictedCertain currency/token paths may be temporarily unavailable. Check system status or contact support
409/duplicate on pullReuse of idempotencyKey. Generate a new key for a fresh attempt
Need to switch accountsCall unlink, then repeat link flow with the new account
Merchant limit exceededComplete merchant verification or wait for limit reset. Monitor transaction capacity

📘

When troubleshooting, always check the lastError field 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

ActionEndpoint
Create user profilePOST /user/linked-bank-account/profile
Link external bank accountPUT /user/linked-bank-account/link
Read linked bank accountGET /user/linked-bank-account
Unlink external bank accountDELETE /user/linked-bank-account/unlink
Create ACH PullPOST /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/transactions endpoint 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 all fiat transactions for corporate's Virtual IBANs

GET /user/ledger/transactions?limit=50&offset=0

Response:

[
  {
    "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