Skip to content

Plaid Integration

Connect bank accounts through Plaid for automatic transaction syncing. Link, unlink, reauthorize and delete calls need admin permission on the business.

Where this fits in the API. The easiest way to connect a bank is the Integrations page in the DayZero dashboard, which drives the flow below for you. The /api/v1/plaid/* endpoints are callable with an API token but are not part of the public API Reference — they exist to support DayZero's own UI and may change. Once a bank is connected, its data appears through the public Transactions and Ledgers endpoints, which is where integrations should read from.

Connect a Bank Account

There are two flows. Hosted Link needs no frontend code: DayZero returns a Plaid-hosted URL (and emails it to the recipient) where the account holder logs in to their bank.

bash
curl -X POST "https://api.ondayzero.com/api/v1/plaid/hosted-link" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{ "email": "owner@acme.com", "completion_redirect_uri": "https://app.example.com/connected" }'

Plaid's webhook completes the connection; poll GET /api/v1/plaid/pending-connection-requests and GET /api/v1/plaid/connections to see it arrive. The response has hosted_link_url, link_token, email_sent and recipient_email. The URL is valid for 4 hours by default (url_lifetime_seconds); other optional fields include language, products (default ["transactions"]), country_codes (default ["US"]) and user_id. Pending requests that are never completed expire automatically, and POST /api/v1/plaid/connection-requests/{connection_request_id}/cancel cancels one that is still pending, being finalized, or recently failed.

Embedded Link is for your own UI using react-plaid-link (or another Plaid Link SDK):

  1. Mint a Link token — the response has a link_token for the SDK and a connection_request_id to echo back:

    bash
    curl -X POST "https://api.ondayzero.com/api/v1/plaid/link-token" \
      -H "Authorization: Bearer dz_your_token_here" \
      -H "x-business-id: YOUR_BUSINESS_ID" \
      -H "Content-Type: application/json" \
      -d '{}'
  2. Open Plaid Link with the link_token.

  3. In the SDK's onSuccess callback, exchange the public token:

    bash
    curl -X POST "https://api.ondayzero.com/api/v1/plaid/exchange-public-token" \
      -H "Authorization: Bearer dz_your_token_here" \
      -H "x-business-id: YOUR_BUSINESS_ID" \
      -H "Content-Type: application/json" \
      -d '{
        "public_token": "public-sandbox-...",
        "connection_request_id": "CONNECTION_REQUEST_UUID",
        "institution_id": "ins_3",
        "institution_name": "Chase"
      }'

Choose Which Accounts to Sync

A connection can expose several accounts (checking, savings, a card). Pick the ones to sync and, optionally, map each to an existing ledger or set how far back to import:

bash
curl -X POST "https://api.ondayzero.com/api/v1/plaid/link-selected-accounts" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "connection_id": "CONNECTION_UUID",
    "plaid_account_ids": ["PLAID_ACCOUNT_ID"],
    "entries_start": "2026-01-01"
  }'

Each linked account becomes a ledger that transactions flow into, with financial_account_type bank_account (Plaid depository), credit_card (credit), loan or investment. To reuse an existing ledger instead of creating a new one, list the eligible ones with GET /api/v1/plaid/ledger-candidates?plaid_account_type=depository (depository → asset ledgers; credit or loan → liability ledgers) and pass "ledger_assignments": {"PLAID_ACCOUNT_ID": "LEDGER_UUID"} when linking. A candidate ledger must be active, not read-only, not mid-reconciliation and not already bound to another Plaid link. Pass "include_deleted": true to restore previously hidden transactions that reappear in a backfill.

For connections with many accounts use POST /api/v1/plaid/link-selected-accounts/async (same body). It returns HTTP 202 immediately and does the work in the background; watch the connection's sync_status in GET /api/v1/plaid/connections until it leaves syncing. (The poll_url in the 202 response points at a per-connection path that does not exist; use the list endpoint.) POST /api/v1/plaid/cancel-linking with {"connection_id": "…"} stops a link that is stuck in syncing and sets its sync_status to error; it returns 400 when there is nothing to cancel.

Transaction Syncing

Once connected, transactions sync automatically: Plaid's transaction webhooks trigger a sync within moments of new data, and a recurring sync runs every 6 hours as a backstop. New transactions are:

  1. Imported with raw bank descriptions (source: plaid)
  2. Cleaned (merchant name normalization into counterparty)
  3. Auto-categorized using AI and user-defined bank rules
  4. Available for review in GET /api/v1/transactions — see Transactions

Balances are not updated by transaction sync. They are written at link time and by POST /api/v1/plaid/refresh-balances (optional connection_id; omit to refresh all connections), which does a live balance pull per connection and is only run on demand, never on a schedule.

Reauthorization

Bank connections occasionally require reauthorization (e.g., when a bank requires re-login or MFA). Plaid's ERROR and PENDING_EXPIRATION item webhooks set the connection's sync_status to requires_reauth (visible in GET /api/v1/plaid/connections, alongside error_code) and prompt users in the dashboard; a LOGIN_REPAIRED webhook sets it back to active and triggers an immediate sync. Programmatically, POST /api/v1/plaid/reauthorize-connection with {"connection_id": "…"} returns a fresh Hosted Link URL, or pass connection_id to /plaid/link-token for an update-mode embedded token.

List Connected Accounts

bash
curl "https://api.ondayzero.com/api/v1/plaid/connections" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

Returns every bank connection with its institution, status and accounts. GET /api/v1/plaid/bank-accounts lists the linked accounts (only those with a ledger link, not every account on the connection) with their cached balances in cents, balance_as_of, last_synced and the ledger_id they sync into. The same connection summary is embedded on each business as plaid_connections in GET /api/v1/businesses.

Disconnect

Unlinking an account stops syncing it and keeps the books. POST /api/v1/plaid/account/unlink with {"connection_id": "…", "plaid_account_id": "…"} marks the account unlinked: no more transactions are imported and its balance leaves /plaid/bank-accounts and the dashboard, but its ledger and every transaction, journal entry, reconciliation and bill payment on it stay, so reports, GL balances and closed periods do not change. Linking the account again (POST /api/v1/plaid/link-selected-accounts) resumes syncing into the same ledger; pick a sync-from date that covers the gap. The response has history_preserved: true.

Deleting history is a separate, explicit choice. Add "delete_history": true to delete the account's ledger together with every transaction, line entry and journal entry on it (and any bill payments); the Plaid account record is kept so it can be linked again to a fresh ledger without going through Plaid Link. Deleting a connection does the same for every ledger under it and revokes the Plaid access token; the cleanup runs in the background after the call returns.

Preview the impact first:

bash
curl -X POST "https://api.ondayzero.com/api/v1/plaid/account/unlink-preview" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{"connection_id": "CONNECTION_UUID", "plaid_account_id": "PLAID_ACCOUNT_ID"}'

The preview returns the linked ledgers, row counts, stop_sync_available, requires_confirmation and a confirm_phrase. To delete one account's history, POST /api/v1/plaid/account/unlink with the same body plus "delete_history": true and confirm_phrase echoed exactly when the preview requires it (the linked ledger's name). To remove a whole bank connection, call POST /api/v1/plaid/connection/delete-preview with {"connection_id": "…"}, then DELETE /api/v1/plaid/connection with connection_id and confirm_phrase (the ledger name for a single-ledger connection, otherwise DELETE <institution name>). A wrong or missing phrase returns 400.

Deleting history and deleting a connection are refused (400) when bills are linked to the bank account or connection, or when the cascade would delete manual journal entries or entries in a closed period.

Move an Account Between Connections

If the same bank was connected twice, an account that is linked through another live connection can be moved so it syncs through this one instead. The ledger and its history stay and nothing already in the books is imported again.

bash
curl -X POST "https://api.ondayzero.com/api/v1/plaid/account/move" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{"connection_id": "NEW_CONNECTION_UUID", "plaid_account_id": "PLAID_ACCOUNT_ID"}'

The response reports from_connection_id, to_connection_id, the unchanged ledger_id, and how many linked accounts the previous connection has left (zero means it can be removed without deleting anything).

Common errors

Status Cause Fix
403 The token's user is not an admin of the business Use an admin's token for link, unlink, reauthorize, refresh and delete calls
400 confirm_phrase missing or not exactly what the preview returned Call the matching …preview endpoint and echo its confirm_phrase
400 Unlink or delete refused: bills are linked to the account, or the cascade would delete manual journal entries or entries in a closed period Unlink the bills, or reopen the period, then retry
400 cancel-linking with nothing in progress Nothing to cancel; check sync_status in GET /api/v1/plaid/connections