Skip to main content

Bank Accounts

Overview

The API lets users connect external bank accounts via Plaid, then keeps balances and transactions in sync automatically. Once linked, account data flows into the unified transaction feed alongside crypto and card activity. The banking system is:
  • Plaid-powered — accounts are connected via Plaid Link, with encrypted access token storage
  • Auto-syncing — transactions sync on demand, via webhooks, or both
  • Cursor-based — uses Plaid’s incremental sync to fetch only new/modified transactions
  • Paginated — large histories are synced in pages (up to 10,000 transactions per sync)
  • Multi-currency — transactions are stored in their native currency

Quick Start

Get a Plaid Link token to open the bank connection UI in your application:
curl -X POST /v0/user/me/bank/link-token \
  -H "x-juno-jwt: <token>" \
  -H "Content-Type: application/json" \
  -d '{}'
Use the link_token from the response to open Plaid Link in your frontend. When the user selects an account, Plaid returns a public_token and account_id.

3. Exchange the token

Exchange the Plaid public token to link the account:
curl -X POST /v0/user/me/bank/accounts \
  -H "x-juno-jwt: <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "public_token": "public-sandbox-abc123",
    "account_id": "plaid_account_id",
    "set_as_primary": true
  }'
Response: 201 Created
{
  "account": {
    "external_id": "acc_abc123",
    "provider": "plaid",
    "institution_name": "Chase",
    "account_name": "Checking",
    "account_mask": "4567",
    "account_type": "checking",
    "is_primary": true,
    "status": "active",
    "verification_status": "verified",
    "balance_current": "2450.75",
    "balance_available": "2200.00",
    "balance_currency": "USD",
    "balance_updated_at": 1700000000000,
    "created_at": 1700000000000
  },
  "auth_fetched": true,
  "_links": {
    "self": { "href": "/v0/user/me/bank/accounts/acc_abc123" },
    "user": { "href": "/v0/user/me" },
    "list": { "href": "/v0/user/me/bank/accounts" }
  }
}

4. Trigger a sync

Fetch the account’s transactions from Plaid:
curl -X POST /v0/user/me/bank/accounts/acc_abc123/sync?sync_type=transactions \
  -H "x-juno-jwt: <token>"
Response: 202 Accepted — the sync runs asynchronously.

5. Read transactions

Once the sync completes, fetch the transactions:
curl /v0/user/me/bank/accounts/acc_abc123/transactions \
  -H "x-juno-jwt: <token>"

Bank Account Data

Listing Accounts

GET /v0/user/me/bank/accounts
Returns all linked bank accounts for the authenticated user. Response: 200 OK
{
  "accounts": [
    {
      "external_id": "acc_abc123",
      "provider": "plaid",
      "institution_name": "Chase",
      "account_name": "Checking",
      "account_mask": "4567",
      "account_type": "checking",
      "is_primary": true,
      "status": "active",
      "verification_status": "verified",
      "balance_current": "2450.75",
      "balance_available": "2200.00",
      "balance_currency": "USD",
      "balance_updated_at": 1700000000000,
      "created_at": 1700000000000
    }
  ],
  "total": 1,
  "_links": {
    "self": { "href": "/v0/user/me/bank/accounts" },
    "link-token": { "href": "/v0/user/me/bank/link-token", "method": "POST" }
  }
}

Response Shape

FieldTypeDescription
external_idstringStable account identifier for API calls
providerstringBanking provider (plaid)
institution_namestringBank name (e.g. “Chase”, “Wells Fargo”)
institution_logo_urlstring or nullURL to the institution’s logo
account_namestringAccount name from the bank (e.g. “Checking”)
account_maskstring or nullLast 4 digits of the account number
account_typestringOne of: checking, savings, credit_card, loan, investment
is_primarybooleanWhether this is the user’s primary bank account
statusstringConnection status (see Account Statuses)
verification_statusstring or nullOwnership verification state (see Verification Statuses)
nicknamestring or nullUser-set custom name
balance_currentdecimal or nullCurrent balance
balance_availabledecimal or nullAvailable balance (after pending transactions)
balance_currencystringCurrency code (e.g. USD)
balance_updated_atinteger or nullWhen balances were last refreshed (epoch ms)
created_atintegerWhen the account was linked (epoch ms)

Transaction Sync

How Sync Works

Transaction sync uses Plaid’s cursor-based incremental sync API. Each sync fetches only transactions that have been added, modified, or removed since the last sync. Syncs can be triggered two ways:
  1. On demandPOST /v0/user/me/bank/accounts/{id}/sync
  2. Automatically — Plaid sends a webhook when new transactions are available

Triggering a Sync

