Transactions
Transactions represent bank transactions synced from Plaid, imported from Ramp, Xero or QuickBooks, uploaded from CSV, or created manually. They are the core data type for bank feeds, categorization, and reconciliation.
Transaction Lifecycle
- Sync — Transactions arrive from a connected bank (
source: plaid), a card program (ramp), a migration (xero,quickbooks), a CSV upload (bulk_upload) or the API (manual) - Clean — Merchant names are normalized into
counterpartyautomatically - Categorize — AI, bank rules, or manual assignment to an expense/revenue ledger via the transaction's journal entry
- Review — A user confirms the categorization (
review_status: reviewed) - Reconcile — Matched against bank statements (
reconciled: true) — see Reconciliation
List Transactions
curl "https://api.ondayzero.com/api/v1/transactions?limit=25&only_uncategorized=true&start_date=2026-03-01" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"{
"success": true,
"data": {
"items": [
{
"id": "0198a2c4-0000-7d3a-9a51-3f6f0c1d2e10",
"amount": -4250,
"display_amount": -4250,
"currency": "USD",
"datetime": "2026-03-14",
"counterparty": "Staples",
"description": "STAPLES 00123 AUSTIN TX",
"ledger_id": "0198a2c4-0000-7d3a-9a51-3f6f0c1d2e01",
"ledger_name": "Business Checking",
"posted_status": "posted",
"review_status": "unreviewed",
"reconciled": false,
"source": "plaid",
"categorization_method": "uncategorized",
"opposing_line_entries": []
}
],
"next_cursor": "0198a2c4-0000-7d3a-9a51-3f6f0c1d2e10",
"prev_cursor": null,
"has_next": true,
"has_prev": false,
"limit": 25,
"total": null
}
}Rows carry more fields than shown (see Key Fields). Page with next_cursor as described in Pagination.
Filter Parameters
| Parameter | Type | Description |
|---|---|---|
start_date / end_date |
date | Transaction date range |
ledger_id |
UUID | Bank account ledger the transaction belongs to |
opposing_ledger_id |
UUID | Category ledger (the expense/revenue side of the journal entry) |
source_account_id |
UUID[] | Plaid source account(s) |
search |
string | Case-insensitive match on description or counterparty |
amount_direction |
string | inflow (money in) or outflow (money out) |
only_uncategorized |
boolean | Only transactions with no category yet |
missing_counterparty |
boolean | Only transactions with no counterparty |
review_status |
string | unreviewed, reviewed, or comma-separated |
reconciled |
boolean | true only reconciled, false only unreconciled |
posted_status |
string | posted (cleared) or pending (unsettled authorizations) |
source |
string | plaid, ramp, bulk_upload, manual, xero, quickbooks, or comma-separated |
rule_id |
UUID | Only transactions this bank rule categorized (categorized_by.rule_id) |
has_attachments |
boolean | true only transactions with a receipt/document attached, false only those with none |
receipt_status |
string | attached, exempt, not_required, requested, missing, or comma-separated (applies the business's receipt policy) |
card_holder |
string | One cardholder's transactions (a Ramp employee, or the card member a bank reports on a multi-card account), by name exactly as GET /api/v1/transactions/card-holders lists it (case-insensitive) |
tag_id / journal_entry_id / id |
UUID | Exact-match filters |
Plus the standard pagination parameters.
Key Fields
| Field | Type | Description |
|---|---|---|
amount |
integer | Amount in cents as stored (positive = money in, negative = money out on bank accounts). Credit-card accounts can carry Plaid-native signs, so use display_amount for a consistent sign |
display_amount |
integer | Cents in cash-flow convention: negative = outflow/expense, positive = inflow/income, on every account type |
currency |
string | ISO currency code |
datetime |
datetime | When the transaction occurred |
description |
string | Raw bank description |
counterparty |
string | Cleaned merchant / payee name |
ledger_id / ledger_name |
UUID / string | The bank account ledger |
journal_entry_id / journal_entry |
UUID / object | The double-entry record that carries the categorization |
opposing_line_entries |
array | The category side(s) of that journal entry — where the money was booked |
posted_status |
string | not_posted, posted, deleted |
review_status |
string | unreviewed, reviewed |
reconciled |
boolean | Matched to a bank statement |
categorization_method |
string | ai, rules, similarity, transfer_between_accounts, user, historical, global_pattern, merchant, pfc, account_type, counterparty_default, uncategorized |
categorized_by |
object | When a bank rule chose the category: rule_id, rule_name, field_matched (description, counterparty, amount, ...); otherwise null |
source |
string | Where the transaction came from (see filters) |
attachment_count / receipt_status |
integer / string | Receipts attached (entries are in meta.attachments) and where the transaction stands: attached, exempt, not_required, requested, missing |
card_holder_name / account_mask |
string | Whose card the charge was on (the Ramp employee, or the card member the bank reports on an account with several cards) and the last four digits of the card/account; null when unknown |
counterparty_provenance |
object | Why counterparty shows this name (manual rename, naming rule, saved rename, registry, or raw bank text) |
comment_count |
integer | Comments on the transaction (GET /api/v1/transactions/{transaction_id} also returns the thread) |
can_be_deleted |
boolean | Whether the caller can delete or hide it (see Delete and Restore) |
invoice_ids / bill_ids |
UUID[] | Invoices or bills this transaction pays |
Create a Transaction
For bank accounts that aren't connected to a feed. Every field is optional; datetime is a date (YYYY-MM-DD), currency is one of USD, CAD, AUD, EUR, GBP, and source_account_id is the bank account ledger UUID. Use a negative amount for money out:
curl -X POST "https://api.ondayzero.com/api/v1/transactions" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"amount": -5000,
"currency": "USD",
"datetime": "2026-03-15",
"description": "Office supplies",
"source_account_id": "SOURCE_ACCOUNT_UUID"
}'POST /api/v1/transactions/bulk creates many at once ({"transactions": [...]} of the same shape; the response reports success_count, error_count and errors). For CSV/Excel statements use the upload flow, which works from an s3_key of a file you already uploaded:
POST /api/v1/transactions/bulk-upload/pastewith{"pasted_text": "..."}turns copied CSV/TSV text into ans3_key(androw_count);POST /api/v1/transactions/bulk-upload/from-statementwith{"s3_key", "ledger_id"}reads a PDF, image, CSV or Excel statement into upload-ready rows (nothing is imported).GET /api/v1/transactions/bulk-upload/templatereturns a sample CSV (columnsdate,amount,description,category) ascsv_content_stringifiedandcsv_content_base64.POST /api/v1/transactions/bulk-upload/headers(discover the columns),…/bulk-upload/preview, then…/bulk-uploadwith acolumn_mapping(keysdate,amount,description, optionalcategory). Target an existing ledger withledger_id, or create one by sendingledger_nameplusfinancial_account_type. Duplicates are skipped by default (check_duplicates). Acategorythat names no ledger refuses the file (400) unlessallow_unmatched_categoriesis true. The import runs as a background workflow; the response returns aworkflow_id.POST /api/v1/transactions/enrich-from-exportmatches a detailed bank "account activity" export to transactions already in a ledger and appends wire originators, payers and other detail to the description and counterparty.dry_rundefaults totrue; set it tofalseto write. Rows are matched on amount and a date withindate_tolerance_days(default 2, max 7), one-for-one. Detected columns are echoed back; override them withcolumn_mapping({role: header}fordate,amount,debit,credit,description,party,detail). Hand-edited descriptions and counterparties are left alone unlessoverwrite_user_editsistrue, and a counterparty taken from invoices or bills is never replaced.
When the account already has part of the statement's period (a bank feed, an earlier upload), first call POST /api/v1/transactions/bulk-upload/diff with the s3_key and ledger_id. It is read-only and pairs each file row with at most one existing transaction — same amount and date and description, or same amount within date_tolerance_days (default 3) with a shared word or matching counterparty — and lists the rows the ledger is missing, ambiguous same-amount pairs, and ledger rows the file lacks. Then upload with only_missing_from_diff: true (or include_row_numbers) to import exactly the missing rows. Pass exclude_statuses: ["Pending"] when the export includes unsettled rows.
Categorization
Every transaction is categorized through its journal entry — the bank ledger on one side, the category ledger on the other. You rarely build that entry yourself:
- Recategorize one or many:
PATCH /api/v1/transactions/batch/categorywith{"transaction_ids": [...], "ledger_id": "CATEGORY_LEDGER_UUID"}moves the opposing line entries to the new ledger. - Every uncategorized transaction for a merchant:
POST /api/v1/transactions/recategorize-by-counterpartywith{"counterparty": "Uber", "ledger_id": "..."}moves the merchant's still-uncategorized transactions, creates a bank rule so future ones follow, and returns{"updated": 12, "counterparty": "Uber"}. - Let AI do it:
POST /api/v1/transactions/categorize-allruns the full categorization workflow (bank rules → history → LLM) over all uncategorized transactions, auto-applies high-confidence results, and returns a workflow ID immediately.POST /api/v1/transactions/suggest-categoriesreturns suggestions without applying them; it takes query parameterslimit(1-100, default 20),only_uncategorized(defaulttrue) andinclude_ai(defaultfalse; pattern layers only unless set). - Rename the counterparty in bulk:
PATCH /api/v1/transactions/batch/counterpartywith{"transaction_ids": [...], "counterparty": "Acme Corp", "remember": true}.rememberalso saves the rename for the business's other rows under the same name. - Edit many rows, a different change each:
PATCH /api/v1/transactions/batchtakes up to 500rowsof{transaction_id, description (or memo), ledger_id, counterparty, review_status}plusrememberanddry_run. One invalid row rejects the whole request; write-time failures are reported per row. A description edit keeps the counterparty and marks the row hand-edited so bank syncs do not overwrite it. Through the assistant or MCP,bulk_edit_transactionsalso accepts an uploaded file (s3_key) instead of rows, matching bytransaction_idor date + amount (up to 2,000 rows) and applying what matches. - Split across categories:
PATCH /api/v1/transactions/{transaction_id}/split:
curl -X PATCH "https://api.ondayzero.com/api/v1/transactions/{transaction_id}/split" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"split": [
{ "ledger_id": "MEALS_LEDGER_UUID", "allocation_type": "percent", "value": 7000 },
{ "ledger_id": "TRAVEL_LEDGER_UUID", "allocation_type": "remainder" }
]
}'allocation_type is percent (value in basis points — 7000 = 70%), amount (cents) or remainder. Each leg may carry a project_id.
If you do need a hand-built journal entry (for a transaction with no bank feed, an accrual, a correction), see Journal Entries.
Review and Reconcile
PATCH /api/v1/transactions/batch/review-statuswith{"transaction_ids": [...]}marks those transactions reviewed (it only moves towardreviewed; usePUTto set a row back tounreviewed);POST /api/v1/transactions/batch/mark-reviewed-by-periodwith{"period_start": "2026-03-01", "period_end": "2026-03-31"}does it for every unreviewed transaction in a period and returnsmarked_count.PATCH /api/v1/transactions/batch/reconciliation-statuswith{"transaction_ids": [...], "reconciled": true}setsreconciledin bulk (falseclears it).PUT /api/v1/transactions/{transaction_id}updates a single transaction'scounterparty(add"remember": trueto save it as a business-wide rename),description,review_status,reconciled,personalflag, and so on. A field the request does not list (for examplecounterparty_nameorvendor) is rejected with 400, never ignored.GET /api/v1/transactions/{transaction_id}/suggested-matcheslists invoices and bills this transaction probably pays; link them from the Invoices and Bills payment endpoints.
Delete and Restore
DELETE /api/v1/transactions/{transaction_id} hides a Plaid- or Xero-synced transaction on the DayZero ledger engine (soft delete; POST …/restore brings it back) and permanently removes a manual or CSV-uploaded one. Other integration sources are rejected with 400; check can_be_deleted on the transaction first. POST /api/v1/transactions/batch/delete with {"transaction_ids": [...]} applies the same rules per ID and returns deleted, hidden and a failed list. DELETE /api/v1/transactions/by-date-range?start_date=…&end_date=… clears an imported range — preview with GET …/by-date-range/preview first.
The date-range delete is permanent. The preview reports the transactions and journal entries that would go, reconciled rows, severed invoice/bill payment links, per-ledger counts and any closed_period_conflict. Add ledger_id to either call to limit it to one bank account.
Receipts
PATCH /api/v1/transactions/{transaction_id}/attachmentuploads a receipt as multipart form data (file: PDF, PNG, JPG, JPEG, WEBP, HEIC or HEIF). The new entry is the last item ofmeta.attachmentsand carries anid.DELETE …/attachments/{attachment_id}removes it,POST …/attachments/{attachment_id}/extractreads the merchant, totals and line items from it, andPOST …/attachments/import-ramppulls the receipts a Ramp cardholder uploaded.GET /api/v1/transactions/receipt-policyandPATCH …/receipt-policy(admin) read and set the policy:enabled,threshold_cents(default 7500; expenses at or above it need a receipt),reminder_days(default 3) andmax_reminders(default 3).GET /api/v1/transactions/receipt-coverage?start_date=…&end_date=…returnsrequired_count,attached_count,requested_count,missing_count,exempt_count,coverage_pctand the outstanding amounts in cents. List the gaps withGET /api/v1/transactions?receipt_status=missing.PATCH /api/v1/transactions/batch/receipt-exemptwith{"transaction_ids": [...], "exempt": true, "reason": "Bank fee"}marks rows as needing no receipt.POST /api/v1/transactions/batch/receipt-requestsasks the client for receipts (up to 200 transactions; firm staff on a firm-managed business only).
Common errors
| Status | Code | Cause | Fix |
|---|---|---|---|
400 |
VALIDATION_570 |
A bulk upload has rows whose category names no ledger |
Fix the category names (the preview's row_errors lists the nearest ledger names), or send allow_unmatched_categories: true |
400 |
VALIDATION_615 |
A cursor from one sort_by replayed under another |
Drop cursor and start from the first page |
400 |
closed-period message | DELETE /api/v1/transactions/by-date-range overlaps a closed or locked accounting period |
Narrow the range, or reopen the period first. The preview's closed_period_conflict shows it beforehand |
400 |
— | PUT /api/v1/transactions/{transaction_id} with a field it does not list, or a delete on a source that cannot be deleted |
Send only listed fields; check can_be_deleted first |
422 |
— | cursor does not end in a valid UUID, or a body field has the wrong type (for example a dollar amount instead of cents) |
Pass next_cursor back exactly as received; send amounts as integer cents |