Invoices
Invoices represent accounts receivable — money owed to your business by customers.
Invoice Status Flow
draft → open (finalized) → partially_paid → manual_paid / stripe_paid
↘ void / uncollectible| Status | Description |
|---|---|
draft |
Editable, not yet issued. No journal entry, nothing in Stripe |
open |
Finalized, awaiting payment |
partially_paid |
Some payment received |
stripe_paid |
Fully paid through the Stripe payment link |
manual_paid |
Fully paid via linked bank transaction(s) |
void |
Cancelled after finalization |
uncollectible |
Written off as bad debt |
Create an Invoice
curl -X POST "https://api.ondayzero.com/api/v1/invoices" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "019ab37c-c057-7000-8000-000000000001",
"due_date": "2026-11-01",
"description": "October retainer",
"line_items": [
{
"description": "Monthly retainer",
"quantity": 1,
"unit_price": 250000
},
{
"variant_id": "019ab37c-0a71-7000-8000-000000000001",
"quantity": 3
}
]
}'
unit_priceis in cents.250000= $2,500.00.
Line items come in three kinds:
- Custom — omit
variant_id(or send"custom");descriptionandunit_priceare required,quantitydefaults to1. - Catalog — pass a product
variant_id(or acatalog_item_id) and aquantity; the catalog price applies unless you sendunit_priceto override it. The price must be0or more. - Discount or credit — a custom line with a negative
unit_price, for example{"description": "Discount (10%)", "quantity": 1, "unit_price": -25000}. This is what the app's Discount control sends.
A discount line lowers the invoice total. It goes to Stripe as a negative invoice item, and the finalization journal entry credits Sales with the lines net of the discount. When sales tax applies, the discount also lowers the taxable amount, spread across the other lines in proportion to their taxable share (see Tax). The lines may add up to 0 (a 100% discount) but not less: a larger discount returns 400 VALIDATION_880. For money you owe the customer back, issue a credit memo instead.
Optional fields: issue_date (also accepted as invoice_date), invoice_number (otherwise assigned on finalize; must be unique within the business), currency (USD, CAD, AUD, EUR or GBP), project_id, fulfillment_location_id (warehouse to deduct inventory from; a line's location_id overrides it), historical: true (import an already-issued invoice without side effects), and recurring (see Recurring Templates). Line items also accept a note and a deferred tag.
Two more optional fields belong to payment terms and sales tax. The Payment Terms and Tax Settings pages are switched off in the DayZero app for now and new businesses start with no terms or tax rates, but both fields work through the API:
| Field | Description |
|---|---|
payment_term_id |
The term the invoice is issued under (Net 30, Due on Receipt …). Recorded for reference only: it does not set due_date, which you always send. Must be a term of this business, otherwise 404. See Payment Terms |
tax_exempt |
true charges no sales tax on this invoice. It only has an effect when the business has a default tax rate; without one no tax is charged anyway and the response returns "tax_exempt": false. See Tax |
due_date (YYYY-MM-DD) must be today or later and no more than 180 days out, unless historical is true, which allows a past date (the 180-day ceiling still applies).
Stripe requirement. An invoice you issue normally is created through the business's connected Stripe account; without one the call fails with 400 BUSINESS_007. To record an invoice without Stripe (a books migration, or a business that does not use Stripe), send "historical": true: it is written locally as a draft, never reaches Stripe or the customer, and can still be finalized locally.
The response data is the new invoice, as a draft:
{
"success": true,
"data": {
"id": "019ab37c-1a01-7000-8000-000000000001",
"status": "draft",
"number": null,
"currency": "USD",
"customer_id": "019ab37c-c057-7000-8000-000000000001",
"customer_name": "Acme Corporation",
"total": 250000,
"subtotal": 250000,
"tax_lines": [],
"tax_exempt": false,
"payment_term_id": null,
"total_paid": 0,
"balance_due": 250000,
"due_date": "2026-11-01",
"description": "October retainer",
"line_items": [
{
"variant_id": "custom",
"quantity": 1,
"description": "Monthly retainer",
"unit_price": 250000,
"amount": 250000
}
]
}
}subtotal is the sum of the line amounts, discount lines included. A draft carries no sales tax: tax is worked out at finalization, after which tax_total and tax_lines (one entry per rate) show it and total is subtotal plus tax_total. tax_total is null when a response did not load the tax lines. Field details are in Tax.
The invoice is created as a draft. Edit it with PUT /api/v1/invoices/{invoice_id} (send the fields you want to change plus id; line items can only be changed while the invoice is a draft, and a discount line is sent like any other line). PUT also accepts invoice_date, invoice_number (not on Stripe-issued invoices), contract_id (link or unlink the contract the invoice bills against; same customer required), revenue_treatment (earned or deferred, a label that posts no journal entry), payment_term_id (null clears it; it never moves due_date) and tax_exempt (drafts only; false charges the default rate again). DELETE /api/v1/invoices/{invoice_id} removes a draft, void or uncollectible invoice together with its journal entries; an open, partially_paid or paid invoice must be voided or written off first.
To create many invoices at once, use POST /api/v1/invoices/batch (an items array of customer_id, line_items and due_date, with optional finalize) or POST /api/v1/invoices/bulk (import an uploaded CSV by s3_key, with finalize, preview, allow_partial, allow_duplicates and historical options). POST /api/v1/invoices/drafts/bulk-delete deletes up to 500 drafts per call, by invoice_ids or all_drafts (with an optional due-date window and dry_run).
Finalize an Invoice
Draft invoices must be finalized before they can be sent or paid:
curl -X PUT "https://api.ondayzero.com/api/v1/invoices/{invoice_id}/finalize" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"Finalization moves the status to open, assigns the invoice number if it was blank, adds sales tax when the business has a default tax rate and the invoice is not tax_exempt (see Tax), posts the AR journal entry, and — when Stripe is connected — finalizes the Stripe invoice so a hosted payment link exists. Without a Stripe account the invoice is finalized locally. Only draft invoices can be finalized, and the accounting period for the invoice date must be open.
Send an Invoice
curl -X POST "https://api.ondayzero.com/api/v1/invoices/{invoice_id}/send" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{"attach_pdf": true, "cc_sender": false, "custom_message": "Thanks for your business!"}'The body is optional. The customer must have an email address (400 VALIDATION_178 otherwise). custom_message is limited to 2,000 characters, attach_pdf defaults to true (the PDF is only attached for an invoice that is finalized and linked to Stripe), and sending the same invoice again within a 5-minute cooldown returns 409 CONFLICT_022. The response has customer_email, cc_email, message_id and pdf_attached. GET /api/v1/invoices/{invoice_id}/pdf returns a fresh pdf_url and hosted_invoice_url (finalized, Stripe-linked invoices only; the URL expires, so fetch it when you need it); PUT /api/v1/invoices/{invoice_id}/delivered?delivered_on=2026-10-20 (the delivered_on query parameter is required) marks an invoice delivered when you send it yourself.
List Invoices
curl "https://api.ondayzero.com/api/v1/invoices?status=open&sort_by=due_date&descending=false&limit=50" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"Filters: status (repeatable, e.g. ?status=open&status=partially_paid, or status[]=), id (repeatable, to batch-fetch known invoices), customer_id, number (exact, case-insensitive), search (number, description, line-item description or customer name), due_date_from / due_date_to (inclusive), product_id / variant_id, plus the standard pagination parameters. sort_by accepts any invoice column (for example due_date, created_at or total). status also accepts two virtual values: overdue (past due and unpaid) and paid (matches both manual_paid and stripe_paid). GET /api/v1/invoices/aging returns the AR aging summary and GET /api/v1/invoices/metrics the headline numbers.
Stripe Payment Links
If Stripe is connected, finalized invoices carry a hosted_invoice_url for customers to pay online; GET /api/v1/invoices/{invoice_id}/payment-link returns it on demand. Payments made there are recorded automatically via Stripe webhooks and move the invoice to stripe_paid. POST /api/v1/invoices/{invoice_id}/stripe/sync re-checks Stripe if a payment seems to be missing.
Record a Manual Payment
Link a bank transaction to the invoice:
curl -X POST "https://api.ondayzero.com/api/v1/invoices/{invoice_id}/payments" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{"transaction_id": "TRANSACTION_UUID", "amount": 250000, "paid_on": "2026-10-28"}'Partial amounts move the invoice to partially_paid; paying the balance moves it to manual_paid. One transaction can be split across several invoices as long as the allocations do not exceed the transaction amount, and a payment cannot exceed the invoice's balance due. For a card-processor deposit that arrived net of fees, add fee_cents: the invoice is credited amount + fee_cents (or amount if it is already the gross) and the fee is expensed on the deposit's journal entry, which must already exist. Payments can be updated (PATCH …/payments/{payment_id}), removed (DELETE), or moved to another invoice (POST …/payments/{payment_id}/move). GET /api/v1/invoices/suggestions lists AI-matched transaction/invoice pairs you can accept or reject, and POST /api/v1/invoices/match runs the matcher on demand (body auto_match, default false; when true, matches at confidence 0.85 or higher are linked as payments straight away, otherwise matches are created as suggestions; optional min_confidence, default 0.50, and invoice_ids), and POST /api/v1/invoices/bulk-payments settles up to 500 invoices at once (invoices rows of invoice_id, optional paid_on and transactions; rows are applied independently and each gets a result).
If you don't have a transaction to link, PUT /api/v1/invoices/{invoice_id}/paid marks an open or partially_paid invoice manual_paid outright and posts an AR-relief journal entry. PUT …/unpaid reverses that for manual_paid invoices only; add ?unlink_payments=true to also unlink bank payments that cover the total.
Void an Invoice
curl -X PUT "https://api.ondayzero.com/api/v1/invoices/{invoice_id}/void" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"Voiding reverses the AR journal entry and voids the Stripe invoice if there is one. Only open invoices can be voided: drafts should be deleted instead, and paid invoices cannot be voided (remove the payments first, or write the balance off with PUT /api/v1/invoices/{invoice_id}/uncollectible, which works on open or partially_paid invoices and debits Bad Debt Expense). Voiding a partially_paid invoice is also refused.
Errors
| Status | code |
Cause |
|---|---|---|
400 |
BUSINESS_007 |
Creating a non-historical invoice with no Stripe account connected |
400 |
VALIDATION_017 / VALIDATION_018 |
A custom line item is missing unit_price (or amount) or description |
400 |
VALIDATION_880 |
The line items add up to less than 0 (a discount or credit line larger than the rest of the invoice). Lower the discount, or issue a credit memo |
400 |
GEN_002 |
A negative unit_price (or amount) on a catalog line (variant_id or catalog_item_id); only a custom line can be a discount |
400 |
BUSINESS_061 |
Changing line items, due_date, description or tax_exempt on an invoice that is no longer a draft |
404 |
NOT_FOUND_001 |
payment_term_id is not a term of this business |
400 |
validation_error |
due_date in the past (without historical), more than 180 days out, or not YYYY-MM-DD; the message names the problem and errors.due_date echoes the value |
409 |
CONFLICT_* |
invoice_number is already used by another invoice in the business |
400 |
BUSINESS_019 |
Finalizing an invoice that is not a draft. Finalizing, voiding or writing off an invoice whose journal entry would land in a closed accounting period is also refused with 400 |
400 |
BUSINESS_016 |
Voiding a draft (delete it instead) or any status other than open |
400 |
BUSINESS_068 |
Writing off an invoice that is not open/partially_paid, or has no balance |
400 |
BUSINESS_079 |
Deleting an open, partially_paid or paid invoice |
400 |
VALIDATION_008 |
PUT …/paid on an invoice that is not open/partially_paid |
400 |
VALIDATION_465 |
PUT …/unpaid on an invoice that is not manual_paid (a stripe_paid invoice must be refunded in Stripe) |
409 |
CONFLICT_022 |
Sending the same invoice again inside the 5-minute cooldown |
404 |
NOT_FOUND_006 |
Invoice not found in this business |