POST /v0/user/me/bank/accounts/{external_id}/sync
Query ParameterTypeDefaultDescription
sync_typestringtransactionsOne of: balance, transactions, full
Sync TypeDescription
balanceFetch current balances only
transactionsFetch new/modified/removed transactions (includes balances)
fullFull re-sync of transactions and balances
Response: 202 Accepted
{
  "event_id": "msg_abc123",
  "status": "pending",
  "_links": {
    "self": { "href": "/v0/user/me/bank/accounts/acc_abc123/sync", "method": "POST" },
    "status": { "href": "/v0/user/me/bank/accounts/acc_abc123/sync/status" },
    "account": { "href": "/v0/user/me/bank/accounts/acc_abc123" }
  }
}
The sync runs asynchronously. Use the status link to poll for completion.

Checking Sync Status

GET /v0/user/me/bank/accounts/{external_id}/sync/status
Response: 200 OK
{
  "account_external_id": "acc_abc123",
  "last_synced_at": 1700000060000,
  "cursor": "CAESJjBi...",
  "is_stale": false,
  "_links": {
    "self": { "href": "/v0/user/me/bank/accounts/acc_abc123/sync/status" },
    "account": { "href": "/v0/user/me/bank/accounts/acc_abc123" },
    "trigger-sync": { "href": "/v0/user/me/bank/accounts/acc_abc123/sync", "method": "POST" }
  }
}
FieldTypeDescription
last_synced_atinteger or nullLast successful sync timestamp (epoch ms). Null if never synced
cursorstring or nullPlaid sync cursor for incremental fetching
is_stalebooleanWhether the sync data is older than the staleness threshold

Pagination

When a sync returns more data than fits in one page, the system automatically triggers continuation pages. Each page syncs up to 500 transactions. The maximum is 20 pages per sync (10,000 transactions).
If a sync hits the 20-page limit, some older transactions may not be fetched. Trigger another sync to continue where it left off.

Endpoints

POST /v0/user/me/bank/link-token
Creates a Plaid Link token for the bank connection UI. Request body (optional):
{
  "redirect_uri": "https://app.example.com/callback",
  "products": ["transactions", "auth"]
}
FieldTypeDefaultDescription
redirect_uristring or nullnullCustom OAuth redirect URI
productsarray of strings or nullnullOverride default Plaid products
Response: 200 OK
{
  "link_token": "link-sandbox-abc123",
  "expiration": 1700003600,
  "_links": {
    "self": { "href": "/v0/user/me/bank/link-token", "method": "POST" },
    "exchange": { "href": "/v0/user/me/bank/accounts", "method": "POST" }
  }
}

Exchange Public Token

POST /v0/user/me/bank/accounts
Exchanges a Plaid public token from Link completion to create a linked bank account. Request body:
{
  "public_token": "public-sandbox-abc123",
  "account_id": "plaid_account_id",
  "set_as_primary": true
}
FieldTypeRequiredDescription
public_tokenstringYesPublic token from Plaid Link
account_idstringYesSelected account ID from Plaid Link
set_as_primarybooleanNoSet this account as primary (default: false)
Response: 201 Created The auth_fetched field indicates whether ACH account/routing numbers were retrieved automatically. If false, the account needs manual verification via micro-deposits. Behavior:
  • If the account is already linked, returns 409 Conflict
  • If set_as_primary is true, any existing primary account is demoted
  • Auto-fetches ACH auth data when available (instant verification)

Get Bank Account

GET /v0/user/me/bank/accounts/{external_id}
Response: 200 OK — single account with _links.

Update Bank Account

PATCH /v0/user/me/bank/accounts/{external_id}
Request body:
{
  "nickname": "Main Checking",
  "is_primary": true
}
FieldTypeDescription
nicknamestring or nullCustom name (1-100 characters)
is_primaryboolean or nullSet as primary bank account
Response: 200 OK Behavior:
  • Setting is_primary: true demotes the current primary account

Delete Bank Account

DELETE /v0/user/me/bank/accounts/{external_id}
Response: 204 No Content Behavior:
  • Soft-deletes the account and disconnects from Plaid
  • Cannot delete the primary bank account — set another account as primary first (returns 403)

Verify Bank Account

POST /v0/user/me/bank/accounts/{external_id}/verify
Verify account ownership via micro-deposits. Only needed when auth_fetched was false during account creation. Request body:
{
  "amounts": [0.01, 0.03]
}
FieldTypeDescription
amountsarray of numbersExactly two micro-deposit amounts
Response: 200 OK
{
  "verified": true,
  "verification_status": "verified",
  "_links": { "..." : "..." }
}
Behavior:
  • If the account is already verified, returns the current status (no-op)
  • On success, fetches ACH auth data automatically
  • On failure, sets verification_status to failed

Get Account Transactions

