Skip to content

Bills

Bills represent accounts payable — money your business owes to vendors for goods or services received.

Bill Status Flow

text
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

bash
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_amount is in cents. 45000 = $450.00. type is one_time or recurring.

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 (or expected_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, null clears it. It stays editable on any bill that isn't canceled, including paid ones.
  • An early-payment discount on the term is planning only. It feeds the discount band of the pay plan while the window is open, but DayZero never takes it off the bill's amount or its AP journal entry. If you pay early at the discounted amount, the rest of the bill stays open until you lower the bill's amount or 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:

bash
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:

bash
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:

bash
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:

bash
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

bash
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:

bash
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

bash
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:

bash
curl "https://api.ondayzero.com/api/v1/bills/pay-plan" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"
json
{
  "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.

bash
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/upcoming lists them (filters status, vendor_id, date_from, date_to, limit) and GET …/upcoming/{upcoming_bill_id} reads one.
  • PATCH …/upcoming/{upcoming_bill_id} edits that occurrence only (estimated_amount, expected_date, description, notes, vendor, or status set to expected or dismissed).
  • DELETE …/upcoming/{upcoming_bill_id} removes it; add ?series=true to drop the later still-expected occurrences of the same series too. Converted occurrences are never touched.
  • POST …/upcoming/{upcoming_bill_id}/convert creates a draft bill from the estimate (optional amount, due_date, bill_number, s3_key; defaults to the estimate) and marks the row converted.
  • POST …/upcoming/{upcoming_bill_id}/link with {"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:

bash
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

bash
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.