Banking
Build accounts, bank records, reconciliation, and bank-rule workflows
Mental Model
Accounts define where amounts post. An Account with type=bank is the bank
account used by transactions, journals, statements, and balances.
An AccountConnection is an optional external account identity discovered inside a Connection. It links imported activity to a bank Account but is not a second bank account. Bank records represent imported financial activity; reconciliation links them to transactions, and bank rules turn matching records into proposals or automatic actions.
Resources and Lifecycle
Accounts and Assignments
Account types are ledger, bank, recurring fee, and non-posting. Categories, system assignments, active status, and banking configuration add behavior.
Bank Accounts distinguish trust, operating, and external categories plus
deposit and credit-card behavior. account.banking.connections[] contains
AccountConnections; each item id is an accountConnectionId, while its
nested connection.id is the Connection ID.
Account and AccountConnection reads expose capabilities.achPayments as
{ enabled, disabledReasons }. AccountConnection reasons distinguish an
unsupported provider, inactive connection or account identity, unsupported
account type, and missing provider reference. Account reads aggregate their
embedded AccountConnections and enable ACH when any identity is ready.
For Ramp, readiness requires an active deposit AccountConnection with its
provider account ID in uniqueRef. persistentRef remains the
replacement-stable Plaid identity and is not a Ramp ACH requirement.
Bank records can be transaction or balance records, active or inactive,
reconciled, or matched by a rule. A CSV-imported bank record can reference only
the canonical accountId; its accountConnectionId remains null.
Plaid Connect Sessions
Plaid setup is a three-operation session workflow. Start Link with
POST /plaid/connect, complete Link and receive discovered bank accounts with
POST /plaid/connect/complete, then submit every account decision together
with POST /plaid/connect/configure.
Session start accepts optional environment=sandbox|production and defaults to
production. The selected environment is persisted on connectId; completion
and configuration do not accept it again. Update and replacement sessions with
reusable credentials retain the existing connection's environment.
New create and replace sessions request Plaid Transactions only. Plaid Auth
is not part of reconciliation and would exclude credit-card accounts from
Link. update sessions omit product selection and retain the existing Item's
products.
The returned connectId owns idempotency and recovery. Completion fetches
account metadata synchronously, but never waits for transaction history. Its
response is the complete configuration read model: accounts contains
eligible existing GL bank accounts, bankAccountCategories reflects team
capabilities, and each bankAccounts row contains configuration status,
suggestions, capabilities, and typed issues. The final configuration creates
one durable initial sync after all selected GL account mappings and banking
initialization have committed.
Production sessions reserve last four digits across active and inactive GL
bank accounts. Plaid sandbox institutions reuse fixed test masks, so sandbox
sessions do not use last-four matches for suggestions, duplicate-account
issues, or canConnect and canCreateAccount capabilities. Provider account
identity duplicate checks still apply. Sandbox masks remain visible on the
AccountConnection but are not copied onto a created or linked GL bank account;
an existing GL last-four value is preserved, and the intentional difference
does not produce an account-connection drift warning.
Plaid credit feeds default to credit-card behavior. Relay Financial accounts
whose names end in Credit Account receive the same classification even when
Plaid reports them as checking/depository accounts.
Bank Records and Matching
Imported records retain source and account identity. Reconciliation and rule matching are relationships with their own state and recovery actions.
Batch ingest warns about possible cross-source duplicates between CSV stopgap
imports and bank feeds. When a new record's account, date, and exact amount
match an active record from the other source type (and no identity match by
sourceId/uniqueRef exists), the batch response reports a static issue. Its
context identifies the candidate record and source type. Because those values
are not a unique transaction identity, neither record is merged or skipped.
Warning candidates are paired one-to-one per batch. Balance records and
opening balances are exempt.
Read Model
Render returned account category and assignments, banking capability issues, bank-record source and reconciliation state, delete locks, matching candidates, and rule preview results. Historical postings retain inactive account identity. Matched sorting evaluates the full filtered set before pagination, so page boundaries preserve the same matched-first or unmatched-first ordering.
Use GET /account-connections?connectionAppId=ramp or filter by
connectionId to discover provider-backed identities that can fund ACH
requests. Each row's capabilities.achPayments reports readiness and typed
disabled reasons. Use only rows where that capability is enabled; a Ramp
Connection by itself does not make every embedded account ACH-capable. No
provider-specific funding-account endpoint is required. Payment execution also
validates that the AccountConnection source contains one Ramp entityId and
bankAccountId, and that the provider bank ID matches the AccountConnection
uniqueRef. Those IDs become the bill's entity_id and
source_bank_account_id.
Owner contact reads expose only masked ACH metadata: provider, connection,
last four digits, and notConfigured | pending | ready | failed. Submit full
legal name, routing number, and account number only to
POST /contacts/{id}/payment-methods/ach. Raw bank details travel to the
targeted Ramp flow and are never stored in VRPlatform. Do not submit a
connectionId: the server selects the single active Ramp Connection whose
pushRampVendors flow listens for contact updates. The first ACH setup
activates that flow when its status has never been configured; an explicitly
inactive flow remains unavailable. Missing setup returns 422; multiple eligible
setups return 409. A successful submission is pending until provider
confirmation; owners may update only their own contact.
Statement payout preview also treats a missing transfersToBills status as an
eligible, unconfigured Ramp flow without mutating it. The first otherwise-valid
POST /statements/pay Ramp payout activates that flow before dispatch. Dry runs
roll back the activation, and an explicitly inactive flow remains unavailable.
Decision Table
Accounts and Feeds
| Decision | Rule |
|---|---|
| Immutable category | Disable category selector |
| Credit-card beginning balance | Negative feed checkpoint; liability-credit journal |
| Account deletion | onLocked=error (default) or archive; active fee references block both |
| Attach conflict | onConflict=error (default), replace, or replace-and-detach-history |
| Detach | mode=preserve-history (default) or detach-history |
Plaid create | Start a new Item; omit connectionId; environment defaults to production |
Plaid update | Repair an Item; require connectionId and reuse credentials |
Plaid replace | Replace credentials; require connectionId and create a new Item |
| Plaid feed not selected | Submit action=archive; the feed becomes inactive |
When initializing banking
(POST /accounts/{id}/banking-init),
a credit-card beginning balance has two representations. The bank feed stores
the checkpoint as a negative balance so later charges increase the displayed
liability. The opening-balance transaction keeps a positive line total so the
journal credits the credit-card account and debits the opening-balance equity
account. Deposit accounts retain the submitted sign for both representations.
Initialization writes the account state, opening transaction, and feed
checkpoint atomically and returns the complete transaction. Concurrent account
deletion is serialized with initialization: if deletion wins first,
initialization returns the standard not-found response; otherwise the complete
initialization response is retained even if the account is deleted immediately
afterward.
Accounts return categoryLock with status: locked and reason: immutable;
respect it and disable the category selector instead of submitting a
classification change. A
deletable account removes its allowed
bank-record history atomically. Protected journal, reconciliation, and other
dependencies retain the existing delete lock; use onLocked=archive only
when the user chooses to preserve the account as inactive. An account used as
a direct or tax posting account by an active recurring fee cannot be deleted
or archived. Repoint or deactivate the fee first; historical postings remain
available after the account is archived. An
attach conflict occurs when
the target bank Account already has an active AccountConnection. replace
deactivates the old connection, while replace-and-detach-history also
detaches its unreconciled bank-record history. On
detach,
preserve-history keeps imported records attached while detach-history
detaches the unreconciled ones.
Bank Rules
| Decision | Rule |
|---|---|
| Internal transfer account | Internal bank account; never external |
| Rule condition | Description or amount comparison |
| Multiple conditions | `matchMode=all |
| Allocation | Flat, percentage, or one remainder line |
| Automatic mode | Create and match only after valid preview/configuration |
Flat and percentage rule lines cannot be mixed, and a percentage rule must satisfy the full percentage allocation without a remainder line. Every line needs an account or supported assignment.
Editability
Bank-record lock.delete.reasons[] can include notCsvSource, plaidSource,
reconciled, balanceRecord, or bankRuleMatched. Disable deletion and show
the corresponding recovery instead of trying the request repeatedly.
Reconciliation constrains transaction payment edits independently from line locks. Account assignment and category changes can affect future journal construction and party rules.
Preview and Preflight
Use Preview a bank rule
(POST /bank-rules/preview) to show
matching records and allocations. Run a saved bank rule
(POST /bank-rules/{id}/run)
supports dry run. Rule configuration has schema validation but no generic
mutation dry run.
Database-backed AccountConnection and bank-record mutations expose dry run where declared. Plaid and other external-system actions have side effects and do not.
Mutation Recipe
- Load account, category, assignment, capability, and current issue data.
- Validate bank classification and internal/external eligibility.
- Load bank-record locks and reconciliation candidates.
- Validate rule conditions and allocation shape locally.
- Preview the rule or matching proposal.
- Dry-run the final supported database mutation or saved rule run.
- Confirm and apply.
- Re-read account, record, transaction payment, and sync state.
Failure and Recovery
Use account issues[] for setup repair, such as a last-four mismatch or an
operating bank account on a team where the Operations Accounting feature is not
enabled (see the issue catalog). For delete locks, direct
the user to unreconcile,
preserve imported history, or remove the matching relationship as the reason
requires. External synchronization failures belong in connection health, not
a local database rollback flow.
Plaid completion and provider calls have finite deadlines. A retryable
PLAID_PROVIDER_UNAVAILABLE or PLAID_CONNECT_IN_PROGRESS response keeps the
same connectId; retry the same completion action instead of creating another
Link session or polling bank accounts. If completion returns
PLAID_CONNECT_REQUIRES_NEW_LINK, follow its strict context directive:
{ "restartMode": "create" }starts a new create session without aconnectionId.{ "restartMode": "replace", "connectionId": "..." }starts replace mode for exactly that tenant-scoped connection.
Never infer the restart mode from local UI state or reuse the failed
connectId.
If final configuration returns PLAID_CONNECT_CONFIGURATION_CHANGED, replace
the screen model with context.result and ask the user to confirm the refreshed
choices. The session is immediately ready for another configuration request;
do not reopen Link or wait for the old lease. This 409 is a discriminated
OpenAPI response: context.result has the same schema as successful Plaid
completion/configuration, while optional accountIds and bankAccountIds
identify the changed Account and AccountConnection inputs.
Plaid Link renders embedded provider errors before completion. In particular,
INVALID_UPDATED_USERNAME means update mode cannot repair the Item; start a
new replace session for the same connection.
Common Recipes
Match an imported expense
Load candidates, select the transaction, verify record totals, dry-run the supported match/update, apply, and re-read both the record and payment lock.
PUT /transactions/{transactionId}?dryRun=true
Content-Type: application/json
{
"payment": {
"status": "paid",
"date": "2026-07-18",
"bankRecordIds": ["55555555-5555-4555-8555-555555555555"]
}
}Create a percentage bank rule
Choose conditions and source accounts, use only percentage allocation lines,
validate the full allocation, preview matches, then create the rule
(POST /bank-rules) and test it.
Send this payload to /bank-rules/preview first, then to /bank-rules after
the proposed matches and allocations are confirmed:
POST /bank-rules/preview
Content-Type: application/json
{
"name": "Split cleaning charges",
"transactionType": "expense",
"mode": "suggest",
"matchMode": "all",
"conditions": [
{
"type": "description",
"operator": "contains",
"value": "cleaning"
}
],
"vendorContactId": "33333333-3333-4333-8333-333333333333",
"accountIds": ["22222222-2222-4222-8222-222222222222"],
"lines": [
{
"type": "percentage",
"amount": 60000,
"accountId": "44444444-4444-4444-8444-444444444444"
},
{
"type": "percentage",
"amount": 40000,
"accountId": "66666666-6666-4666-8666-666666666666"
}
]
}Connect Plaid
- Start
create,update, orreplacemode, optionally select the environment, and open the returned Link token. - Complete the same
connectId; renderaccounts,bankAccountCategories, andbankAccountsfrom that response without another setup read. - Use each row's
configuration.status, capabilities, suggestions, andissues; do not reconstruct eligibility or duplicate detection in the UI. - Preserve accounts already mapped by an unambiguous update or replacement.
- Submit one
connectorarchivedecision for every row whose configuration status isrequired. - Send optional opening balances as
{ date, centAmount }. - Observe the returned sync through the normal sync APIs.
Do not poll GET /account-connections for discovery and do not call the individual
feed mapping endpoint in a loop. Do not fetch /accounts to populate the
session configuration screen. For entry screens outside a connect session,
GET /account-connections?connectionAppId=plaid&status=active&linked=false
returns the current unlinked Plaid feeds; configuration still re-reads and
validates the completion model.
API Reference
- Initialize banking —
POST /accounts/{id}/banking-init - Attach an AccountConnection —
POST /accounts/{id}/connections - Detach an AccountConnection —
DELETE /accounts/{id}/connections/{accountConnectionId} - Create a bank rule —
POST /bank-rules - Start Plaid Link —
POST /plaid/connect - Complete Plaid Link —
POST /plaid/connect/complete POST /plaid/connect/configure— Configure Plaid accounts
