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
1. Create a Link token
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 '{}'
2. Complete Plaid Link
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
| Field | Type | Description |
|---|
external_id | string | Stable account identifier for API calls |
provider | string | Banking provider (plaid) |
institution_name | string | Bank name (e.g. “Chase”, “Wells Fargo”) |
institution_logo_url | string or null | URL to the institution’s logo |
account_name | string | Account name from the bank (e.g. “Checking”) |
account_mask | string or null | Last 4 digits of the account number |
account_type | string | One of: checking, savings, credit_card, loan, investment |
is_primary | boolean | Whether this is the user’s primary bank account |
status | string | Connection status (see Account Statuses) |
verification_status | string or null | Ownership verification state (see Verification Statuses) |
nickname | string or null | User-set custom name |
balance_current | decimal or null | Current balance |
balance_available | decimal or null | Available balance (after pending transactions) |
balance_currency | string | Currency code (e.g. USD) |
balance_updated_at | integer or null | When balances were last refreshed (epoch ms) |
created_at | integer | When 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:
- On demand —
POST /v0/user/me/bank/accounts/{id}/sync
- Automatically — Plaid sends a webhook when new transactions are available
Triggering a Sync
POST /v0/user/me/bank/accounts/{external_id}/sync
| Query Parameter | Type | Default | Description |
|---|
sync_type | string | transactions | One of: balance, transactions, full |
| Sync Type | Description |
|---|
balance | Fetch current balances only |
transactions | Fetch new/modified/removed transactions (includes balances) |
full | Full 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" }
}
}
| Field | Type | Description |
|---|
last_synced_at | integer or null | Last successful sync timestamp (epoch ms). Null if never synced |
cursor | string or null | Plaid sync cursor for incremental fetching |
is_stale | boolean | Whether the sync data is older than the staleness threshold |
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
Create Link Token
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"]
}
| Field | Type | Default | Description |
|---|
redirect_uri | string or null | null | Custom OAuth redirect URI |
products | array of strings or null | null | Override 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
}
| Field | Type | Required | Description |
|---|
public_token | string | Yes | Public token from Plaid Link |
account_id | string | Yes | Selected account ID from Plaid Link |
set_as_primary | boolean | No | Set 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
}
| Field | Type | Description |
|---|
nickname | string or null | Custom name (1-100 characters) |
is_primary | boolean or null | Set 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]
}
| Field | Type | Description |
|---|
amounts | array of numbers | Exactly 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 Parameter | Type | Default | Description |
|---|
from_date | integer | null | Start timestamp (epoch ms) |
to_date | integer | null | End timestamp (epoch ms) |
category | string | null | Filter by spending category |
direction | string | null | Filter by in or out |
limit | integer | 50 | Results per page (1-500) |
offset | integer | 0 | Pagination 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
- Call
POST /v0/user/me/bank/link-token to get a link_token
- Open Plaid Link in your frontend with the token
- When the user selects an account, Plaid returns
public_token and account_id
- Call
POST /v0/user/me/bank/accounts with the exchange payload
- If
auth_fetched is false, show a micro-deposit verification prompt
- Call
POST /v0/user/me/bank/accounts/{id}/sync to trigger the initial transaction sync
- 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:
- Detect accounts with
status: "refresh_required"
- Create a new Link token for re-authentication
- Open Plaid Link in update mode
- 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"
}
| Status | Error Code | Meaning |
|---|
| 400 Bad Request | BNK-400-001 | Token exchange failed (invalid public token) |
| 400 Bad Request | BNK-400-002 | Micro-deposit verification failed |
| 400 Bad Request | BNK-400-005 | Invalid verification amounts (must be exactly two) |
| 401 Unauthorized | — | Missing or invalid authentication token |
| 403 Forbidden | BNK-403-001 | Account does not belong to authenticated user |
| 403 Forbidden | BNK-403-002 | Cannot delete primary bank account |
| 404 Not Found | BNK-404-001 | Bank account not found |
| 409 Conflict | BNK-409-001 | Bank account already linked |
| 500 Internal Server Error | BNK-500-001 | Link token creation failed |
| 502 Bad Gateway | BNK-502-001 | Banking provider error |
Reference Tables
Account Types
| Value | Description |
|---|
checking | Checking/current account (supports ACH transfers) |
savings | Savings account (may have transfer limits) |
credit_card | Credit card account (read-only transaction data) |
loan | Loan or mortgage account (read-only balance) |
investment | Brokerage or retirement account (read-only) |
Account Statuses
| Value | Description |
|---|
pending_connection | Initial link in progress |
active | Connected and syncing transactions |
refresh_required | User must re-authenticate (credential expired) |
disconnected | User disconnected the account |
error | Provider error (contact support) |
Verification Statuses
| Value | Description |
|---|
pending_automatic | Awaiting instant verification via open banking |
pending_manual | Awaiting micro-deposit verification (2-3 business days) |
verified | Account ownership confirmed, ready for transfers |
failed | Verification failed (user must re-link account) |
Transaction Types (Bank)
| Value | Description |
|---|
bank_transfer | ACH/wire transfer between accounts |
direct_debit | Recurring automated payment (bills, subscriptions) |
standing_order | Scheduled fixed payment |
bank_payment | One-time outgoing payment |
bank_refund | Return of funds to bank account |
interest | Interest credit on savings/checking account |
fee | Bank service charge or maintenance fee |
Transaction Statuses
| Value | Description |
|---|
pending | Transaction not yet settled |
completed | Transaction settled |
Sync Types
| Value | Description |
|---|
balance | Fetch current balances only |
transactions | Incremental transaction sync (includes balances) |
full | Full re-sync of transactions and balances |
Next Steps