Catalog Items, Bank Deposits and Customer Statements
Three small route families that sit around the receivables workflow in Invoices:
| Family | Base path | What it does |
|---|---|---|
| Catalog items | /api/v1/catalog-items |
Reusable products and services (price, income account, billing rule) that invoice lines can reference |
| Bank deposits | /api/v1/bank-deposits |
Groups sales receipts held in Undeposited Funds into one bank deposit |
| Customer statements | /api/v1/customer-statements |
Builds an A/R statement for one customer and emails it |
Money is always in integer cents. Every request needs Authorization: Bearer dz_your_token_here and x-business-id: YOUR_BUSINESS_ID. Successful responses use the standard {"success": true, "data": …} envelope and errors use the error envelope. Lists are cursor-paginated (see Pagination).
Permissions. GET needs read, POST and PUT need write, and DELETE needs delete. Write calls (including emailing a statement) are also refused with 403 when the business's trial has ended and it has no active subscription. None of the three families needs an add-on.
Catalog Items
A catalog item is an accounting line definition: a name, an optional default price, and the revenue and expense accounts it posts to. It is not an inventory product. Inventory products and variants (see Inventory) track stock, locations and cost lots. Catalog items never touch stock.
item_type |
Meaning |
|---|---|
service |
A service you sell (default) |
non_inventory |
A non-stocked item such as shipping |
inventory |
Labelled as inventory, but still a catalog line with no stock tracking |
bundle |
A package that expands into component catalog items |
The type is a classification; only bundle changes behavior (it can have components).
Create a catalog item
curl -X POST "https://api.ondayzero.com/api/v1/catalog-items" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"name": "Consulting",
"item_type": "service",
"sku": "CONSULT-HR",
"description": "Hourly consulting",
"billing_rule": "per_unit",
"unit_label": "hour",
"unit_price": 15000,
"taxable": false,
"income_ledger_id": "019ab37c-c057-7000-8000-0000000000a1"
}'Returns 201 with the item in data:
{
"success": true,
"data": {
"id": "019ab37c-c057-7000-8000-0000000000b1",
"business_id": "YOUR_BUSINESS_ID",
"name": "Consulting",
"item_type": "service",
"billing_rule": "per_unit",
"unit_label": "hour",
"sku": "CONSULT-HR",
"description": "Hourly consulting",
"unit_price": 15000.0,
"purchase_cost": null,
"is_sold": true,
"is_purchased": false,
"taxable": false,
"income_ledger_id": "019ab37c-c057-7000-8000-0000000000a1",
"expense_ledger_id": null,
"income_ledger_name": "Consulting Income",
"expense_ledger_name": null,
"is_active": true,
"created_at": "2026-10-11T14:00:00Z",
"updated_at": "2026-10-11T14:00:00Z"
}
}| Field | Notes |
|---|---|
name |
Required, 1-255 characters. Must be unique (case-insensitive) among active items, otherwise 409 |
item_type |
service (default), non_inventory, inventory, bundle |
billing_rule |
flat (default), per_unit, recurring, usage_metered, prepaid_block. Used by the order-to-cash billing engine; see Order to Cash |
unit_label |
Unit-of-measure text such as hour (max 50) |
unit_price |
Default sales price in cents, >= 0. May carry up to two decimal places of a cent for sub-cent rates (5.25 is $0.0525); responses return it as a number |
purchase_cost |
Default purchase cost in whole cents |
is_sold / is_purchased |
Availability flags; default true / false |
taxable |
Taxable by default; default false |
income_ledger_id |
Revenue ledger the item posts to. Must be a revenue ledger of this business |
expense_ledger_id |
Expense ledger used on purchases. Must be an expense ledger of this business |
sku |
Optional, max 100 characters. Not required to be unique |
A ledger from another business returns 404; a ledger of the wrong type returns 400.
List and fetch
curl "https://api.ondayzero.com/api/v1/catalog-items?item_type=service&search=consult&limit=50" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"Filters: active_only (default true), item_type (one of the four values above; anything else is an error, so validate it client-side), search (case-insensitive substring of name or SKU), plus the standard cursor, limit, direction, sort_by and descending parameters. With no sort_by the list is ordered by name ascending; when you pass sort_by, descending defaults to true. GET /api/v1/catalog-items/{item_id} fetches one item; an unknown or foreign ID returns 404.
Update and deactivate
PUT /api/v1/catalog-items/{item_id} takes any subset of the create fields plus is_active. Renaming to the name of another active item returns 409. Because PUT only changes fields you send, set "is_active": true to reactivate an item.
DELETE /api/v1/catalog-items/{item_id} is a soft delete: it sets is_active to false and returns {"success": true, "message": "Item deactivated."}. The item stays retrievable by ID and existing records keep their reference. Inactive items are hidden from the default list (active_only=true) and cannot be used on new invoice lines.
Bundles
A bundle's components are managed as one set. GET /api/v1/catalog-items/{bundle_id}/components returns the exploded list, and PUT replaces it wholesale:
curl -X PUT "https://api.ondayzero.com/api/v1/catalog-items/{bundle_id}/components" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{"components": [
{"component_item_id": "019ab37c-c057-7000-8000-0000000000b1", "qty": 2},
{"component_item_id": "019ab37c-c057-7000-8000-0000000000b2", "qty": 1}
]}'qty must be greater than zero (default 1, fractions allowed). Each response row has component_item_id, name, sku, unit_price, billing_rule and qty. Both calls return 400 when the target is not a bundle; PUT also returns 400 if a component is itself a bundle or the bundle itself, and 404 for an unknown component. An empty components array clears the bundle.
Using a catalog item on an invoice
Reference the item from an invoice line with catalog_item_id instead of a variant_id:
{ "catalog_item_id": "019ab37c-c057-7000-8000-0000000000b1", "quantity": 3 }- The line price is the item's
unit_price; send an integer-centunit_priceon the line to override it. If neither exists the request is rejected. - The line description is the one you send, otherwise the item's
name. The item's owndescriptionis not copied. quantitydefaults to1.- The stored line uses the
customvariant sentinel and keepscatalog_item_idfor traceability, so no inventory is deducted and no COGS is posted. To move stock, use a realvariant_id. If a line has both a realvariant_idand acatalog_item_id, the variant wins and the catalog item is ignored. - A missing, inactive or other-business item returns
404.
catalog_item_id is also accepted on order lines, contract lines and time entries.
Bank Deposits
A bank deposit mirrors what lands on the bank statement: it moves posted sales receipts out of Undeposited Funds and into a bank account in one journal entry. The deposit posts immediately:
Dr <deposit_to_ledger_id> total
Cr Undeposited Funds sum of the grouped receipts
Cr <from_ledger_id> each ad-hoc lineSales receipts are parked. The app, Regent and the MCP server no longer offer sales receipts to create. Their REST routes (
/api/v1/sales-receipts: list, create, get and void) still work for existing data. Receipts recorded earlier that still sit in Undeposited Funds can be deposited here. If there are none, the queue is empty and a deposit can only be built fromother_lines.
The undeposited queue
curl "https://api.ondayzero.com/api/v1/bank-deposits/undeposited" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"Lists every posted sales receipt whose deposit account is Undeposited Funds and that is not yet in a deposit, oldest first, with the running total in cents:
{
"success": true,
"data": {
"items": [
{
"receipt_id": "019ab37c-c057-7000-8000-0000000000c1",
"number": "SR-00001",
"receipt_date": "2026-10-09",
"customer_name": "Acme Corp",
"total": 45000,
"deposit_to_ledger_name": "Undeposited Funds"
}
],
"total": 45000
}
}This list is not paginated. A sales receipt only lands in the queue when it was created with the Undeposited Funds ledger as its deposit_to_ledger_id.
Create a deposit
curl -X POST "https://api.ondayzero.com/api/v1/bank-deposits" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"deposit_to_ledger_id": "019ab37c-c057-7000-8000-0000000000d1",
"deposit_date": "2026-10-11",
"sales_receipt_ids": ["019ab37c-c057-7000-8000-0000000000c1"],
"other_lines": [
{
"from_ledger_id": "019ab37c-c057-7000-8000-0000000000e1",
"description": "Interest",
"amount": 1200
}
],
"memo": "Friday deposit"
}'| Field | Notes |
|---|---|
deposit_to_ledger_id |
Required. The bank or cash account receiving the money; must be an asset ledger of this business |
sales_receipt_ids |
Receipts from the undeposited queue. Each must be posted, in Undeposited Funds and not already in a deposit |
other_lines |
Ad-hoc lines, each with from_ledger_id (account credited, any ledger of this business), amount (cents, > 0) and optional description |
deposit_date |
Defaults to today (UTC) |
number |
Optional, max 100. Defaults to DEP-00001, DEP-00002, … |
memo, currency |
Optional note; currency defaults to USD |
Returns 201 with the deposit. total is the receipts plus the other lines, undeposited_total is the receipts only, journal_entry_id points at the posted entry, and sales_receipt_ids echoes the grouped receipts:
{
"success": true,
"data": {
"id": "019ab37c-c057-7000-8000-0000000000f1",
"number": "DEP-00001",
"status": "posted",
"deposit_date": "2026-10-11",
"deposit_to_ledger_id": "019ab37c-c057-7000-8000-0000000000d1",
"deposit_to_ledger_name": "Operating Checking",
"sales_receipt_ids": ["019ab37c-c057-7000-8000-0000000000c1"],
"other_lines": [
{"from_ledger_id": "019ab37c-c057-7000-8000-0000000000e1", "description": "Interest", "amount": 1200}
],
"undeposited_total": 45000,
"total": 46200,
"memo": "Friday deposit",
"currency": "USD",
"journal_entry_id": "019ab37c-c057-7000-8000-000000000101",
"void_journal_entry_id": null,
"created_at": "2026-10-11T14:00:00Z",
"updated_at": "2026-10-11T14:00:00Z"
}
}Errors (all 400 unless noted): a bank account that is not an asset ledger; a receipt that is unknown, not posted, already deposited or not in Undeposited Funds; a deposit with no receipts and no lines (or a zero total). An unknown ledger ID returns 404.
List, fetch and void
GET /api/v1/bank-deposits?status=posted lists deposits, filtered by status (posted or void; other values are an error) with the standard cursor parameters. GET /api/v1/bank-deposits/{deposit_id} fetches one (404 if not found).
curl -X POST "https://api.ondayzero.com/api/v1/bank-deposits/{deposit_id}/void" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"Voiding posts a reversing journal entry (void_journal_entry_id), sets status to void, and returns the grouped receipts to the undeposited queue. Voiding a voided deposit returns 400. There is no edit endpoint: void and re-create to change a deposit. Voiding is a POST, so it needs write permission, not delete.
Customer Statements
A statement is computed on demand from one customer's invoices, payments and applied credit memos. Nothing is stored and the ledger is never changed.
Build a statement
curl "https://api.ondayzero.com/api/v1/customer-statements/{customer_id}?start_date=2026-07-01&end_date=2026-09-30" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"| Query parameter | Default | Meaning |
|---|---|---|
start_date |
one year before end_date |
First day of the period (YYYY-MM-DD) |
end_date |
today (UTC) | Last day of the period |
as_of_date |
end_date |
Date used to age open balances |
Both period dates are inclusive. An unknown customer, or one from another business, returns 404.
{
"success": true,
"data": {
"customer_id": "019ab37c-c057-7000-8000-000000000001",
"customer_name": "Acme Corp",
"customer_email": "ap@acme.example",
"customer_address": "1 Main St, Springfield",
"business_name": "Example LLC",
"start_date": "2026-07-01",
"end_date": "2026-09-30",
"as_of_date": "2026-09-30",
"opening_balance": 10000,
"lines": [
{"date": "2026-07-15", "type": "invoice", "reference": "INV-0042", "description": "Invoice", "charges": 250000, "payments": 0, "balance": 260000},
{"date": "2026-08-20", "type": "payment", "reference": "INV-0042", "description": "Payment", "charges": 0, "payments": 100000, "balance": 160000}
],
"total_charges": 250000,
"total_payments": 100000,
"closing_balance": 160000,
"aging": {"current": 0, "days_1_30": 0, "days_31_60": 150000, "days_61_90": 0, "days_91_plus": 0, "total": 150000}
}
}What is included:
- Invoices. Every invoice for the customer except
draft,voidanduncollectible, as achargesline of typeinvoice. The line is dated by the invoice's creation date, not its issue date. - Payments. Each recorded invoice payment, as a
paymentsline of typepayment. An invoice marked paid (manual_paidorstripe_paid) whose payments do not cover its total gets one synthetic payment line for the remainder, dated at its paid date, so it nets to zero. - Credit memos. Each credit memo application, as a
paymentsline of typecreditdescribed asCredit memo <number>. See Credit Memos. - Ordering and balance. Lines inside the period sort by date, with invoices before payments before credits on the same day, and
balanceis the running total starting fromopening_balance(everything dated beforestart_date). - Aging. Built from each open invoice's balance due and due date, relative to
as_of_date:current(not yet due),days_1_30,days_31_60,days_61_90,days_91_plus, and theirtotal. Aging reflects current balances, so it can differ fromclosing_balancewhen you pick a past period.
customer_email, customer_address and business_name are returned for rendering your own PDF or page. The endpoint does not validate that start_date is before end_date.
Email a statement
curl -X POST "https://api.ondayzero.com/api/v1/customer-statements/{customer_id}/send" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"start_date": "2026-07-01",
"end_date": "2026-09-30",
"custom_message": "Please remit the 31-60 day balance.",
"cc_email": "billing@yourcompany.example"
}'All body fields are optional: the three dates behave as above, custom_message is added to the email's closing lines, and cc_email adds a CC. The email goes to the customer's address on file and lists each statement line with opening, closing and amount-due figures. The response (status 201) is:
{"success": true, "data": {"status": "sent", "sent_to": "ap@acme.example", "closing_balance": 160000}}A customer with no email returns 400 ("Customer has no email address on file…"), and demo businesses return 403 because they cannot email real customers. There is no PDF attachment and no scheduling on this endpoint, and it does not record that a statement was sent.
Regent and MCP clients can read a statement with get_customer_statement (customer_id, optional start_date, end_date, as_of_date). No MCP tool emails it.
For the rest of the customer API (contacts, balances, bulk upload) see Customers.
Not covered here
The sales receipt routes themselves (/api/v1/sales-receipts); catalog-driven order and contract lines (Order to Cash); and inventory products and variants (Inventory).