GET /v0/user/me/bank/accounts/{external_id}/transactions
Returns synced transactions for a specific bank account.
Query ParameterTypeDefaultDescription
from_dateintegernullStart timestamp (epoch ms)
to_dateintegernullEnd timestamp (epoch ms)
categorystringnullFilter by spending category
directionstringnullFilter by in or out
limitinteger50Results per page (1-500)
offsetinteger0Pagination offset
Response: 200 OK
{
  "transactions": [
    {
      "external_id": "txn_def456",
      "type": "bank_payment",
      "status": "completed",
      "direction": "out",
      "source": "bank_account",
      "amount": "42.50",
      "category": "restaurants",
      "merchant_name": "Chipotle",
      "created_at": 1700000000000
    }
  ],
  "total": 156,
  "offset": 0,
  "limit": 50,
  "_links": {
    "self": { "href": "/v0/user/me/bank/accounts/acc_abc123/transactions" },
    "account": { "href": "/v0/user/me/bank/accounts/acc_abc123" }
  }
}

Integration Patterns

Building a Bank Connection Flow

  1. Call POST /v0/user/me/bank/link-token to get a link_token
  2. Open Plaid Link in your frontend with the token
  3. When the user selects an account, Plaid returns public_token and account_id
  4. Call POST /v0/user/me/bank/accounts with the exchange payload
  5. If auth_fetched is false, show a micro-deposit verification prompt
  6. Call POST /v0/user/me/bank/accounts/{id}/sync to trigger the initial transaction sync
  7. Poll GET /v0/user/me/bank/accounts/{id}/sync/status until last_synced_at updates
Pseudocode:
link = POST /v0/user/me/bank/link-token
open_plaid_link(link.link_token)
on_plaid_success(public_token, account_id):
    result = POST /v0/user/me/bank/accounts {
        public_token, account_id, set_as_primary: true
    }

    if not result.auth_fetched:
        show_micro_deposit_prompt()

    POST /v0/user/me/bank/accounts/{result.account.external_id}/sync
    poll_sync_status(result.account.external_id)

Keeping Data Fresh

Plaid sends webhooks when new transactions are available. Your platform receives these automatically — no polling needed for ongoing sync. For manual refresh, check the sync status and trigger a sync when stale:
status = GET /v0/user/me/bank/accounts/{id}/sync/status

if status.is_stale:
    POST /v0/user/me/bank/accounts/{id}/sync

Handling Re-authentication

When a bank credential expires, the account status changes to refresh_required. Your application should:
  1. Detect accounts with status: "refresh_required"
  2. Create a new Link token for re-authentication
  3. Open Plaid Link in update mode
  4. The account status returns to active after re-authentication

Error Handling

All errors follow RFC 7807 Problem Details format:
{
  "type": "https://api.sumvin.com/errors/bnk-404-001",
  "title": "Bank Account Not Found",
  "status": 404,
  "detail": "Bank account acc_xyz789 not found",
  "instance": "/v0/user/me/bank/accounts/acc_xyz789",
  "error_code": "BNK-404-001"
}
StatusError CodeMeaning
400 Bad RequestBNK-400-001Token exchange failed (invalid public token)
400 Bad RequestBNK-400-002Micro-deposit verification failed
400 Bad RequestBNK-400-005Invalid verification amounts (must be exactly two)
401 UnauthorizedMissing or invalid authentication token
403 ForbiddenBNK-403-001Account does not belong to authenticated user
403 ForbiddenBNK-403-002Cannot delete primary bank account
404 Not FoundBNK-404-001Bank account not found
409 ConflictBNK-409-001Bank account already linked
500 Internal Server ErrorBNK-500-001Link token creation failed
502 Bad GatewayBNK-502-001Banking provider error

Reference Tables

Account Types

ValueDescription
checkingChecking/current account (supports ACH transfers)
savingsSavings account (may have transfer limits)
credit_cardCredit card account (read-only transaction data)
loanLoan or mortgage account (read-only balance)
investmentBrokerage or retirement account (read-only)

Account Statuses

ValueDescription
pending_connectionInitial link in progress
activeConnected and syncing transactions
refresh_requiredUser must re-authenticate (credential expired)
disconnectedUser disconnected the account
errorProvider error (contact support)

Verification Statuses

ValueDescription
pending_automaticAwaiting instant verification via open banking
pending_manualAwaiting micro-deposit verification (2-3 business days)
verifiedAccount ownership confirmed, ready for transfers
failedVerification failed (user must re-link account)

Transaction Types (Bank)

ValueDescription
bank_transferACH/wire transfer between accounts
direct_debitRecurring automated payment (bills, subscriptions)
standing_orderScheduled fixed payment
bank_paymentOne-time outgoing payment
bank_refundReturn of funds to bank account
interestInterest credit on savings/checking account
feeBank service charge or maintenance fee

Transaction Statuses

ValueDescription
pendingTransaction not yet settled
completedTransaction settled

Sync Types

ValueDescription
balanceFetch current balances only
transactionsIncremental transaction sync (includes balances)
fullFull re-sync of transactions and balances

Next Steps