Payments & Transfers

For safe retries on POST/PUT (optional Idempotency-Key header, cached responses, TTL), see Idempotent requests.


Recipient Management

Add and manage recipients (external bank accounts) for payouts in supported currencies (EUR, GBP, USD, NGN).

Note:
Available in Both APIs: The same concept is exposed in the Stablecoin API (where it is called "beneficiaries") for backwards compatibility. Use Accounts API for same-currency transfers.

ACH Pull Use Case

ACH Pulls allow users to deposit funds from their external bank account to their Bakkt account:

  • Pull funds from bank → Keep in user's account balance
  • Use case: User wants to deposit funds to their account

Create Recipient

POST /user/bank-account/remote

Add an external bank account with details for payouts. Works for both domestic and international destinations.

List Recipients

GET /user/bank-account/remote

View all saved recipients.

Get Recipient Details

GET /user/bank-account/remote/{uuid}

View specific recipient information.

Update

PATCH /user/bank-account/remote

Set a specific recipient as the default destination for payouts.

Delete

DELETE /user/bank-account/remote/{uuid}

Remove a saved recipient.

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.

Info:
Note: Sending money to recipients happens when you use payment or remittance endpoints. Direct "send" endpoint coming in future release.

Money OUT (Payouts)

Domestic Payout

Send to same-currency recipient:

POST /user/bank-account/remote

{
  "account_name": "Main Bank Account",
  "main_recipient": true,
  "account_details": {
    "currency": "USD",
    "transfer_method": "ACH",
    "account_number": "12365498",
    "routing_number": "026009593",
    "account_type": "Checking",
    "recipient_relationship": "Customer"
  }
}

After saving the recipient, the user initiates a payment via your UI — debiting their Bakkt account and crediting the recipient. 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.

Recipient Types

The Accounts API handles all types of recipients:

Domestic Recipients

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 Recipient Currencies

Recipients can be created in the same currencies as bank accounts:

  • EUR (SEPA - Europe-wide)
  • GBP (FPS - United Kingdom)
  • USD (ACH - United States)
  • NGN (Local - Nigeria)

Info:
Other Currencies: For PKR, INR, TRY, and other cross-border destinations, use the Stablecoin API (its equivalent feature is called "beneficiaries").

ACH Operations

ACH Push (via Recipient)

Credit recipient's external bank account:

POST /user/bank-account/remote

{
  "account_name": "Payroll Account",
  "main_recipient": false,
  "account_details": {
    "currency": "USD",
    "transfer_method": "ACH",
    "account_number": "987654321",
    "routing_number": "026009593",
    "account_type": "Checking",
    "recipient_relationship": "Customer"
  }
}

The user then initiates a payment to this recipient — debiting their Bakkt account and crediting the recipient. 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.

Accounts API (Bakkt checking, General Ledger)

The Accounts API exposes ACH pull against the user's Bakkt checking balance via General Ledger (CreateAchPull):

POST /accounts/user/account/bakkt/{account_uuid}/ach-pull

Destination checking is {account_uuid} in the path. Auth: userSessionOrUuidAuthorizer (session or user-uuid header plus API key).

Request body (JSON only — do not send account_uuid in the body; it is taken from the path):

{
  "amount": 100.5,
  "currency": "USD",
  "source_account_uuid": "<linked-bank-account-uuid>",
  "ip_address": "203.0.113.1"
}

Optional fields: receiver_type (defaults to BANK_ACCOUNT), target_account_uuid, user_note (max 500 characters). amount must be positive with at most two decimal places; ip_address must be a valid IPv4 or IPv6 string.

Success (201) — GL result shape, plus process_uuid set equal to transaction_id for compatibility with older integrations:

{
  "transaction_id": "<gl-transaction-id>",
  "process_uuid": "<same-as-transaction_id>",
  "user_uuid": "<end-user-uuid>",
  "amount": 100.5,
  "currency": "USD"
}

Full OpenAPI: see ach-pull-request / ach-pull-response and the POST operation under Payouts & Transfers in docs/accounts_api.json.

Linked-bank account endpoint

POST /user/linked-bank-account/pull

Example Payload:

{
  "amount": 25000
}

Example Response:

{
  "uuid": "<ach-pull-uuid>",
  "status": "PENDING"
}

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.

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
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

End-to-end ACH Pull flow:

Step 1 — Ensure user is FULL_USER

GET /user

The response status field must be FULL_USER. Otherwise, complete KYC before linking a bank account.

Step 2 — Get Plaid link URL

PUT /user/linked-bank-account/link

Response:

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

Step 3 — Direct user to Plaid flow

Redirect the user to the link_url (or open it in Plaid's modal/iframe). After the user completes the flow, you'll receive a webhook or can poll the status.

Step 4 — Verify the link

GET /user/linked-bank-account

The response status must be ACTIVE. If not, inspect lastError and have the user retry the Plaid flow.

Step 5 — Validate amount

Reject locally before calling the API if the requested amount is below the $10.00 USD minimum.

Step 6 — Initiate ACH Pull

POST /user/linked-bank-account/pull
{
  "amount": 25000
}

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)

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:
Cross-Border Remittance: The Accounts API remittance flow (POST /user/remittance/quotePOST /user/remittance/start) supports any-to-any conversion between the following 20 currencies: USD, EUR, GBP, NGN, PKR, INR, TRY, MXN, BRL, ARS, IDR, PHP, AED, OMR, QAR, VND, COP, GHS, JPY, GTQ.

Warning:
Currently disabled: The remittance flow is not yet enabled in production. Reach out to your Bakkt contact for rollout timing.

Transaction Monitoring

Get Payment History

View all payment and transfer transactions:

GET /user/ledger/transactions

Required headers:

  • Authorization: <your_api_key>
  • bakkt-session-id: <session_id>

The response is an array of transactions, each with type, amount, currency, status, and timestamp fields.

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 accounts

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 checking and savings accounts and account management

Advanced Features

Exchange rates, webhooks, compliance, and testing

API Reference

Explore all endpoints with interactive examples