Transaction categorization

Sort every transaction in a statement set, and every receipt or invoice, into a chart of accounts -- your own vendor rules first, then AI restricted to accounts that actually exist in your chart.

Annual categorized transactions retain readable rows when another statement row is unreadable. An unreadable amount is null, needs_input is true, and review_reason identifies what our reading missed.

Account names must contain a non-whitespace character. Category rule priorities must be integers. Settings updates must include large_purchase_threshold or work_description; send null to clear the work description. Vehicle deduction years must be integers from 2000 through 2100. Invalid values return 400.

Group Undo restores each row to its state immediately before confirmation. Return each undoToken in confirmedRows unchanged. A supplied token must be a string; other types return 400. Rows edited later are kept and counted in skippedCount, with details in failedRows.

Possible transfers need your confirmation. Opposite amounts alone stay in review with reason code transfer_suggestion and are not marked as transfers. Your rules take precedence. AI transfer suggestions always need confirmation, regardless of confidence.

Categorization runs on top of a statement set or a receipt/invoice document you’ve already uploaded and verified. It never changes an amount, and it never invents an account: every category it assigns points at a real row in your own chart of accounts.

Unknown transaction dates remain empty and need a date before they count in any tax year. Upload dates are never used as transaction dates.

The chart of accounts

A brand-new org starts with a standard chart the first time it’s used: a self-employed business chart mapped to IRS Schedule C (Form 1040) -- every expense account carries the exact line it maps to (e.g. “Line 18 Office expense”) -- plus a short personal-spending chart, so a mixed-use account’s personal transactions land somewhere honest instead of being forced into a business account. Fully editable afterward (PATCH /v1/chart-of-accounts/{id}), and importable from a QuickBooks or Xero chart-of-accounts CSV export. The standard chart also works without importing a file.

Creating or renaming an account to a name already used in the same scope returns HTTP 409 with ACCOUNT_NAME_EXISTS. Choose another name.

Rules, then AI

Rule creation and updates reject unknown values for amount_direction, match_field, and pattern_type with HTTP 400. Omitted fields on creation use the documented defaults.

Every transaction is checked against your own vendor rules first (a QuickBooks/Xero-style condition: description contains/starts with/exactly matches/matches a pattern, optionally gated by money-in/money-out direction and an amount range). A rule match is instant and free. Anything a rule doesn’t catch goes to an AI step whose answer is restricted to the account names that actually exist in your chart -- it can never invent one, and it always comes back with a confidence score.

New orgs start with a few built-in rules for common self-employed patterns:

  • Incoming payments from Stripe, PayPal, Upwork, Uber, or DoorDash route to Business income.
  • An outgoing payment to a seller or contractor via Upwork, Fiverr, or Freelancer.com routes to Contract labor -- a platform fee stays on Commissions and fees.
  • An explicit transfer to yourself routes to Owner’s draw. A bare ATM or cash withdrawal does not auto-categorize on its own -- it’s genuinely ambiguous (a business expense or a personal draw), so it’s sent to your input unless you’ve created your own rule for your own withdrawals.

Group undo requires a non-negative integer rowIndex and a non-empty documentId for each row. If supplied, undoToken must be a non-empty string. Invalid rows return HTTP 400 with INVALID_ROWS before any changes are made.

Reviewing by merchant, not by row

The review screen groups your transactions by merchant instead of showing one row at a time -- the same pattern QuickBooks and Xero use for their own bank rules. Each group shows the merchant, how many transactions, the total, the date range, and the suggested account. One action confirms the whole group at once and saves a rule, so the same merchant sorts itself automatically on every future upload too. You can still expand any group and change a single row.

Two kinds of transactions are never bulk-confirmed, even inside a group: a large or equipment-looking purchase, and a bare ATM or cash withdrawal. Both always stay individual, so they get the judgment call they need. A known mixed-use merchant (Amazon, Walmart, Costco, Target, Apple, PayPal, Venmo) always opens expanded, with a note that purchases there can be business or personal -- confirming that group never creates an automatic rule unless you choose to. Undo is available right after confirming a group, and reverses both the categorization and the rule.

GET /v1/statement-sets/{id}/categorization/groups returns the same merchant-grouped view; POST .../categorization/groups/confirm confirms a group by its own merchant_key; POST .../categorization/groups/undo undoes it.

Transfers and owner’s draws

A transfer between two of your own accounts -- including paying a credit card from checking -- is detected automatically (opposite-signed amounts of the same size, a few days apart, on two different account numbers) and marked as a transfer, never as income or an expense, so it’s never double-counted. This works even when the two accounts are uploaded as two separate statement sets.

Large purchases go to review

