Skip to main content
All Docs
FeaturesMaking Tax DigitalUpdated April 1, 2026

Connecting a Bank Account

Connecting a Bank Account

The platform uses TrueLayer Open Banking to securely connect your bank accounts and import transactions. The OAuth flow is handled entirely server-side via two dedicated API routes.

How the connection flow works

1. Initiate — GET /api/bank/authorise

When you click Connect Bank Account, the app redirects you to /api/bank/authorise. This route:

  1. Verifies you are authenticated and resolves your active organisation.
  2. Generates a random CSRF nonce and encodes it with your userId and orgId into a base64url state string.
  3. Stores the state in a secure HttpOnly, SameSite=Lax cookie with a 10-minute expiry.
  4. Redirects your browser to TrueLayer's consent screen.

Rate limit: You can initiate a connection a maximum of 10 times per minute. Exceeding this redirects to /dashboard/bank?error=rate_limited.

Error redirects from /api/bank/authorise

ConditionRedirect target
Not authenticated/sign-in
Rate limit exceeded/dashboard/bank?error=rate_limited
No organisation membership found/dashboard/bank?error=missing_org
TrueLayer environment variables not configured/dashboard/bank?error=config_error

2. Callback — GET /api/bank/callback

After you grant (or deny) consent on TrueLayer's screen, TrueLayer redirects your browser back to /api/bank/callback. This route:

  1. CSRF validation — Compares the state query parameter against the value in the tl_oauth_state cookie. Mismatches are rejected immediately.
  2. State decoding — Extracts userId and orgId from the state payload. No active session is required at this point.
  3. Token exchange — Exchanges the one-time auth code for TrueLayer access and refresh tokens.
  4. Account fetch — Lists the connected bank accounts to retrieve provider metadata (providerName, providerLogoUrl).
  5. Record upsert — Creates or updates the bank_connections record with encrypted tokens and status: "connected".
  6. Account upsert — Creates or updates bank_accounts records. Sensitive fields (sort code, account number, IBAN) are encrypted at rest.
  7. Audit log — Records a bank.connected audit event.
  8. Background sync — Fires the bank/sync.requested Inngest event so transactions are fetched immediately in the background.
  9. Redirect — Clears the CSRF cookie and sends you to /dashboard/bank?connected=true.

Error redirects from /api/bank/callback

ConditionRedirect target
User denied consent/dashboard/bank?error=access_denied
code parameter missing/dashboard/bank?error=missing_code
CSRF cookie/state mismatch/dashboard/bank?error=invalid_state
State payload malformed/dashboard/bank?error=malformed_state
Token exchange or other error/dashboard/bank?error=connection_failed

Partial account failures during callback are non-fatal. If listing accounts fails, the connection record is still saved and accounts will be fetched during the first background sync.


Security

MechanismDetail
CSRF protectionstate cookie is HttpOnly, SameSite=Lax; validated on every callback
Encrypted credentialsTrueLayer tokens stored encrypted in bank_connections; sort code, account number, and IBAN encrypted in bank_accounts
Rate limitingOAuth initiation capped at 10 requests/minute per user
Cookie expiryCSRF state cookie expires after 10 minutes

Sync error reporting

After a bank account is connected, background syncs are run by the bank/sync.requested Inngest function. As of v1.0.479, sync errors are accurately reflected on the connection status:

  • All accounts failed to sync → connection status is set to "error"
  • Some accounts failed → connection status remains "connected"; the partial failure detail is stored in lastSyncError
  • Individual account errors are reported to the error tracking service via captureError()

Previously, the connection was always marked "connected" after a sync run regardless of whether any transactions were successfully fetched.


Required environment variables

VariableDescription
TRUELAYER_CLIENT_IDTrueLayer application client ID
TRUELAYER_CLIENT_SECRETTrueLayer application client secret
TRUELAYER_REDIRECT_URIMust be set to <your-app-url>/api/bank/callback

If any of these are missing, the authorise route redirects to /dashboard/bank?error=config_error rather than throwing a 500.