Reconciliation
Verify that your ledger balances match your bank statements. DayZero uses AI to parse statements and deterministic matching to suggest transaction pairs.
How It Works
- Upload a bank statement (PDF, CSV, Excel, or a PNG/JPG image) for a ledger and statement period, or reconcile against the synced bank feed
- AI parses the statement into structured transaction lines
- Matching engine pairs statement lines with ledger transactions in that period
- Review suggested matches — approve, reject, or match manually
- Complete the reconciliation to lock it and mark the transactions reconciled
| Status | Meaning |
|---|---|
pending |
Created, processing not started yet |
processing |
Parsing the statement and running matching |
ai_matched |
Suggestions ready for review |
in_review |
A user is approving/rejecting matches |
completed |
Locked; matched transactions flagged reconciled |
failed |
Processing failed — POST …/start retries it |
Matches can only be approved, rejected or completed while the status is ai_matched or in_review (otherwise 400). The first approval moves the reconciliation to in_review. POST …/start returns 400 for a reconciliation that is already processing or completed.
Create a Reconciliation
From File Upload
The ledger and statement period are query parameters; the file is the multipart body:
curl -X POST "https://api.ondayzero.com/api/v1/reconciliations/upload?ledger_id=BANK_LEDGER_UUID&start_date=2026-03-01&end_date=2026-03-31&opening_balance=1250000&closing_balance=1387550" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-F "file=@statement.pdf"ledger_id, start_date and end_date are required. opening_balance / closing_balance are in cents and let DayZero validate the reconciliation before completion; when the parser finds balances on the statement itself, those are stored too. Accepted file types are PDF, CSV, XLSX, XLS, PNG, JPG and JPEG, up to 250 MB (larger files are rejected with 400). Processing starts automatically unless you pass auto_start=false.
The ledger must be an asset account that is a bank or cash account (a bank-linked ledger, or a checking, savings or cash account); anything else is a 400. end_date before start_date is also a 400. The response is the reconciliation (id, ledger_id, ledger_name, source_type, status, status_display, statement_filename, statement_start_date, statement_end_date, opening_balance, closing_balance, locked, error_message, workflow_started).
From an Uploaded S3 Key
If the statement was already uploaded (see File Uploads):
curl -X POST "https://api.ondayzero.com/api/v1/reconciliations" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"ledger_id": "BANK_LEDGER_UUID",
"start_date": "2026-03-01",
"end_date": "2026-03-31",
"source_type": "manual",
"statement_s3_key": "statements/march-2026.pdf",
"statement_filename": "march-2026.pdf",
"opening_balance": 1250000,
"closing_balance": 1387550
}'source_type is manual (the default; uploaded statement, statement_s3_key required and must belong to your business) or plaid (reconcile against the synced bank feed for the period, no file needed). auto_start (default true) controls whether processing begins immediately.
Review Matches
Poll GET /api/v1/reconciliations/{reconciliation_id} until status is ai_matched. The response includes input_txns (statement_txns and ledger_txns), ai_match_results (matches with statement_txn_id, ledger_txn_ids, confidence and reasoning, plus unmatched_statement and unmatched_ledger), user_committed_matches and match_counts. Statement lines and ledger transactions carry id, date, description and amount (cents).
# Approve one match (one statement line ↔ one or more ledger transactions)
curl -X POST "https://api.ondayzero.com/api/v1/reconciliations/{reconciliation_id}/matches/approve" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{"statement_txn_id": "STATEMENT_LINE_ID", "ledger_txn_ids": ["TRANSACTION_UUID"]}'
# Approve everything the engine is confident about
curl -X POST "https://api.ondayzero.com/api/v1/reconciliations/{reconciliation_id}/matches/bulk-approve" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{"min_confidence": 0.9}'bulk-approve takes min_confidence (0.0-1.0, default 0.9) or statement_txn_ids (which overrides the threshold) and returns approved_count, skipped_count, approved and skipped. Approving with your own ledger_txn_ids creates a manual match; the ledger transactions must belong to the reconciliation's ledger.
POST …/matches/reject with {"statement_txn_id": "..."} and POST …/matches/bulk-reject with {"statement_txn_ids": [...]} remove approved (committed) matches so the lines can be matched again; they do not discard AI suggestions. POST …/explain-matches asks the AI to explain the suggested pairs (explanations with match_id, reasons and confidence).
Check the Balance
curl "https://api.ondayzero.com/api/v1/reconciliations/{reconciliation_id}/balance" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"Reports whether opening balance + net matched statement transactions = closing balance (within 1 cent). The response has balanced, balances_available, opening_balance, closing_balance, net_transactions, expected_closing and difference, all in cents. Without both an opening and a closing balance, balances_available is false and balanced is false.
GET …/balance-report ties the statement to the books instead: statement opening and closing, the ledger's book balance the day before the period and at period end (ledger_opening, ledger_closing), difference_cents (statement closing less ledger closing), and the statement and ledger rows still without a committed match (up to 500 each, truncated when there are more).
Complete a Reconciliation
After reviewing matches:
curl -X POST "https://api.ondayzero.com/api/v1/reconciliations/{reconciliation_id}/complete" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{"skip_balance_check": false}'Completion validates the balance (pass "skip_balance_check": true to override; the body is optional), locks the reconciliation, and marks matched transactions as reconciled. It returns 400 if the reconciliation is already completed, is not yet in review, or does not balance, including when no opening and closing balance were supplied. The response carries a message and code, not the reconciliation; fetch it with GET. GET …/pdf generates the reconciliation report on demand and streams it back as a PDF attachment (not JSON); it is only available once completed:
curl -o reconciliation.pdf "https://api.ondayzero.com/api/v1/reconciliations/{reconciliation_id}/pdf" -H "Authorization: Bearer dz_your_token_here" -H "x-business-id: YOUR_BUSINESS_ID"List Reconciliations
curl "https://api.ondayzero.com/api/v1/reconciliations?status=in_review&limit=20" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"This endpoint uses limit (1-100, default 50) / offset paging rather than cursors. status filters by the statuses above; pass include_total_count=true to also get total (more expensive). The response is items, limit, has_next, has_prev and, when requested, total.