# Bankable > Bankable gives owner-led small and midsize businesses an AI-powered CFO and bookkeeper: daily bank sync, reviewable books, financial statements tied to the model, and forecasts from reconciled months, including lender-ready models and capital package preparation. Bankable is for owners, operators, and finance teams at owner-led small and midsize businesses. Public pages below describe the product, published prices, privacy practices, and the newsletter. The signed-in app, API routes, and customer books are private. ## Product pages - [Home](https://getbankable.ai/): Bankable is an AI-powered CFO and bookkeeper for owner-led businesses: daily bank sync, reviewable books, statements tied to the model, and forecasts. Markdown: https://getbankable.ai/index.md - [Pricing](https://getbankable.ai/Pricing): Bankable plans are priced by companies and bank connections. Unlimited users, daily sync, and no transaction-count fees. Starter, Growth, Portfolio, and Scale. Markdown: https://getbankable.ai/Pricing.md - [Privacy Policy](https://getbankable.ai/Privacy): How Bankable collects, uses, shares, and protects account, business, and Plaid data. Effective May 13, 2026. Contact support@getbankable.ai. Markdown: https://getbankable.ai/Privacy.md - [Newsletter](https://getbankable.ai/Newsletter): Practical cash-flow, bookkeeping, and AI notes from Bankable founder Mundo Sanchez. Free to read. No Bankable account required. Markdown: https://getbankable.ai/Newsletter.md ## Optional - [Expanded product and Connect reference](https://getbankable.ai/llms-full.txt): full public-page text plus the Bankable Connect API reference. - [Connect API docs](https://getbankable.ai/api-docs.md): frozen Connect v0 event types and read actions. - [OpenAPI](https://getbankable.ai/openapi.json): request and response schema for the Connect gateway. - [Integration agent](https://getbankable.ai/integration-agent.md): read-only Staylist partner tools. ## Pricing published on the site - Starter: $49/mo with benchmarking opt-in, $79/mo standard. 1 company. 3 bank connections. - Growth: $99/mo with benchmarking opt-in, $119/mo standard. Up to 3 companies. 9 bank connections. - Portfolio: $279/mo with benchmarking opt-in, $299/mo standard. Up to 10 companies. 30 bank connections. - Scale: For 8+ companies, 18+ bank connections, higher-frequency sync, custom onboarding, security review, or advanced support requirements. No fixed price is published for Scale. - Annual billing is 30% off the monthly price times 12. ## Bankable Connect > Post property-operating events into Bankable and read the posted books. Connect v0. Partners authenticate with the `x-api-key` header (`bk_…`), not a user JWT. The edge function is deployed with `verify_jwt = false`. Base URL: https://getbankable.ai/functions/v1/api_gateway ## Discovery - [OpenAPI 3](https://getbankable.vercel.app/openapi.json): request and response schema for the gateway - [API docs](https://getbankable.vercel.app/api-docs.md): every frozen v0 event type and the read actions - GET the base URL: health JSON (`name`, `connect_version`, `auth_header`, `idempotency_header`, and these links) ## Write — scope `write:events` POST the base URL. Header `Content-Type: application/json`. Header `Idempotency-Key` is optional; when it is omitted, `external_id` is the idempotency key. Optional header `apikey` is the Supabase anon key and is not Bankable auth. ```json { "type": "invoice.issued", "occurred_at": "2026-09-01T15:00:00Z", "external_id": "inv_1001", "data": {} } ``` Frozen event types, and no others: customer.upserted, booking.created, lease.activated, invoice.issued, payment.received, expense.recorded, vendor_bill.issued, journal_entry.posted `journal_entry.posted` is the partner-supplied balanced journal. Optional `data.vendor_id` and/or `data.vendor_name` attach a vendor contact in this company, the same way `expense.recorded` does. `vendor_name` creates or reuses the contact. `vendor_id` must already belong to this company. `generalLedger` is a read, not an event type. `deleteJournalEntry` (scope `write:events`): `{ "action": "deleteJournalEntry", "params": { "id": "JOURNAL_ENTRY_ID" } }`. Soft-deletes that journal entry in the key's company only (`deleted_at`, status `deleted`). Not a void. Missing and other-company ids return `not_found`. Does not delete bank transactions or contacts. Closed periods return `period_closed`. `createJournalEntry` (scope `create:journal_entries`, not `write:events`): `{ "action": "createJournalEntry", "params": { "external_id": "smd-opening", "entry_date": "2024-01-01", "description": "Opening balances", "lines": [{ "gl_account_number": "1510", "debit": 250000, "credit": 0, "memo": "Building" }, { "gl_account_number": "2510", "debit": 0, "credit": 250000 }] } }`. Lines resolve on this company's chart by `gl_account_id`, `gl_account_number`, or `name`. Debits must equal credits. `params.company_id` is ignored. The same `external_id` and payload returns `already_posted`. A different payload returns `idempotency_conflict`. A closed period returns `period_closed`. `createGlAccount` (scope `update:accounts`): `{ "action": "createGlAccount", "params": { "name": "Building", "account_number": "1510", "category": "fixed_assets" } }`. `category` examples: `bank`, `fixed_assets`, `long_term_liabilities`. `type` is an alias for `category`. `account_type` is `balance_sheet` or `income_statement` and is inferred from the category. The id is `gl_{account_number}`. The same number and name returns `already_exists`. A different account with that number returns `account_number_taken`. Only the key's company chart is updated. `mapBankAccount` (scope `update:accounts`): `{ "action": "mapBankAccount", "params": { "id": "BANK_ACCOUNT_ID", "gl_account_number": "1011" } }`. Sets `BankAccount.gl_account_id` for one bank account in the key's company. The target GL account must already be on the chart and its category must be `bank`, so checking and savings can use different cash accounts. Another company's bank account returns `not_found`. A repeat returns `already_mapped`. `categorizeBankTransaction` (scope `write:events`, no new scope): `{ "action": "categorizeBankTransaction", "params": { "id": "BANK_TRANSACTION_ID", "approve_suggestion": true } }` or `"gl_account_id": "ACCOUNT_ID"`. Approves one bank transaction in the key's company the same way the Bank Transactions page posts a category: sets the GL account and one split, marks `user_approved`, and posts a journal with `buildJournalEntryForBankTransaction` when the row is postable. `approve_suggestion` uses `suggested_gl_account_id`. An explicit `gl_account_id` wins when both are sent. Optional `splits` is `{ gl_account_id, amount }[]`. Amounts must sum to the absolute bank amount. The cash side stays one line. Loan payments that need a split are saved and not posted (`needs_split_review`, message `Loan payment requires split review before posting`) on the single-account path. Passing `splits` posts that loan payment. Other companies, missing ids, and deleted rows return `not_found`. Pending rows return `pending`. Unknown accounts return `unknown_gl_account`. Closed periods return `period_closed`. A repeat returns `already_categorized`. A row locked by a journal link or a transfer returns `already_linked`. `params.company_id` is ignored. Read scopes cannot call it. `updateGlAccount` (scope `update:accounts`) and `listGlAccounts` / `listChartAccounts` (scope `read:accounts` or `read:ledger`) read and update the key's chart. `updateForecastSeasonality` and `updateAccountNoiTreatment` require `write:events`. `setBankTransactionStatus` (scope `update:bank_transaction_status`): `{ "action": "setBankTransactionStatus", "params": { "id": "BANK_TRANSACTION_ID", "status": "needs_mapping" } }`. `status` is `do_not_post`, `needs_mapping`, or `needs_review`. Optional `memo` and `reason`. A posted row returns `already_posted`. Nothing is deleted. `needs_mapping` clears a stuck `ready_to_post` / `needs_split_review` state. `linkBankTransactionToJournalEntry` (scope `link:bank_transactions`): `{ "action": "linkBankTransactionToJournalEntry", "params": { "bank_transaction_id": "…", "journal_entry_id": "…" } }`. Links the feed row to an existing journal entry in the same company. The entry must have a line on the bank row's GL account and the amounts must agree (`journal_account_mismatch`, `amount_mismatch`). Sets `posting_status` `posted` and does not create a journal. Linked rows are not counted again in `financialReport`. `linkBankTransactionTransfer` (scope `link:bank_transfers`): `{ "action": "linkBankTransactionTransfer", "params": { "bank_transaction_id": "…", "paired_bank_transaction_id": "…" } }`. Same company, equal amounts, opposite directions, different bank accounts. Optional `journal_entry_id` links both rows to that entry. Otherwise one transfer journal is posted (debit the receiving bank GL account, credit the sending bank GL account) and both rows are linked so neither can be categorized later. `createTransactionRule` (scope `create:transaction_rules`), `listTransactionRules` (scope `read:transaction_rules`), and `applyTransactionRule` (scope `apply:transaction_rules`) use the `TransactionRule` entity in the key's company. `createTransactionRule` with `apply_to_open_queue: false` saves the rule and does not categorize existing open rows (`applied_count: 0`, `posted: false`). `dry_run: true` on `createTransactionRule` (with `apply_to_open_queue: false`) or `applyTransactionRule` is read-only: no rule insert, no bank update, and no journal. An `approval_required` agent key can call that preview when it has the action scope or `read:transaction_rules`. `dry_run` with `post` or `apply_to_open_queue: true` returns `invalid_request`. A real create or apply still returns `approval_required` for that agent. `applyTransactionRule` does not post a journal unless `post` is true. `getBankTransaction` (scope `read:ledger`) reads one bank transaction UUID in the key's company. Month-close actions `getMonthClose` and `getBankStatement` require `read:reconciliations` and `read:ledger`. `reserveBankStatementUpload`, `confirmBankStatementUpload`, and `sendMonthCloseMessage` require `write:reconciliations` and `read:ledger`. They run on the live book only. GET discovery includes `month_close_agent`. Mortgage actions use `read:mortgage_statements`, `write:mortgage_statements`, or `write:mortgage_reconciliation` (`getMortgageReconciliation` also needs `read:ledger`). Asset actions use `read:assets` or `write:assets`. Onboarding uses `read:onboarding` or `write:onboarding`. GET discovery includes `mortgage_agent`. ## Read - `generalLedger` (scope `read:ledger`): `{ "action": "generalLedger", "params": {} }` — up to 500 journal entries, newest first, soft-deleted entries omitted. `params` are not filters in v0. - `listBankTransactions` (scope `read:ledger`): `{ "action": "listBankTransactions", "params": {} }` — bank transactions for the API key's company only. Omit `status` for the review queue (uncategorized or `needs_review`). Narrow with `status`: `uncategorized`, `needs_mapping`, `needs_review` (alias `needs-review`), `pending`, `posted`, `ready_to_post`, `do_not_post`, `unposted`, or `all`. Optional `start_date`, `end_date`, `bank_account_id`, and `limit` (1–100, default 50). `params.company_id` is ignored. Test keys read the sandbox book. Read only. - `financialReport` (scope `read:reports`): `{ "action": "financialReport", "params": { "report_type": "income_statement", "start_date": "YYYY-MM-DD", "end_date": "YYYY-MM-DD" } }` — v0 returns one summary (income statement, balance sheet, totals). `report_type` is echoed. `start_date` and `end_date` filter the period. - `listEvents` / `getEvent`: allowed with any of `write:events`, `read:events`, `read:ledger`, `read:reports`. ## MCP Local server in the getbankable repo at `packages/bankable-mcp` (not published to npm). Tools: `ingest_event`, `delete_journal_entry`, `create_journal_entry`, `create_gl_account`, `map_bank_account`, `general_ledger`, `list_bank_transactions`, `categorize_bank_transaction`, `set_bank_transaction_status`, `link_bank_transaction_to_journal_entry`, `link_bank_transaction_transfer`, `create_transaction_rule`, `list_transaction_rules`, `apply_transaction_rule`, `financial_report`, `list_events`, `get_event`. Mortgage, asset, forecast, onboarding, `listGlAccounts`, and `updateGlAccount` are gateway actions and are not MCP tools. Env: `BANKABLE_API_KEY`. Optional `BANKABLE_API_BASE_URL` defaults to the base URL above. Optional `SUPABASE_ANON_KEY` is forwarded as `apikey` when set. There are no underwriting or capital-package tools. ## Partner Integration Bridge Read-only Staylist tools: `list_integrations`, `list_integration_units`, `list_integration_reservations`, `get_integration_reservation`. Gateway actions: `listIntegrations`, `listIntegrationUnits`, `listIntegrationReservations`, `getIntegrationReservation`. Contract: https://getbankable.ai/integration-agent.md. Use the existing live assigned agent key, explicit `read:reports`, and the company owner's integration-specific approval. Never request partner tokens. Connecting in Bankable approves the selected existing agents atomically. Remote MCP installations must update this adapter and rediscover tools once; no new Bankable key is needed. Provider bookings are not posted ledger revenue. ## User-linked bookkeeper payables Call `{ "action": "capabilities", "params": {} }` first. It returns supported actions and which are currently available to the key. Do not guess bill action names or replace a user-linked key with a company-wide key. - `createBill`: `{ "action": "createBill", "params": { "external_id": "stable-vendor-bill-id", "bill_date": "YYYY-MM-DD", "due_date": "YYYY-MM-DD", "vendor_id": "VENDOR_ID", "amount": 100, "expense_account_id": "GL_ACCOUNT_ID", "description": "Bill description" } }`. Creates the bill and its accrual journal through `vendor_bill.issued`. Keep `external_id` stable on retry. Requires trusted approval plus create:bills, create:journal_entries, write:events, create:vendors, update:vendors, and create:accounts because the existing event importer can create/update vendor and chart records. Does not record payment. - `listBills`: `{ "action": "listBills", "params": { "limit": 50, "offset": 0 } }`. Requires read:bills. Follow next_offset when present; optional external_id filter. - `getBill`: `{ "action": "getBill", "params": { "id": "BILL_ID" } }`. Requires read:bills. - `matchBillPayment`: `{ "action": "matchBillPayment", "params": { "bill_id": "BILL_ID", "bank_transaction_id": "BANK_TRANSACTION_ID" } }`. Requires trusted approval and update:bills, create:journal_entries, update:transactions. Records an existing outgoing bank payment using its exact amount, date and mapped bank account. Atomically posts DR A/P, CR bank cash, reduces the bill balance and links the bank transaction. Does not initiate or transfer funds. A same-pair retry returns already_matched. Rejects pending payments, payments above the bill balance, mismatched currencies/vendors, and bank rows already posted or matched elsewhere. If multiple A/P accounts exist, provide ap_gl_account_id. Existing closed-period and ledger guards still apply. Remote MCP tools: connection_capabilities, create_bill, list_bills, get_bill, match_bill_payment. Refresh tool discovery after deployment. Creating an accrual bill on one date and matching a later bank payment keeps expense recognition separate from cash movement. ### Correct a standalone payment before bill matching Inspect getBankTransaction and generalLedger first. If the payment was posted as a standalone expense, call `unpostBankTransaction` with bank_transaction_id, expected_journal_entry_id and a reason. Requires trusted approval, update:transactions and update:journal_entries. It atomically voids that exact standalone journal and returns the bank transaction to unposted. It retains audit history and rejects reconciled rows, transfer links and existing bill/invoice matches. A retry returns already_unposted. This does not reverse separate workaround journals. If the unposted payment points to the wrong vendor record, call `setBankTransactionVendor` with bank_transaction_id, vendor_id, expected_vendor_id (null when absent) and a reason. Requires trusted approval, update:transactions and read:vendors. It validates an active vendor in the same company and updates the vendor/party references together. It does not infer identity from matching names or merge vendors. A stale vendor expectation is rejected; a retry returns already_linked. Then call matchBillPayment. Existing posted/document-matched transactions must be reviewed before a vendor correction. MCP tools: unpost_bank_transaction and set_bank_transaction_vendor. Use the same connection key; call connection_capabilities to see effective permissions. Refresh tool discovery after deployment.