A purchase at or above a threshold (default $2,500, the IRS de minimis safe harbor for tangible property, adjustable per org via PATCH /v1/chart-of-accounts/settings) is never auto-categorized as an ordinary expense -- it always lands in your review instead, no matter its category or description. Schedule C line 13 depreciation needs a useful-life/placed-in-service decision that bank data alone can never supply. Under that threshold, a purchase is an ordinary expense in its own category, even one that sounds like equipment -- your accountant-ready export notes when that’s the case, citing the de minimis safe harbor.

Vehicle costs and how you deduct them

A “Car and truck expenses” candidate depends entirely on how you deduct your vehicle, so it’s never guessed at until you say -- GET/PATCH /v1/tax-years/{year}/vehicle-deduction sets one of standard_mileage (the IRS per-mile rate already covers repairs, fuel, and insurance -- a vehicle-cost transaction is marked non-deductible, never claimed again on Line 9), actual_expenses (real vehicle costs are the Line 9 deduction), or no_vehicle. Scoped per tax year, since the choice can change year to year. Unanswered for a year, a vehicle-cost transaction from that year always lands in Needs your input.

What kind of work you do

PATCH /v1/chart-of-accounts/settings also accepts work_description, a one-time, plain-English answer to “What kind of work do you do?” fed into the AI categorization step as context -- the same idea apps like Keeper use to tailor categories to the job. It’s a hint only: the AI still only ever picks from your own chart, and a vague or wrong answer here never overrides a clearer signal.

Running categorization

GET /v1/statement-sets/{id}/categorization always runs a free, rule-only pass (never calls the AI on its own, so simply opening the page never costs anything). POST /v1/statement-sets/{id}/categorize runs the full pass, rules then AI:

http
POST /v1/statement-sets/sts_9f2a1c/categorize HTTP/1.1
Authorization: Bearer sk_live_...
json
{
  "transactions": [
    {
      "transaction": { "documentId": "doc_january", "rowIndex": 4, "date": "2026-01-11",
        "description": "STRIPE PAYOUT", "amount": 842.10 },
      "category": { "status": "categorized", "confidence": 1, "source": "rule", "is_transfer": false },
      "account": { "id": "acct_income", "name": "Business income", "type": "income",
        "tax_line": "Line 1 Gross receipts or sales" },
      "reconciled": true
    },
    {
      "transaction": { "documentId": "doc_january", "rowIndex": 9, "date": "2026-01-14",
        "description": "BEST BUY #4021", "amount": -3199.00 },
      "category": null,
      "account": null,
      "reconciled": true
    }
  ]
}

Correcting a category

A manual correction always becomes your own final answer, with one level of undo. Set create_rule to also learn a rule from the correction and apply it to the rest of the set right away:

http
PATCH /v1/statement-sets/sts_9f2a1c/categorization/rows/doc_january/9 HTTP/1.1
Authorization: Bearer sk_live_...
Content-Type: application/json

{
  "account_id": "acct_office",
  "create_rule": true
}

Needs your input

GET /v1/statement-sets/{id}/categorization/questions lists every transaction that still needs your own look -- nothing decided it automatically -- each with the date, amount, description, and whatever suggestion (if any) came back. Every one also carries a reason: one short, plain-English line explaining why (for example, “this looks like a large purchase or piece of equipment”, or “this looks like a vehicle expense, but you haven’t said how you deduct your vehicle yet”, or just “our best guess, but not confident enough to apply automatically”). Answer it with the same PATCH endpoint above.

Deductions and excluded transactions

GET /v1/statement-sets/{id}/deductions groups every confirmed business-expense transaction in USD by its Schedule C line. Each transaction includes its original currency. Foreign and unresolved currencies appear in currencyReview with a reviewReason and stay out of confirmedTotal and possible totals. GET /v1/statement-sets/{id}/excluded lists exactly what got excluded as Owner’s draw or personal spending, so you can confirm nothing was mis-sorted in either direction.

Two separate signals, never merged

Every categorized transaction carries reconciled (whether its source document’s own extraction and balance checks passed) and category.confidence (how sure the categorization is) as two separate fields. They answer different questions and are never combined into one badge -- a confidently categorized transaction on a document with a real balance mismatch is never shown as if everything about it were fine.

Receipts and invoices

A receipt or invoice is categorized as its own single transaction (its total) via GET/POST/PATCH /v1/documents/{id}/categorization. It’s also matched automatically to its bank or card transaction by exact amount, a date window, and merchant similarity -- a match is never claimed by two different receipts, so nothing is ever double-counted.

Exports

The combined CSV and XLSX exports get an “Account” column. Xero’s CSV export switches to Xero’s own documented “precoded” format (adding an Account Code column) once every transaction in the set has an assigned account with a code -- never a half-coded file. QuickBooks’ own plain bank-transaction CSV format ignores any category column on import (per Intuit’s own documentation), so its export is unchanged.