Bills
Bills represent accounts payable — money your business owes to vendors for goods or services received.
Bill Status Flow
draft → forecasted (approved) → received → partially_paid → paid
↘ pending_approval ──────↗ ↘ canceled| Status | Description |
|---|---|
draft |
Editable, not yet approved. Not in the general ledger |
pending_approval |
Submitted for AP approval (when the approval workflow is enabled). Posts to the ledger only once approved and received |
forecasted |
Approved, awaiting receipt |
received |
Goods/services received; the AP journal entry has been posted |
partially_paid |
Some payment linked |
paid |
Fully paid via linked bank transaction(s) |
canceled |
Cancelled |
Create a Bill
curl -X POST "https://api.ondayzero.com/api/v1/bills" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"vendor_id": "019ab37c-ae0d-7000-8000-000000000001",
"description": "March office supplies",
"type": "one_time",
"expected_amount": 45000,
"expected_paid_on_date": "2026-05-01",
"bill_number": "INV-2026-0311"
}'
expected_amountis in cents.45000= $450.00.typeisone_timeorrecurring.
Optional: currency, s3_key (an uploaded document to attach — see File Uploads), recurring (schedule — see Recurring Templates), payment_term_id (see below), and submitted_for_review: true to flag a client-submitted expense for the assigned advisor.
Payment term on a bill
payment_term_id records the term the vendor billed under (Net 30, 2/10 Net 30 …). POST /api/v1/bills, POST /api/v1/bills/received and PUT /api/v1/bills/{bill_id} all accept it, and bill responses return it.
- It is for reference: it never sets or moves the due date. Send
due_on(orexpected_paid_on_date) yourself. - It must be a term of this business, otherwise
404(NOT_FOUND_001). A deactivated term is still accepted. - On
PUT,nullclears it. It stays editable on any bill that isn'tcanceled, including paid ones. - An early-payment discount on the term is planning only. It feeds the
discountband of the pay plan while the window is open, but DayZero never takes it off the bill'samountor its AP journal entry. If you pay early at the discounted amount, the rest of the bill stays open until you lower the bill'samountor apply a vendor credit.
The Payment Terms page is switched off in the DayZero app for now and new businesses start with no terms, so create or seed them through the Payment Terms API before using this field.
Shortcut: create directly as received
If the bill has already arrived, skip the approve → receive steps and post it in one call:
curl -X POST "https://api.ondayzero.com/api/v1/bills/received" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"vendor_id": "019ab37c-ae0d-7000-8000-000000000001",
"amount": 45000,
"ledger_id": "EXPENSE_LEDGER_UUID",
"received_on": "2026-03-20",
"due_on": "2026-04-30",
"bill_number": "INV-2026-0311",
"description": "March office supplies"
}'Only vendor_id and amount are required (amount must be greater than 0). Pass line_items ([{amount, description, ledger_id}]) instead of a single ledger_id to split the expense across accounts; the line amounts must add up to amount. received_on defaults to now, journal_entry_date defaults to received_on, and type defaults to one_time.
AI extraction from documents
Upload the vendor's PDF first (POST /api/v1/bills/upload, multipart file, returns an s3_key), then:
curl -X POST "https://api.ondayzero.com/api/v1/bills/ai-create" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"single_bill_s3_keys": ["uploaded-file-key.pdf"]
}'multi_bill_s3_key handles one file containing several bills; pasted_text extracts from raw text. Extracted bills are created as drafts for review.
Approve a Bill
Draft bills must be approved before they can be received or paid:
curl -X PATCH "https://api.ondayzero.com/api/v1/bills/{bill_id}/approval" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"This moves draft → forecasted; any other starting status returns 400. Businesses with the AP approval workflow enabled route a draft into approval with POST /api/v1/bills/{bill_id}/submit-for-approval instead, which moves it to pending_approval until an approver signs off. Both that call and POST /api/v1/bills/{bill_id}/submit-for-ap-approval (which re-routes an already forecasted bill into approval) take the same body as the receipt call below (amount and ledger_id required) and return 400 if the approval workflow is not enabled. Callers with admin permission can unstick a pending_approval bill with POST /api/v1/bills/{bill_id}/resolve-approval ({"action": "approve" | "reject", "ledger_id": ..., "reason": ...}), and POST /api/v1/bills/{bill_id}/revert-to-draft moves a forecasted, pending_approval or received bill back to draft for editing (refused if a payment or credit is applied, or the bill's period is closed).
Mark as Received
When goods or services are received, record the final amount and the expense account:
curl -X PATCH "https://api.ondayzero.com/api/v1/bills/{bill_id}/receipt" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"amount": 45000,
"ledger_id": "EXPENSE_LEDGER_UUID",
"received_on": "2026-03-20",
"due_on": "2026-04-30"
}'This posts the accounts payable journal entry (expense debit, AP credit). amount and ledger_id are required; journal_entry_date defaults to received_on. The bill must be forecasted (or pending_approval with approval granted) — receiving a draft returns 400.
Record a Payment
curl -X POST "https://api.ondayzero.com/api/v1/bills/{bill_id}/payments" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"transaction_id": "BANK_TRANSACTION_UUID",
"amount": 45000,
"paid_on": "2026-04-28"
}'Only received and partially_paid bills can take a payment. A partial amount moves the bill to partially_paid; when payments cover the full balance the status becomes paid. A bill on payment hold refuses the payment unless you send override_hold: true with an override_reason (up to 500 characters). GET /api/v1/bills/{bill_id}/payments lists them and DELETE …/payments/{payment_id} unlinks one. To split a single bank transaction across several bills (and optionally book the remainder straight to expense), use POST /api/v1/bills/payments/batch:
curl -X POST "https://api.ondayzero.com/api/v1/bills/payments/batch" -H "Authorization: Bearer dz_your_token_here" -H "x-business-id: YOUR_BUSINESS_ID" -H "Content-Type: application/json" -d '{
"transaction_id": "BANK_TRANSACTION_UUID",
"allocations": [
{"bill_id": "BILL_UUID_1", "amount": 30000},
{"bill_id": "BILL_UUID_2", "amount": 15000}
],
"expense_lines": [{"ledger_id": "EXPENSE_LEDGER_UUID", "amount": 500, "description": "Bank fee"}],
"paid_on": "2026-04-28T00:00:00"
}'The response gives total_allocated, payment_count and the payments created. override_hold / override_reason work as on a single payment.
PATCH /api/v1/bills/{bill_id}/payment-initiation with {"initiated": true} (optionally initiated_on) flags a bill whose payment has been sent but hasn't cleared the bank yet; linking the bank payment clears the flag.
Payment holds
POST /api/v1/bills/{bill_id}/hold with {"reason": "Do not pay until deposit confirmed"} records a "do not pay" instruction without changing the bill's status (paid and canceled bills cannot be held). POST /api/v1/bills/{bill_id}/release-hold (optional {"note": "..."}) lifts it. Filter held bills with on_hold=true on the list endpoint.
List Bills
curl "https://api.ondayzero.com/api/v1/bills?status=received&sort_by=expected_paid_on_date&descending=false" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"Filter Parameters
| Parameter | Type | Description |
|---|---|---|
id |
UUID (repeatable) | Fetch a known set of bills in one request |
status |
string | draft, pending_approval, forecasted, received, partially_paid, paid, canceled |
vendor_id |
UUID | Filter by vendor |
vendor_search |
string | Fuzzy vendor-name match. Disables cursor pagination and returns at most 50 bills |
bill_number |
string | Bill number (case-insensitive partial match) |
search |
string | Search bill number, description or vendor name (case-insensitive partial match) |
payment_method |
string | Wire Transfer, ACH, Check or Credit Card |
journal_entry_id |
UUID | Bills linked to this journal entry |
on_hold |
boolean | true for bills on payment hold, false for the rest |
due_date_from / due_date_to |
date | Inclusive due-date range (YYYY-MM-DD), applied to the due date or, when none, the expected payment date |
Plus the standard pagination parameters. GET /api/v1/bills/aging returns the AP aging summary (vendors, bills, totals and held_total_cents). GET /api/v1/bills/metrics returns headline totals and GET /api/v1/bills/duplicate-check (query bill_number, vendor_id, amount, received_on) returns possible duplicates of a bill you are about to enter; it is advisory and never blocks creation. To create many bills from an uploaded CSV use POST /api/v1/bills/bulk (s3_key, plus approve for forecasted or create_as_received for received; otherwise rows are created as draft).
Pay plan
GET /api/v1/bills/pay-plan ranks the bills worth paying next. It is read-only, deterministic (no AI call) and dated against today:
curl "https://api.ondayzero.com/api/v1/bills/pay-plan" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"{
"success": true,
"data": {
"as_of": "2026-10-14",
"window_days": 7,
"recommendations": [
{
"bill_id": "019ab37c-b111-7000-8000-000000000001",
"bill_number": "INV-1042",
"vendor_id": "019ab37c-ae0d-7000-8000-000000000001",
"vendor_name": "Acme Hosting",
"amount": 125000,
"original_amount": 125000,
"due_date": "2026-10-09",
"days_until_due": -5,
"band": "overdue",
"recommended": true,
"rank": 1,
"priority_score": 1.0,
"reason": "5 days late (due Oct 9)",
"discount_cents": null,
"discount_deadline": null
}
],
"total_ranked": 1,
"cash_available_cents": 900000,
"cash_accounts": 2,
"projected_balance": 775000,
"pay_now": {"count": 1, "cents": 125000},
"later": {"count": 0, "cents": 0},
"on_hold": {"count": 0, "cents": 0},
"in_flight": {"count": 0, "cents": 0},
"awaiting_approval": {"count": 0, "cents": 0},
"drafts": {"count": 0, "cents": 0},
"discounts_available_cents": 0,
"held_bills_skipped": 0
}
}Only received and partially_paid bills with a balance are ranked, and amount is the balance due in cents. Each falls in one band, in this order: overdue (past due, longest late first), discount (an early-pay discount from the bill's payment term closes inside the window, soonest deadline first), due_soon (due within window_days, which is 7), later, then no_due_date. recommended is true for the first three bands, projected_balance is cash_available_cents minus everything recommended, and recommendations holds the first 250 of total_ranked.
Bills that cannot be paid yet are counted, not ranked: on_hold (payment hold), in_flight (payment initiated, waiting for the bank transaction), awaiting_approval (pending_approval) and drafts. cash_available_cents is null when the business has no bank account. POST /api/v1/bills/suggest-payment-priority returns the same response and exists for older clients. To build a dated pay list from your cash, see AP Workflows.
Upcoming (expected) bills
Forecast rows for bills you expect but have not received, such as a quarterly insurance premium or a contractor retainer. They are estimates: no journal entry, and they stay out of AP totals, aged payables and the balance sheet.
curl -X POST "https://api.ondayzero.com/api/v1/bills/upcoming" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"vendor_id": "019ab37c-ae0d-7000-8000-000000000001",
"description": "Quarterly liability insurance premium",
"estimated_amount": 180000,
"expected_date": "2026-11-01",
"recurrence": {"frequency": "quarterly", "day_of_month": 1, "occurrences": 4}
}'estimated_amount (cents) and expected_date are required. Name the vendor with vendor_id or a free-text vendor_name. Without recurrence you get one row; with it, up to 24 occurrences (default 12) are created up front, frequency being daily, weekly, biweekly, monthly, quarterly or annually. The response is the list of rows created (items, total, total_estimated_amount); each row has status expected, converted or dismissed, is_estimate: true and, for a series, a series_id.
GET /api/v1/bills/upcominglists them (filtersstatus,vendor_id,date_from,date_to,limit) andGET …/upcoming/{upcoming_bill_id}reads one.PATCH …/upcoming/{upcoming_bill_id}edits that occurrence only (estimated_amount,expected_date,description,notes, vendor, orstatusset toexpectedordismissed).DELETE …/upcoming/{upcoming_bill_id}removes it; add?series=trueto drop the later still-expected occurrences of the same series too. Converted occurrences are never touched.POST …/upcoming/{upcoming_bill_id}/convertcreates a draft bill from the estimate (optionalamount,due_date,bill_number,s3_key; defaults to the estimate) and marks the rowconverted.POST …/upcoming/{upcoming_bill_id}/linkwith{"bill_id": "…"}matches the row to a bill that already arrived, so the obligation is not counted twice.
Payment Suggestions
DayZero can automatically match bank transactions to open bills:
curl -X POST "https://api.ondayzero.com/api/v1/bills/payment-suggestions/trigger" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{"min_confidence": 0.8}'The optional body also takes bill_ids (restrict matching to those bills) and auto_match: true (link the confident matches immediately instead of leaving them as suggestions). The response counts suggestions_created, auto_linked and total_candidates.
Review them with GET /api/v1/bills/payment-suggestions, then POST …/payment-suggestions/{suggestion_id}/accept or /reject individually, or POST …/payment-suggestions/bulk-approve with {"min_confidence": 0.9} (default 0.85) to accept everything above a threshold. The list filters on bill_id, status (default pending), min_confidence, start_date and end_date.
Cancel or Delete a Bill
curl -X PATCH "https://api.ondayzero.com/api/v1/bills/{bill_id}/cancellation" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"Cancelling keeps the record with status canceled and posts reversing entries for any journal entries the bill created. Only draft, pending_approval, forecasted and received bills can be cancelled; a bill with payments linked (partially_paid or paid) cannot. DELETE /api/v1/bills/{bill_id} removes a draft, pending_approval, forecasted, received or canceled bill together with its journal entries, and is refused if vendor credits are applied to it. Add ?force=true to skip those status and vendor-credit checks (applied vendor credits are reversed); a bill whose journal entries sit in a closed accounting period still cannot be deleted.