{"openapi":"3.1.0","info":{"title":"POST /api/v1/transactions/bulk-upload/diff","version":"1.0.0","description":"Compare a bank/card export with a ledger"},"servers":[{"url":"https://api.ondayzero.com","description":"Production"}],"paths":{"/api/v1/transactions/bulk-upload/diff":{"post":{"tags":["transactions"],"summary":"Compare a bank/card export with a ledger","description":"Read-only. Compares an uploaded bank or credit-card export (CSV/Excel) with\nthe transactions one ledger already holds and reports which rows of the file\nare missing from the ledger — the rows to upload.\n\nEach file row is paired with at most one ledger transaction:\n\n- **exact** — same amount, date and description;\n- **strong** — same amount, dated within `date_tolerance_days` (default 3),\n  and the descriptions share a word or the counterparty agrees (catches\n  Plaid-synced and renamed rows);\n- **ambiguous** (`amount_only`) — same amount within the window, nothing else\n  agrees. Listed for review; never uploaded automatically.\n\nPairing is one-to-one, so two identical legitimate charges need two ledger\nrows. Amounts are compared in cash-flow sign (negative = money out): a\ncredit-card file's positive charge matches the stored charge.\n\nUpload the result with `POST /transactions/bulk-upload` and\n`only_missing_from_diff: true` (re-runs this diff), or pass\n`missing_row_numbers` (plus any ambiguous rows you checked) as\n`include_row_numbers`.","operationId":"ledger_diff_bulk_upload_export","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"x-business-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Business-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LedgerDiffRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope_LedgerDiffResponse_"}}}},"400":{"description":"Bad Request - Invalid input","content":{"application/json":{"example":{"error":"validation_error","message":"Validation failed: due_date: Input should be a valid date","errors":{"due_date":"Input should be a valid date"},"code":"GEN_002","request_id":"3f0e7c2a-9b4d-4e1a-8c6f-2d5b7a1e9c30"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized - Missing or expired token, an MCP OAuth token used against the REST API, or no access to this business","content":{"application/json":{"example":{"error":"unauthorized","message":"Authentication required","code":"AUTH_001","request_id":"3f0e7c2a-9b4d-4e1a-8c6f-2d5b7a1e9c30"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Forbidden - Insufficient permissions","content":{"application/json":{"example":{"error":"forbidden","message":"You don't have permission to access this resource","code":"AUTH_002","request_id":"3f0e7c2a-9b4d-4e1a-8c6f-2d5b7a1e9c30"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not Found - Resource does not exist","content":{"application/json":{"example":{"error":"not_found","message":"Resource not found","code":"NOT_FOUND_001","request_id":"3f0e7c2a-9b4d-4e1a-8c6f-2d5b7a1e9c30"},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"validation_error","message":"Validation failed: amount: Input should be a valid integer","errors":{"amount":"Input should be a valid integer"},"code":"GEN_002","request_id":"3f0e7c2a-9b4d-4e1a-8c6f-2d5b7a1e9c30"}}}}},"x-fastapi-operation-id":"ledger_diff_bulk_upload_export_api_v1_transactions_bulk_upload_diff_post"}}},"components":{"schemas":{"LedgerDiffExcludedRow":{"properties":{"row_number":{"type":"integer","title":"Row Number"},"date":{"type":"string","title":"Date"},"amount":{"type":"integer","title":"Amount"},"description":{"type":"string","title":"Description"},"status":{"type":"string","title":"Status"}},"type":"object","required":["row_number","date","amount","description","status"],"title":"LedgerDiffExcludedRow","description":"A file row left out by exclude_statuses."},"LedgerDiffExportRow":{"properties":{"row_number":{"type":"integer","title":"Row Number","description":"Spreadsheet row (header = 1)"},"date":{"type":"string","title":"Date"},"amount":{"type":"integer","title":"Amount","description":"Cents, cash-flow sign (negative = money out), as the import would store it"},"file_amount":{"type":"integer","title":"File Amount","description":"Cents exactly as the file signs it"},"description":{"type":"string","title":"Description"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"}},"type":"object","required":["row_number","date","amount","file_amount","description"],"title":"LedgerDiffExportRow","description":"One row of the file."},"LedgerDiffLedgerRow":{"properties":{"transaction_id":{"type":"string","title":"Transaction Id"},"date":{"type":"string","title":"Date"},"amount":{"type":"integer","title":"Amount","description":"Cents, cash-flow sign (negative = money out)"},"description":{"type":"string","title":"Description"},"counterparty":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Counterparty"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"}},"type":"object","required":["transaction_id","date","amount","description"],"title":"LedgerDiffLedgerRow","description":"One existing transaction of the ledger."},"LedgerDiffPair":{"properties":{"tier":{"type":"string","title":"Tier","description":"exact | strong | amount_only"},"row_number":{"type":"integer","title":"Row Number"},"export":{"$ref":"#/components/schemas/LedgerDiffExportRow"},"ledger":{"$ref":"#/components/schemas/LedgerDiffLedgerRow"},"days_apart":{"type":"integer","title":"Days Apart"},"reason":{"type":"string","title":"Reason"}},"type":"object","required":["tier","row_number","export","ledger","days_apart","reason"],"title":"LedgerDiffPair","description":"A file row paired with a ledger transaction."},"LedgerDiffRequest":{"properties":{"s3_key":{"type":"string","minLength":1,"title":"S3 Key","description":"S3 key of the CSV/Excel export (from file upload or /bulk-upload/paste)"},"ledger_id":{"type":"string","title":"Ledger Id","description":"Ledger (bank account or credit card) to compare against"},"start_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Start Date","description":"Only compare file rows on or after this date; also bounds the ledger rows reported missing from the file. Defaults to the file's first date."},"end_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"End Date","description":"Only compare file rows on or before this date. Defaults to the file's last date."},"column_mapping":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Column Mapping","description":"Mapping from DayZero field names to CSV headers: 'date', 'amount', 'description', optional 'category' and 'counterparty'. Auto-detected when omitted (see /bulk-upload/headers)."},"date_tolerance_days":{"type":"integer","maximum":10.0,"minimum":0.0,"title":"Date Tolerance Days","description":"How many days a ledger date may differ from the file's and still match (bank vs posted date).","default":3},"status_column":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status Column","description":"Header of the file's settled/pending column. Auto-detected (status, transaction_status, state, posting_status) when omitted."},"exclude_statuses":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Exclude Statuses","description":"File rows whose status is one of these (case-insensitive, e.g. ['Pending']) are left out of the comparison and listed separately."},"invert_transaction_amounts":{"type":"boolean","title":"Invert Transaction Amounts","description":"The file signs amounts the opposite way from usual (bank: deposits positive; credit card: charges positive).","default":false}},"type":"object","required":["s3_key","ledger_id"],"title":"LedgerDiffRequest","description":"Compare an uploaded export with one ledger. Nothing is written."},"LedgerDiffResponse":{"properties":{"ledger_id":{"type":"string","title":"Ledger Id"},"ledger_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ledger Name"},"financial_account_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Financial Account Type"},"amount_sign":{"type":"string","title":"Amount Sign","description":"How every amount below is signed"},"credit_card_file_sign_flipped":{"type":"boolean","title":"Credit Card File Sign Flipped","description":"True for a credit-card ledger: the file's positive charges were read as money out"},"start_date":{"type":"string","title":"Start Date"},"end_date":{"type":"string","title":"End Date"},"date_tolerance_days":{"type":"integer","title":"Date Tolerance Days"},"summary":{"$ref":"#/components/schemas/LedgerDiffSummary"},"missing_row_numbers":{"items":{"type":"integer"},"type":"array","title":"Missing Row Numbers","description":"Rows to upload — pass as include_row_numbers, or upload with only_missing_from_diff=true"},"missing_from_ledger":{"items":{"$ref":"#/components/schemas/LedgerDiffExportRow"},"type":"array","title":"Missing From Ledger"},"ambiguous":{"items":{"$ref":"#/components/schemas/LedgerDiffPair"},"type":"array","title":"Ambiguous"},"missing_from_export":{"items":{"$ref":"#/components/schemas/LedgerDiffLedgerRow"},"type":"array","title":"Missing From Export"},"matched":{"items":{"$ref":"#/components/schemas/LedgerDiffPair"},"type":"array","title":"Matched"},"excluded_by_status":{"items":{"$ref":"#/components/schemas/LedgerDiffExcludedRow"},"type":"array","title":"Excluded By Status"},"invalid_row_numbers":{"items":{"type":"integer"},"type":"array","title":"Invalid Row Numbers"},"status_column":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status Column"}},"type":"object","required":["ledger_id","amount_sign","credit_card_file_sign_flipped","start_date","end_date","date_tolerance_days","summary","missing_row_numbers","missing_from_ledger","ambiguous","missing_from_export","matched","excluded_by_status","invalid_row_numbers"],"title":"LedgerDiffResponse","description":"Which rows of the export the ledger is missing, and the reverse."},"LedgerDiffSummary":{"properties":{"file_rows":{"type":"integer","title":"File Rows","description":"Data rows in the file (header excluded)"},"compared_rows":{"type":"integer","title":"Compared Rows","description":"File rows compared with the ledger"},"invalid_rows":{"type":"integer","title":"Invalid Rows","description":"Rows with no usable date or amount"},"filtered_by_date":{"type":"integer","title":"Filtered By Date","description":"Rows outside start_date/end_date"},"excluded_by_status":{"type":"integer","title":"Excluded By Status","description":"Rows dropped by exclude_statuses"},"ledger_transactions":{"type":"integer","title":"Ledger Transactions","description":"Ledger transactions dated inside the window"},"matched_exact":{"type":"integer","title":"Matched Exact","description":"Same amount, date and description"},"matched_strong":{"type":"integer","title":"Matched Strong","description":"Same amount within the date tolerance and the text or counterparty agrees"},"ambiguous":{"type":"integer","title":"Ambiguous","description":"Same amount within the tolerance, nothing else agrees"},"missing_from_ledger":{"type":"integer","title":"Missing From Ledger","description":"File rows the ledger does not have — what to upload"},"missing_from_export":{"type":"integer","title":"Missing From Export","description":"Ledger transactions in the window the file does not have"}},"type":"object","required":["file_rows","compared_rows","invalid_rows","filtered_by_date","excluded_by_status","ledger_transactions","matched_exact","matched_strong","ambiguous","missing_from_ledger","missing_from_export"],"title":"LedgerDiffSummary","description":"Counts for one diff."},"SuccessEnvelope_LedgerDiffResponse_":{"properties":{"success":{"type":"boolean","title":"Success","default":true},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"},"data":{"anyOf":[{"$ref":"#/components/schemas/LedgerDiffResponse"},{"type":"null"}]}},"additionalProperties":true,"type":"object","title":"SuccessEnvelope[LedgerDiffResponse]"},"ErrorResponse":{"title":"ErrorResponse","type":"object","description":"Envelope returned by every non-2xx response. Branch on `code` (stable) rather than `message` (human-readable, may change).","required":["error","message","code"],"properties":{"error":{"type":"string","description":"Error category: `validation_error`, `unauthorized`, `forbidden`, `not_found`, `conflict`, `rate_limited`, `server_error`, or `service_unavailable`."},"message":{"type":"string","description":"Human-readable explanation, safe to show to end users."},"code":{"type":"string","description":"Stable machine-readable code in `CATEGORY_NNN` form (e.g. `NOT_FOUND_006`, `AUTH_010`, `GEN_002`)."},"request_id":{"type":"string","description":"Correlation id for support requests. Echoes the `x-request-id` request header when one was supplied."},"errors":{"type":"object","additionalProperties":{"type":"string"},"description":"Field-level validation messages keyed by field name. Present on 400/422 validation failures only."}}}},"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Token","description":"API token authentication. Format: `Bearer dz_...`"}}},"security":[{"BearerAuth":[]}]}