VRPlatformVRPlatform
Build a Product UI

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.

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.

StatusMeaning
healthyRecent successful bank-record ingestion with no known supported problem
warningA known warning, such as expiring consent or a failed sync
actionRequiredConfirmed authentication, replacement, or consent recovery is required
unknownRequired 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:

IssueAction
authenticationRequiredreconnect
replacementRequiredreplace
consentExpiring or consentRequiredrenewConsent
syncFailedretry only when present in actions
healthUnknownNo 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:

StepRequestResult
StartPOST /plaid/connectconnectId and Link token
Complete LinkPOST /plaid/connect/completeComplete configuration read model
Confirm mappingPOST /plaid/connect/configureAccount 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

ModeUseConnection ID
createAdd a new Plaid ItemOmit
updateRepair an existing Item with reusable credentialsRequire
replaceReplace credentials with a new ItemRequire

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:

  • accounts contains eligible existing GL bank accounts;
  • bankAccountCategories contains available classifications; and
  • bankAccounts[] 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

DecisionBehavior
Immutable account categoryDisable the selector
Initialize bankingWrite account state, opening Transaction, and feed checkpoint atomically
Credit-card opening balanceNegative feed checkpoint and positive opening Transaction line
Delete a protected AccountRespect the lock or use onLocked=archive when offered
Attach over an active feedonConflict=error, replace, or replace-and-detach-history
Detach a feedPreserve history by default; detach unreconciled history only when requested
Unselected Plaid feedSubmit 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

ErrorClient action
PLAID_PROVIDER_UNAVAILABLERetry the same action and connectId
PLAID_CONNECT_IN_PROGRESSRetry the same completion; do not start another session
CONNECTION_RECONNECT_REQUIREDRun the existing Item's reconnect flow
PLAID_CONNECT_REQUIRES_NEW_LINKFollow context.restartMode and its Connection ID exactly
PLAID_CONNECT_CONFIGURATION_CHANGEDReplace 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

On this page