Skip to content

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

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

json
{
  "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

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

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

json
{ "catalog_item_id": "019ab37c-c057-7000-8000-0000000000b1", "quantity": 3 }
  • The line price is the item's unit_price; send an integer-cent unit_price on 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 own description is not copied.
  • quantity defaults to 1.
  • The stored line uses the custom variant sentinel and keeps catalog_item_id for traceability, so no inventory is deducted and no COGS is posted. To move stock, use a real variant_id. If a line has both a real variant_id and a catalog_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:

text
Dr  <deposit_to_ledger_id>   total
Cr  Undeposited Funds        sum of the grouped receipts
Cr  <from_ledger_id>         each ad-hoc line

Sales 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 from other_lines.

The undeposited queue

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

json
{
  "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

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

json
{
  "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).

bash
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

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

json
{
  "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, void and uncollectible, as a charges line of type invoice. The line is dated by the invoice's creation date, not its issue date.
  • Payments. Each recorded invoice payment, as a payments line of type payment. An invoice marked paid (manual_paid or stripe_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 payments line of type credit described as Credit 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 balance is the running total starting from opening_balance (everything dated before start_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 their total. Aging reflects current balances, so it can differ from closing_balance when 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

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

json
{"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).