Bank Accounts and Connections
Connect Plaid, map feeds to GL accounts, and recover configuration
Last Updated: 2026-09-12
Version: 1.7
A bank Account is the GL posting target. An AccountConnection is an optional external identity discovered through Plaid or another provider. Connecting a feed never creates a second accounting account.
Account And Feed Model
account.banking.connections[] contains the linked AccountConnections. The
outer item id is the accountConnectionId; its nested connection.id is the
provider Connection ID.
Bank Accounts distinguish trust, operating, and external categories plus
deposit and credit-card behavior. Respect the category, assignments,
categoryLock, capabilities, and issues[] returned by the API.
Bank Fees and Interest Income are ordinary ledger accounts. Add them to the published global or partner ledger template when they should be present in a new team's chart. Use a BankRule when one team wants a repeatable interest mapping. The rule must explicitly select the intended account and party; VRPlatform has no global beneficiary default because the legal recipient depends on the team and jurisdiction.
Bank Data Consent Status
SISS uses the generic POST /connections/connect flow with appId: "siss".
Reconnect an existing feed through POST /connections/{id}/connect.
Forward callback query values unchanged in params, preserving repeated values as arrays.
When present, forward the single nonempty sessionKey query value as the request's
sessionKey; it is separate from SISS's state=success outcome. The API validates
and normalizes provider fields. Complete within 15 minutes as the initiating user
and team; consumed or failed journeys require a fresh connection flow.
Connections whose integrations declare bank-data consent expose an optional
consent snapshot, also nested under account.banking.connections[].connection:
{
"status": "expiring",
"expiresAt": "2026-10-01",
"checkedAt": "2026-09-07T10:00:00.000Z"
}Status is active, expiring, expired, or revoked. Display the expiry date
and offer reconnection when consent needs renewal. checkedAt identifies when
the provider state was checked; absence of the snapshot does not establish
active consent. Credentials and private connection state are never returned.
GET /me exposes team.addressCountry, and GET /team exposes the same
persisted country as addressCountry. This value is independent of billing
and statement addresses. An absent country must not imply Australian eligibility.
The provider connection flow enforces eligibility on the server.
Stored Bank Feed Health
Portfolio Health uses the same assessment for connection alerts and returns its account-scoped reasons and actions.
Connections from supported services expose health on GET /connections and its detail
response. The same object appears at connection.health on AccountConnections and at
banking.connections[].connection.health on Accounts, in both lists and details. These reads
use stored observations and make no provider health request per connection.
| Status | Meaning |
|---|---|
healthy | Recent successful bank-record ingestion with no known supported problem |
warning | A known warning, such as expiring consent or a failed sync |
actionRequired | Confirmed authentication, replacement, or consent recovery is required |
unknown | Required observations or successful ingestion are missing, stale, or future-dated |
The initial confidence limit is 48 hours. Healthy does not guarantee institution availability,
transaction completeness, or reconciliation. Absence of health means the integration has not
enabled this contract; it does not mean healthy. Stored evidence the API cannot read also projects
as unknown; it never fails the read. Existing isErrorState retains its legacy meaning and must
not be used to infer a specific recovery action.
checkedAt is the oldest required observation time and is null for an incomplete assessment.
lastSuccessfulSyncAt advances only after completed bank-record ingestion, including a run with
no changes. Account discovery and partial ingestion do not advance it. A successful sync does
not erase an expiring-consent warning. Reconnection invalidates prior successful assessments;
health can remain unknown or show unresolved conditions until verified integration work runs.
Each issue has a finite code, severity, static message, nullable observedAt, supported
actions, and typed context. Render the listed actions through the existing permitted flows:
| Issue | Action |
|---|---|
authenticationRequired | reconnect |
replacementRequired | replace |
consentExpiring or consentRequired | renewConsent |
syncFailed | retry only when present in actions |
healthUnknown | No inferred action |
A failed sync alone does not require reconnecting. Actions describe recovery semantics; existing permissions and endpoint preconditions still apply. All issues remain visible when one determines the overall status; action-required issues take precedence over warnings and unknown evidence.
SISS serves Australian teams, and one SISS connection can include several bank logins. Consent
issues use context: { scope: "account", accountConnectionId }. Show those against the affected
feed. scope: "unmappedAccount" means the provider account has not been mapped to a public feed;
do not mark every linked account broken. Access and sync issues use scope: "connection".
The connection summary is shared across its account rows, so inspect issue scope when drawing
an account-specific badge. syncFailed describes incomplete ingestion for the attempted run.
It does not establish that every account failed. Pending or unknown SISS sharing states prevent
healthy status and carry no inferred renewal action unless their expiry date establishes one.
Consent expiresAt is a calendar date evaluated in UTC: the expiry day itself already requires
renewal, and an expiry within 30 days warns.
{
"status": "warning",
"checkedAt": "2026-09-12T09:00:00Z",
"lastSuccessfulSyncAt": "2026-09-12T09:00:00Z",
"issues": [{
"code": "consentExpiring",
"severity": "warning",
"message": "Bank data consent expires soon.",
"observedAt": "2026-09-12T09:00:00Z",
"actions": ["renewConsent"],
"context": {
"scope": "account",
"accountConnectionId": "0d94277b-91e2-40d9-a80e-b03a75b05fde"
}
}]
}GET /plaid/health remains available for Plaid details. Stored health adds no outage polling,
overdue-update detection, or requirement to call that endpoint for other providers.
Connect Plaid
Plaid configuration is one three-step session:
| Step | Request | Result |
|---|---|---|
| Start | POST /plaid/connect | connectId and Link token |
| Complete Link | POST /plaid/connect/complete | Complete configuration read model |
| Confirm mapping | POST /plaid/connect/configure | Account decisions and one initial sync |
The connectId owns idempotency, selected environment, recovery, and final
configuration. Do not create a second session while completion is still
retryable.
An embedded product can receive embed:banking:v1. That bundle grants only
the three Plaid connect requests above. It does not grant general Account,
BankRecord, Transaction, report, or team-administration access. The embedded
session remains bound to its team, partner, allowed origin, expiry, and
revocation state. Use /banking as the embedded view path.
Plaid credentials, public and access tokens, Item IDs, Link session IDs, and provider payloads never appear in public audit events. Reviewed connection, account-connection, and account configuration changes can appear in the Activity Log.
Choose The Session Mode
| Mode | Use | Connection ID |
|---|---|---|
create | Add a new Plaid Item | Omit |
update | Repair an existing Item with reusable credentials | Require |
replace | Replace credentials with a new Item | Require |
The optional environment is sandbox or production and defaults to
production. Completion and configuration reuse the session's environment and
do not accept it again.
New and replacement Items request Plaid Transactions only. Plaid Auth is not part of reconciliation and would exclude credit-card accounts. Update mode retains the existing Item's products.
Hostaway-region sessions use the hostaway Plaid customization, United States
country scope, and the Hostaway client name for create, update, and replace.
Render Completion Directly
The completion response is the configuration screen model:
accountscontains eligible existing GL bank accounts;bankAccountCategoriescontains available classifications; andbankAccounts[]contains status, suggestions, capabilities, and typed issues for every discovered feed.
Do not fetch /accounts again or poll AccountConnections for discovery. Submit
one connect or archive decision for every row whose configuration status is
required. Optional opening balances use { date, centAmount }.
The final configuration executes all decisions sequentially in one transaction. A conflict rolls back every mapping. After commit, the API creates one durable initial sync.
Identity And Replacement Safety
A four-digit banking suffix is unique among a team's GL bank accounts. A feed and its linked Account may share the suffix because they represent the same bank account.
Read capabilities.useLastDigitsForBanking on AccountConnections, including
those nested under an Account. When false, display lastDigits but do not use
it for GL suggestions or copy it into banking.last4. The integration catalog
declares this policy, so providers with display-only masks can disable banking
identity matching without API changes. Plaid sandbox masks retain the same
restriction because distinct feeds may share the same digits. Provider references still identify
feeds and records.
Explicitly attach each feed to its intended GL Account; any manually configured
GL suffix remains unchanged and must satisfy the team's uniqueness rule.
When replacement identity matches several historical feeds, the API does not guess. It returns the feed as unlinked and keeps eligible GL Accounts available for an explicit decision. A confirmed replacement can archive inactive predecessors but cannot displace an active feed. Historical BankRecords stay on the same GL Account.
Plaid credit feeds default to credit-card behavior. Relay Financial names that
end in Credit Account receive the same classification when Plaid reports a
checking or depository type.
Import Bank Activity
Plaid and provider feeds retain their source and AccountConnection identity.
CSV imports use the canonical accountId; their accountConnectionId is
null. See Import Data from CSV.
Records earlier than a feed's configured start date are stored as inactive history. This is an expected exclusion, not a failed sync or reconnect signal.
When a feed resumes after a CSV stopgap import, the API warns if a new record has the same account, date, and exact amount as an active record from the other source. Those fields are not a unique identity, so neither record is merged or skipped. Let a manager review the warning.
Account And Feed Decisions
| Decision | Behavior |
|---|---|
| Immutable account category | Disable the selector |
| Initialize banking | Write account state, opening Transaction, and feed checkpoint atomically |
| Credit-card opening balance | Negative feed checkpoint and positive opening Transaction line |
| Delete a protected Account | Respect the lock or use onLocked=archive when offered |
| Attach over an active feed | onConflict=error, replace, or replace-and-detach-history |
| Detach a feed | Preserve history by default; detach unreconciled history only when requested |
| Unselected Plaid feed | Submit action=archive |
An Account referenced by a BankRule selector or posting line cannot be hard
deleted; use onLocked=archive to preserve the rule. An active recurring fee
that uses an Account as a direct posting account, tax posting account, or
formula input blocks deletion and archive. Repoint or deactivate the fee first.
Banking initialization can create its opening-balance Transaction before the
team has selected statementStartAt. Ordinary Transactions remain unavailable
until accounting start is configured.
ACH Readiness
Account and AccountConnection reads expose
capabilities.achPayments = { enabled, disabledReasons }. Use only an
AccountConnection whose capability is enabled; a Ramp Connection does not make
every embedded account eligible.
Ramp funding requires an active deposit AccountConnection with its provider
account ID in uniqueRef. The replacement-stable persistentRef is not a
funding requirement. Payment execution also checks that the linked source has
one Ramp entity and bank-account ID and that the provider account matches the
AccountConnection.
Team readiness telemetry reports ready: true when at least one
AccountConnection has capabilities.achPayments.enabled: true. It changes to
false when the team loses its last eligible funding account. Flow settings,
payment permissions, recipient setup, and individual payment validation are
separate checks. A team readiness observation does not guarantee a payment
will pass preflight.
Contact reads expose only masked ACH state: provider, connection, last four,
status, failure reason, and hasInFlightPayment. Full routing and account
numbers are write-only through POST /contacts/{id}/payment-methods/ach and
are never stored by VRPlatform.
Do not send a client-selected connectionId for ACH setup. The API resolves
the single eligible Ramp flow. The first setup activates an unconfigured flow;
an explicitly inactive, missing, or ambiguous setup remains blocked.
Ramp vendor creation requires contact email and country, plus state for a
United States vendor. Phone and address are optional. When the contact has no
location, the integration uses the connected Ramp business billing address.
The pushRampVendors flow must select an active Ramp user as Vendor owner.
ACH setup returns HTTP 422 before it queues verification when that setting is
missing or no longer resolves to an active user on the same Ramp connection.
The error code is RAMP_VENDOR_OWNER_REQUIRED; its context returns the flow,
connection, and vendorOwner setting key so clients can open the existing
connection setting.
An existing ownerless vendor cannot inherit a different vendor's owner or use
the connection default retroactively. Contact ACH state returns
status=failed with instructions in failureReason; send the user to that
vendor in Ramp to assign an owner before ACH becomes ready.
Statement payout preview treats an unconfigured transfersToBills flow as
eligible without activating it. The first otherwise-valid payout activates the
flow. A dry run rolls that activation back, and an explicitly inactive flow
remains unavailable.
An in-flight payout stays on the bank account selected when it was scheduled. Keep bank editing available, but explain that the change affects future payments only.
Plaid Recovery
| Error | Client action |
|---|---|
PLAID_PROVIDER_UNAVAILABLE | Retry the same action and connectId |
PLAID_CONNECT_IN_PROGRESS | Retry the same completion; do not start another session |
CONNECTION_RECONNECT_REQUIRED | Run the existing Item's reconnect flow |
PLAID_CONNECT_REQUIRES_NEW_LINK | Follow context.restartMode and its Connection ID exactly |
PLAID_CONNECT_CONFIGURATION_CHANGED | Replace the screen with context.result and reconfirm |
Plaid Link error INVALID_UPDATED_USERNAME means update mode cannot repair the
Item. Start a new replace session for the same Connection.
Do not infer create or replace mode from local state. Use the server-owned recovery directive. Display the stable top-level error and use allowlisted provider fields only for diagnostics.
API Reference
- Initialize banking:
POST /accounts/{id}/banking-init - Attach a feed:
POST /accounts/{id}/connections - Detach a feed:
DELETE /accounts/{id}/connections/{accountConnectionId} - Start Plaid Link:
POST /plaid/connect - Complete Plaid Link:
POST /plaid/connect/complete - Configure Plaid accounts:
POST /plaid/connect/configure
