Skip to content

CFO Portal

The CFO portal is a set of read-only "sections" that roll the books, open AR and AP, purchase orders, Shopify orders and budgets into leadership views, plus a planner that lets you shape the 13-week cash forecast. Every route lives under one prefix:

text
/api/v1/businesses/{business_id}/cfo-portal/...

This guide covers the data sections and the cash forecast planner. Settings (average order value, Shopify scope, excluded budget ledgers), managerial bucket and mapping management and the CFO projection scenarios are documented elsewhere, so this guide links to them instead of repeating them:

Topic Where
configuration, unit-economics/settings, cash-forecast/settings, po-forecast-settings, buckets, recipe-book/* writes, managerial key mappings Businesses
/cfo-projection scenarios Dashboards and AI Reporting
Budgets and the /budgets/{id} budget-versus-actual Budgets
Accounting periods, close score, sign-off Closing the Books

All examples use https://api.ondayzero.com, Authorization: Bearer dz_your_token_here and x-business-id: YOUR_BUSINESS_ID. Money is integer cents everywhere. The UI side of each section is described in the Knowledge Base articles whose names start with "CFO" (for example Weekly Pulse, Flux Analysis and Reorder Cash Planner).

Access and gating

CFO Advisory Suite add-on. Every route under /cfo-portal requires the CFO Advisory Suite add-on (add-on slug cfo_suite). Without it the API answers 403 with code BILLING_003. The gate is applied to the whole router, so reads are gated too. A business passes when any of these is true:

  • it is in an active trial, or is a demo workspace (every add-on is unlocked);
  • cfo_suite is in its paid add-on list;
  • it is on the grandfathered or partner-program plan and the add-on is in its grandfathered list (the partner-program plan always includes it);
  • it is a firm client and its firm's plan bundles it (Firm Professional and Firm Enterprise).

The gate is not applied to managerial-key-mappings, which is a separate router.

Active billing. POST, PUT, PATCH and DELETE also need an active trial or subscription: once a trial has ended with no subscription they return 403 BILLING_001, while reads keep working. Firm-client, grandfathered, partner-program and demo businesses are not blocked.

Permissions. The HTTP method decides the permission, as on every route: GET needs read, POST, PUT and PATCH need write, DELETE needs delete (a direct business member has read and write only). A read_only API token can call GET routes only. Two POST routes are read-only in effect but still need write: cash-forecast/demand-plan (it persists nothing) and ask.

Path ID must match the header. {business_id} in the path must equal x-business-id; a different ID is 403 AUTH_002 (send the same UUID twice). No route in this family takes a different business.

The section envelope

Every section is a GET that returns the same wrapper, described in Businesses: business_id, section, generated_at, optional start_date and end_date, sources (each key, label, status of available, empty, partial or unavailable, and message), data and, on statement-derived sections, basis. A few behaviors worth knowing before you build on it:

  • A failing section is still 200. If a section throws, you get empty data and one unavailable source with the message "This section is temporarily unavailable." Always check sources before trusting an empty data. HTTPExceptions (auth, validation) keep their real status.
  • basis is accrual or cash and follows the business's default accounting basis. There is no query parameter to change it. It is set on summary, statements, managerial-pl, flux and cash-forecast.
  • Short-lived cache. summary, statements, managerial-pl, benchmarks and valuation are cached for 180 seconds, and unit-economics, landed-cogs and shipping-analysis for 300 seconds. Writes to ledgers, transactions, journal entries, bills and invoices (and a change of the business's accounting basis) clear the first group. Shopify-derived sections rely on the TTL alone. Edits to managerial buckets or mappings are not wired to clear the managerial P&L cache, so allow up to three minutes before they show there. cash-forecast, configuration, monthly-close, alerts, ar-aging, ap-aging and the other sections are not cached.
  • Date parameters are YYYY-MM-DD. For the dated sections, send both start_date and end_date or neither; a lone date is ignored and the default window is used. vendor-spend is the exception: it accepts either date on its own and fills in the other from its default. An inverted range is a 400 VALIDATION_101 on statements, flux, budget-vs-actual, vendor-spend and board-deck.

Section reference

Section path Parameters Default window What data holds
summary include_series (default false), include_weekly_pl (default true) Current month to date KPIs, open AR and AP, cash, flags, 4-week P&L table, the weekly pulse
managerial-pl start_date, end_date Last 12 months to today P&L rebuilt on managerial buckets
statements start_date, end_date Last 3 months to today income_statement, balance_sheet, cash_flow
flux start_date, end_date Current calendar month vs the prior month Variances and material swings
budget-vs-actual budget_id, start_date, end_date Current month Budget versus actual by ledger and bucket
cash-forecast budget_id and four switches 13 weeks from this week's Sunday The forecast (see below)
ar-aging, ap-aging none Open items today Aging buckets by customer or vendor
unit-economics none Current month Per-order margin bridge
landed-cogs none All received and completed purchase orders Cost per unit by variant
shipping-analysis none Last 6 months Shipping by month
shopify none Last 6 months Sales by month, top products
vendor-spend start_date, end_date, group_by, sources Last 12 months Spend by vendor or month
purchase-orders status Latest 100 purchase orders Pipeline and commitments
valuation, benchmarks none Current month Illustrative valuation, reference metrics
alerts none Current state Alert rules and the breached ones
monthly-close none Latest 12 periods Close checklist and blockers
reports-documents none Up to 25 of each Generated reports and files
board-deck start_date, end_date Current month One-page board summary
recipe-book none Current state Chart of accounts with bucket mappings

debt-schedule is also a section, with its own write routes; see What is not covered.

Weekly pulse

The weekly pulse is the summary section. Without parameters it covers the current month to date; if the month has no posted income-statement activity yet, it silently uses the most recent month that does and explains this in data.period.note.

bash
curl "https://api.ondayzero.com/api/v1/businesses/YOUR_BUSINESS_ID/cfo-portal/summary?include_series=true" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

include_series=true loads the trailing 13 weeks of P&L and is what unlocks anomalies and the health scorecard; without it pulse.anomalies and pulse.health are empty. include_weekly_pl=false skips the 4-week table.

The main fields of data (trimmed):

json
{
  "period": {"start_date": "2026-10-01", "end_date": "2026-10-11", "note": null},
  "basis": "accrual",
  "kpis": [{"key": "revenue", "label": "Revenue", "value_cents": 4820000, "delta_pct": 12.4}],
  "open_ar_cents": 1530000,
  "open_ap_cents": 910000,
  "cash_cents": 6400000,
  "cash_source": "plaid",
  "flags": [{"severity": "warning", "label": "Overdue AP", "message": "$1,200.00 of bills are overdue.", "route": "/client/bills"}],
  "weekly_pl_rows": [{"label": "Revenue", "indent": false, "section": true, "weeks": [{"week": "Sep 15-21", "amount_cents": 1100000}]}],
  "pulse": {
    "headline": {"text": "...", "sources": []},
    "tone": "neutral",
    "kpis": [], "anomalies": [], "actions": [], "health": {}, "readiness": {"is_ready": true, "steps": []},
    "narration_status": "available",
    "weekly_series": {"revenue": [{"week_start": "2026-07-13", "week_end": "2026-07-19", "amount_cents": 1000000}]}
  }
}
  • kpis has six entries: revenue, gross_profit, net_income, cash, open_ap and shopify_sales. delta_pct compares with the prior month. cash_source is plaid when synced bank balances are used and ledger otherwise; cash_ledger_cents and cash_reconciliation_note appear when the two differ.
  • flags can contain "Overdue AP" (warning) and "Cash coverage" (critical, when cash is below AP due in the next 30 days).
  • pulse.weekly_series (only with include_series) holds revenue, cogs, gross_profit, opex and net_income, each a list of 13 {week_start, week_end, amount_cents}, oldest first.
  • pulse.anomalies flags the latest week of a bucket when it clears all three thresholds against the previous 12 weeks: absolute z-score of 2 or more, a move of at least $500, and at least 50 percent. Each has id, metric_key, current_cents, baseline_mean_cents, delta_cents, delta_pct, z_score, severity (critical, warning or info) and narrative.
  • pulse.health has runway_months (null when the business is not burning cash), ar_days, ap_days and *_status traffic lights (ok, warning, critical, info), plus books_status, books_close_state (closed, none or unavailable) and books_last_close_at (end date of the latest closed or locked accounting period).
  • pulse.actions is a ranked list of at most 12 (id, urgency, title, why, route, kind of anomaly, ap or flag).
  • pulse.readiness.is_ready is false only for an empty tenant; steps lists setup nudges. The AI narration is skipped for a tenant that is not ready. narration_status is available, fallback (deterministic text) or failed.

Cash flow forecast

bash
curl "https://api.ondayzero.com/api/v1/businesses/YOUR_BUSINESS_ID/cfo-portal/cash-forecast?include_unapproved_ap=true" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"
Query parameter Default Effect
budget_id none Budget that supplies planned revenue and expense. Unknown IDs fall back with a warning
apply_overrides false Fold the working override set into the cells. Leave off for the live view
include_projected_replenishment false Add reorder cash the demand-forecast engine expects, as projected_po_out_cents
include_unapproved_ap false Add pending_approval and forecasted bills (not raised from a PO) as an "awaiting approval" outflow. Drafts never count
net_bills_against_budget false Take open bills coded to a budgeted expense ledger off that ledger's budget (floor 0) so they are not counted twice

There is no weeks parameter on this route: the horizon is fixed at 13 weeks. Weeks start on Sunday; week 1 is the Sunday on or before today, and its numbers cover today onward. Everything already due before today lands in an Overdue row (week number 0 in overrides), which is deducted from or added to starting cash before week 1.

How each column is built

Week field Source
cash_in_cents Open invoices by due_date. Invoices with no due date are excluded and counted in unscheduled_ar_count and unscheduled_ar_cents
ap_out_cents Open bills by due (or expected payment) date. Bills with no date go to the Overdue row. Bills due after week 13 are rolled into week 13 and a warning says so
unapproved_ap_out_cents Only with include_unapproved_ap
po_out_cents Open purchase orders with no linked bill, by expected payment date (30 days after creation if unset), using the PO balance
projected_po_out_cents Only with include_projected_replenishment
debt_out_cents, debt_in_cents Scheduled loan payments you owe, and note-receivable repayments. A stored paydown table takes precedence over the cadence estimate
budget_revenue_cents, budget_expense_cents The selected budget, prorated by overlap days per month, or paid on a timing rule (below). Budget lines you excluded in settings are left out. These are part of running cash
one_time_cents One-time items whose applies_on falls in the week
ending_cash_cents Previous ending cash plus cash_in, debt_in, budget_revenue and one_time, minus ap_out, unapproved_ap_out, po_out, projected_po_out, debt_out and budget_expense

The budget used is the budget_id you pass, else the first active budget, then draft, then closed. budget_state_used and warnings tell you when a non-active budget was used. If your budget already covers revenue that open invoices also cover, exclude that ledger in the forecast settings to avoid double counting.

Other fields on data: starting_cash_cents, starting_cash_source (plaid or ledger) with starting_cash_ledger_cents and a reconciliation note, ending_cash_cents, lowest_cash_cents and lowest_cash_week, total_inflow_cents, total_outflow_cents, the overdue_*_cents totals, budget_options, all_budget_ledger_options, excluded_budget_ledger_ids, timing_applied, fixed_cost_timing_gap_cents, budget_netting (per ledger budget_cents, open_bills_cents, netted_cents, bill_ids) and the labels debt_outflow_label and inventory_outflow_label (set by the Recipe Book builder labels). Each week also carries *_overridden booleans that are true when the cell came from an override.

Timing overrides for fixed costs

A budgeted ledger with no timing row is spread over each month's weeks by overlap days. A ledger with a timing row instead pays that month's budget in lump sums on fixed days, lag_months after the month it accrues in (rent for September paid on the 1st of October, for example). The month's amount is split evenly across the pay days.

bash
curl -X PUT "https://api.ondayzero.com/api/v1/businesses/YOUR_BUSINESS_ID/cfo-portal/cash-forecast/timing" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{"rows": [{"ledger_id": "LEDGER_UUID", "lag_months": 1, "pay_days": [15, 31]}]}'
  • lag_months is 0 to 12 (0 is the same month). pay_days is 1 to 4 distinct days of 1 to 31; 31 means month end (a day past the month's length is clamped, so 31 in February is the 28th or 29th). 15 and 31 together are semi-monthly.
  • The write is an upsert per ledger and is all-or-nothing: one invalid row refuses the whole request with 400 VALIDATION_705, and a ledger that is not in this business is 400 VALIDATION_706. Ledgers you leave out keep their rows.
  • The response is {"items": [...]} of the rows you touched, with id, ledger_id, ledger_name, lag_months, pay_days (sorted) and updated_at.
  • GET .../cash-forecast/timing lists every row. There is no delete route for timing rows.
  • Timing rows always apply to the forecast, including saved-version snapshots and what-if scenarios. The forecast reports the ledgers affected in timing_applied and the cash that falls outside the window in fixed_cost_timing_gap_cents.

One-time items

One-time items are single cash events (a tax payment, a funding tranche) that you add to the forecast by date.

bash
curl -X POST "https://api.ondayzero.com/api/v1/businesses/YOUR_BUSINESS_ID/cfo-portal/cash-forecast/one-time-items" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{"label": "Q4 estimated tax", "amount_cents": -2500000, "applies_on": "2026-12-15", "notes": "Federal"}'

label is 1 to 200 characters, amount_cents is positive for cash in and negative for cash out, applies_on is a date and notes (up to 2,000 characters) is optional. The response (201) is the item with id, business_id, created_by_user_id, created_at and updated_at. The item shows in the week whose Sunday-to-Saturday span contains applies_on; a date before this week's Sunday or after week 13 has no effect.

GET .../one-time-items lists them ({"items": [...]}, not paginated), PATCH .../one-time-items/{item_id} updates any of the four fields and leaves omitted ones unchanged, and DELETE .../one-time-items/{item_id} removes one. An unknown ID is 404 NOT_FOUND_001.

Overrides

An override replaces one computed cell with a number you choose. They live in a scope: the business's working set (version_id null), which apply_overrides=true folds into the forecast, or a saved version. The working set belongs to the business, not to a user.

bash
curl -X PUT "https://api.ondayzero.com/api/v1/businesses/YOUR_BUSINESS_ID/cfo-portal/cash-forecast/overrides" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "version_id": null,
    "overrides": [
      {"week_number": 3, "column": "inventory", "amount_cents": 20000000, "note": "Spring reorder deposit"},
      {"week_number": 5, "column": "ar_in", "amount_cents": null}
    ]
  }'

PUT replaces the whole scope by default: existing overrides in that scope are deleted and the rows you send are inserted, so send the full set. An empty overrides list clears the scope. Send "merge": true to set only the cells you send and keep the scope's other overrides (the response lists the scope's full set); Scenario Lab's Apply to my plan uses this. Each override has:

Field Notes
week_number 0 to 104. 0 targets the Overdue row, 1 to 13 a weekly cell. Weeks past the horizon have no effect
column ar_in, ap_out, inventory, debt, debt_in, budget_rev, budget_exp or one_time
amount_cents The literal value used for the cell. null forces the cell to zero
source user (default) or ai
ai_scenario_label, note Optional, up to 200 and 2,000 characters

The columns map to week fields as ar_in to cash_in_cents, ap_out to ap_out_cents, inventory to po_out_cents, debt to debt_out_cents, debt_in to debt_in_cents, budget_rev to budget_revenue_cents, budget_exp to budget_expense_cents and one_time to one_time_cents. Projected replenishment and awaiting-approval AP cannot be overridden. A version_id that does not exist in this business is 404.

GET .../overrides (optionally ?version_id=) returns {"version_id": ..., "items": [...]}; each stored row has id, override_amount_cents, source, ai_scenario_label, note and timestamps. DELETE .../overrides?version_id= clears a scope and DELETE .../overrides/{override_id} removes one row.

Saved versions

A version freezes a configuration together with the forecast it produced, so a scenario stays comparable as live data moves.

bash
curl -X POST "https://api.ondayzero.com/api/v1/businesses/YOUR_BUSINESS_ID/cfo-portal/cash-forecast/versions" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Base plan, October",
    "description": "Active budget, tax paid mid-December",
    "config": {
      "budget_id": "BUDGET_UUID",
      "excluded_budget_ledger_ids": [],
      "weeks": 13,
      "one_time_items_snapshot": [{"label": "Q4 estimated tax", "amount_cents": -2500000, "applies_on": "2026-12-15"}]
    }
  }'

name is 1 to 200 characters and description up to 2,000. The response is the version with id, name, description, snapshot_as_of, created_by_user_id, timestamps, config and snapshot_output (the full forecast JSON at save time). Details that matter:

  • The server recomputes the forecast from the config you send; it does not read your current UI state. Put the one-time items you want frozen in one_time_items_snapshot. If the list is empty, the snapshot is computed with the live one-time items but none are recorded in config.
  • config.overrides omitted or null saves the working override set (what the Reorder Cash Planner does); [] saves none (what the live Cash Flow Forecast tab does). Duplicate cells, or weeks outside 1 to 52, return 400. The version also stores the forecast switches include_projected_replenishment, include_unapproved_ap and net_bills_against_budget, and the detail response returns overrides, overrides_captured and those switches. overrides_captured is false for a version saved before overrides were stored.
  • GET .../versions lists summaries (no config or snapshot), GET .../versions/{version_id} returns the full detail, and PATCH changes name and description only. The configuration and snapshot are immutable: save a new version for a new scenario. DELETE removes the version and the overrides pinned to it.

Re-apply a version

bash
curl -X POST "https://api.ondayzero.com/api/v1/businesses/YOUR_BUSINESS_ID/cfo-portal/cash-forecast/versions/VERSION_UUID/apply" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

The call (a POST, so 201) writes the version's excluded_budget_ledger_ids into the business's forecast settings and replaces the working override set with the version's overrides (an empty set clears it). A version saved before overrides were stored leaves the working overrides untouched and answers overrides_restored: false. It does not reload one-time items. The response gives version_id, budget_id, excluded_budget_ledger_ids, applied_override_count, overrides_restored and the three forecast switches; budget selection is a per-request query parameter, so use budget_id from this response on your next forecast call.

Reorder cash planner (what-if scenarios)

The planner's headline call evaluates a scenario against a clean baseline without saving anything.

bash
curl -X POST "https://api.ondayzero.com/api/v1/businesses/YOUR_BUSINESS_ID/cfo-portal/cash-forecast/demand-plan" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "scenario_description": "Place a $200K reorder in week 3",
    "overrides": [{"week_number": 3, "column": "inventory", "amount_cents": 20000000}],
    "inline_one_time_items": [{"label": "Customer deposit", "amount_cents": 5000000, "applies_on": "2026-11-05"}],
    "weeks": 13
  }'

scenario_description is required (1 to 400 characters). overrides and inline_one_time_items are optional; weeks is 1 to 52 (default 13); budget_id and excluded_budget_ledger_ids select the budget inputs (the saved forecast settings are not read, so pass the exclusions you want). The route runs the forecast twice. The baseline has no overrides and no one-time items; the scenario has only the overrides and inline items you sent. Saved working overrides and saved one-time items take no part in either run, and projected replenishment is not included. Timing rows apply to both.

The response (201) contains weeks (the scenario's weekly rows), week_deltas (week, start_date, baseline_ending_cash_cents, scenario_ending_cash_cents, delta_cents), starting_cash_cents, the baseline and scenario ending cash and lowest cash with their deltas and weeks, a headline, a narrative (it adds a warning line when the scenario turns cash negative or cuts the lowest point by more than half) and warnings. To keep a scenario you like, save its overrides with PUT .../overrides (add "merge": true to keep your other working overrides), then save a version, which captures the working overrides.

To see reorder cash against the live forecast, call cash-forecast with include_projected_replenishment=true (and apply_overrides=true to include the working set). GET .../purchase-orders gives the open commitments (below).

Managerial P&L and the Recipe Book

managerial-pl rebuilds the income statement on managerial buckets. Each ledger row goes to the bucket you mapped it to; an unmapped row falls back by the income statement section it sits in (Operating Revenues to revenue, Cost Of Goods Sold to cogs, Operating Expenses to ga, and so on), and anything unrecognized to other_expense. Create buckets, map ledgers and set the CM1 and CM2 lines through the routes in Businesses.

bash
curl "https://api.ondayzero.com/api/v1/businesses/YOUR_BUSINESS_ID/cfo-portal/managerial-pl?start_date=2026-01-01&end_date=2026-09-30" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

data holds:

  • totals: revenue_cents, cogs_cents, gross_profit_cents, expenses_cents, other_income_cents, other_expense_cents, net_income_cents. They are derived from the bucket totals using each bucket's classification, so they always agree with rows.
  • rows: one per ledger with section, bucket, ledger_id, label, total_cents, periods (cents by period key) and is_mapped.
  • buckets (key, total_cents), bucket_definitions (the active buckets) and bucket_classifications.
  • contribution_margin: cm1_cents and cm2_cents, each from the ledgers you picked for that builder, or gross profit and net income when none are picked, plus builders.
  • cash_flow_outflows: debt_cents and inventory_cents from the cash_outflow_debt and cash_outflow_inventory builders.

GET .../recipe-book returns the editing view: recipes, buckets, bucket_definitions, chart_of_accounts (every revenue and expense ledger, active or not, with its bucket and default_section), managerial_mappings and managerial_builders.

Flux

bash
curl "https://api.ondayzero.com/api/v1/businesses/YOUR_BUSINESS_ID/cfo-portal/flux?start_date=2026-07-01&end_date=2026-09-30" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

With no dates, flux compares the full current calendar month with the full prior month. With both dates, the prior window is the same number of days ending the day before start_date (so a quarter is compared with the quarter before it). data.mode is always month_over_month.

data.variances has four summary rows (key of revenue_cents, cogs_cents, expenses_cents or net_income_cents) with current_cents, prior_cents, variance_cents and variance_pct (null when the prior value is zero). data.material_variances combines those summary rows with every ledger that moved, keeping only variances of at least $1,000 (100,000 cents), largest first among the ledgers. Ledger rows carry ledger_id and label.

AR and AP aging

ar-aging and ap-aging take no parameters and read all open invoices and bills (capped at 1,000 each; past the cap the source is partial and says so).

json
{
  "buckets": {"current": 510000, "1_30": 120000, "31_60": 0, "61_90": 0, "90_plus": 40000, "unknown_due": 15000},
  "customers": [{
    "customer": "Acme Co", "customer_id": "...",
    "current": 510000, "1_30": 0, "31_60": 0, "61_90": 0, "90_plus": 0, "unknown_due": 0,
    "invoices": [{"id": "...", "invoice_number": "INV-1042", "status": "open", "amount_cents": 510000, "paid_cents": 0, "balance_cents": 510000, "due_date": "2026-10-25", "days_past_due": 0, "has_due_date": true}]
  }]
}

ap-aging has the same shape with vendors (each with vendor, vendor_id and bills rows carrying bill_number). Buckets hold balance cents. current means not yet past due, 1_30 is 1 to 30 days past due, and so on; items with no due date go to unknown_due, never to current. Customers and vendors are grouped by ID, so two with the same name stay separate.

Unit economics, landed COGS and shipping

unit-economics gives the current month's per-order margin bridge, from Shopify orders and landed COGS: order_count, aov_cents, aov_mode, shopify_aov_cents, shipping_income_per_order_cents, discounts_per_order_cents, landed_cogs_per_order_cents, gross_profit_per_order_cents (AOV minus discounts minus landed COGS; cm1_per_order_cents and cm2_per_order_cents currently carry the same value) and a four-step waterfall. AOV comes from Shopify unless the configuration sets aov_mode to manual. Shopify order data is used only when unit-economics is in the configuration's shopify_sections list (the default includes it); otherwise the order figures are zero and the shopify_orders source is unavailable.

landed-cogs returns average_landed_cost_cents and products, each with variant_id, name, sku, material_per_unit_cents, freight_per_unit_cents, processing_per_unit_cents, landed_cost_per_unit_cents, price_cents and source (po_average, or recipe_po_average for recipe outputs). It uses purchase orders in received or completed status (each order counts once): material is the quantity-weighted average line price, freight is each order's shipping cost allocated by line quantity, and processing comes from recipe snapshots. With no received or completed orders you get an empty list and a source message. average_landed_cost_cents is a simple mean across products, not weighted by volume.

shipping-analysis sums the shipping on Shopify orders over the last six months (the current month and the five before it) into trend rows of month, orders, shipping_cents and shipping_per_order_cents, with an info flag when there are no orders. It reads up to 10,000 orders. When shipping-analysis is not in the configuration's shopify_sections list, it returns an empty trend, shopify_in_scope: false and an info flag, without reading Shopify orders. shopify returns summary, monthly (orders, gross sales, discounts, shipping and AOV per month), the top 25 products and products_by_month, and returns an empty payload when shopify is not in shopify_sections.

Vendor spend

bash
curl "https://api.ondayzero.com/api/v1/businesses/YOUR_BUSINESS_ID/cfo-portal/vendor-spend?group_by=month&sources=bill&sources=credit_card" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"
Parameter Notes
start_date, end_date Default to the last 12 months. An inverted range is 400 VALIDATION_101
group_by counterparty (default) or month; anything else is 400 VALIDATION_001
sources Repeat the parameter. Values are bill, credit_card and bank; default is all three. Unknown values simply match nothing

Spend combines bills, posted expense transactions classified as bank or card by their account (transactions that pay a bill are left out so bills are not counted twice), and Ramp card transactions (as credit_card). Each of the three is capped at the latest 500 rows in the window. If you pass no dates and the default window is empty, the window widens to start at the earliest spend date. data holds group_by, total_spend_cents, periods (sorted month keys), top_vendors (at most 25, each with vendor, source, spend_cents, spend_pct, open_cents, bill_count, txn_count and per-month periods), grouped_rows (the monthly rows when group_by=month, else the same as top_vendors) and flags: a warning when one vendor is more than 50 percent of recorded spend. Bills are bucketed by the month they were created.

Purchase orders

GET .../purchase-orders?status=shipped returns up to the latest 100 purchase orders: stats (active_count, active_value_cents, in_transit_value_cents, truncated), a pipeline of {status, count, total_cents} and the purchase_orders rows (po_number, vendor, status, total_cents, balance_cents, expected_delivery_date, expected_payment_date, is_matched, matched_bill_id, line_items). Active means submitted, in_production, ready, shipped or partially_received. A PO with a paid linked bill shows as completed, and the status filter is applied to that displayed status. Commitments use the PO balance. A PO with a linked bill flows through the forecast as AP, and one without flows as inventory; po-forecast-settings shows which.

Valuation, benchmarks and alerts

  • valuation returns cap_table (the cap table summary), inputs (monthly_revenue_cents for the current month and a revenue_multiple of 2.5), illustrative_valuation_cents (monthly revenue times 12 times the multiple) and a disclaimer. It is illustrative, not an appraisal.
  • benchmarks returns metrics for net margin (unit of percent) and cash coverage (unit of cents), each with a text benchmark, and a source_note saying the ranges are directional.
  • alerts returns rules (id, name, rule_type, threshold_value, threshold_direction, is_enabled, last_checked_at, last_triggered_at, last_value, currently_breached) and triggered, the enabled rules that are breached right now. currently_breached resets when the metric recovers, unlike last_triggered_at. These routes only read the rules.

Monthly close from the CFO side

monthly-close is a read-only leadership view of the newest accounting period. data.current_period has id, period_start, period_end, status and closed_at; history lists up to 12 periods; checklist has three items, each with key, label and a status of done, pending or blocked:

key done when blocked when
statements the period is closed or locked, or its trial balance balances and has activity the trial balance is out of balance
ap_review no open bill is overdue any open bill is overdue
period_closed the newest period is not open (never; otherwise pending)

blockers lists up to 10 overdue bills. To act on a period (close, sign off, lock, reopen) use the routes in Closing the Books.

Board deck and documents

board-deck builds a one-page summary from the same metrics as the summary plus the statement bundle. Without dates it uses the current month (or the latest month with activity, noted in data.period.note); with both dates the prior comparison window is the same length. data contains headline_kpis (revenue, gross margin percent, net income, cash, AP due in 30 days), highlights, concerns, management_actions, board_decisions (added when cash is tight or net income is negative), risk_register, cash_story (cash_cents, ap_due_30d_cents, coverage_ratio, signal of tight or stable), financial_statement_links and ai_memo_seed. The management_actions are fixed template text with a status driven by the numbers.

bash
curl -X POST "https://api.ondayzero.com/api/v1/businesses/YOUR_BUSINESS_ID/cfo-portal/board-deck/memo?start_date=2026-09-01&end_date=2026-09-30" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"
  • POST .../board-deck/memo takes no body, only the optional start_date and end_date query parameters, and returns title, memo_markdown, generated_at, status and sources. status is available when the AI wrote the memo; on any internal failure the route returns a 200 with status of fallback and a short "Board memo unavailable" text instead of an error.
  • GET .../board-deck/pdf (same two query parameters) streams board-deck-summary-YYYYMMDD.pdf (application/pdf).
  • POST .../board-deck/memo/export turns a memo you hold into a file. Body: memo_markdown (required, 1 to 100,000 characters), title (default "AI Board Memo", up to 200), generated_at and format (pdf default, or html). An empty memo is 400 VALIDATION_002; another format is 400 VALIDATION_001. The response is the file with a Content-Disposition filename built from the title and timestamp.

reports-documents is the board documents view: reports (up to 25 generated reports, each id, report_name, s3_key, downloaded_at, has_signed_url, generation_params), documents (up to 25 files, each id, name, type, source, s3_key, created_at) and app links. It lists only; use Reports and Uploads to generate and add files.

Ask Regent

POST .../ask with {"question": "Why did cash drop in week 4?", "context_section": "cash-forecast"} returns {"answer": "...", "sources": ["Cash forecast"]}. question is 1 to 2,000 characters. context_section picks the data the answer is grounded on: weekly-pulse, summary, ap-aging, cash-forecast, demand-planner, vendor-spend, purchase-orders, monthly-close, shopify, alerts or board-deck; anything else, or no value, uses the summary. With demand-planner, a question shaped like "what if I place a $200K PO in week 3" is also run through the scenario engine. If the assistant fails the call still returns 200 with a "temporarily unavailable" answer and empty sources.

Errors

Status Code When
400 VALIDATION_101 end_date before start_date on statements, flux, budget-vs-actual, vendor-spend, board-deck
400 VALIDATION_001 Unsupported group_by or memo export format
400 VALIDATION_002 Empty memo_markdown on export
400 VALIDATION_705, VALIDATION_706 Invalid timing row; ledger not in this business
400 VALIDATION_266 Invalid unit economics or bucket input (see Businesses)
403 BILLING_003 CFO Advisory Suite add-on not active
403 BILLING_001 Write refused: trial ended and no subscription
403 AUTH_002 Path business_id differs from x-business-id, or the caller's role does not permit the call
403 AUTH_048 Token scope does not permit the call (see Error Handling)
404 NOT_FOUND_001 Unknown one-time item, version or override ID
400 GEN_002 Body or query fails schema validation (for example week_number above 104, weeks above 52); errors names the fields

The error envelope is described in Error Handling.

What is not covered

  • Debt schedule. debt-schedule and its write routes (parse and approve a lending contract, instruments, payments, paydown tables and rows, covenants) exist under the same prefix and the same gating, but are not documented here.
  • Statement sections in detail. statements (income statement, balance sheet and cash flow bundle) and budget-vs-actual are listed above but their line-level payloads are not described. The standalone Reports and Budgets guides cover the same data.
  • Alert rule management, valuation scenarios and document uploads. The CFO portal routes only read alerts and list documents. This guide does not cover how those are created.
  • Field-by-field schemas for the large summary, board-deck and cash-forecast payloads; the API Reference has the response models, but note the sections use a free-form data object.
  • Frontend-only behavior such as the Scenario Lab prompt box, and the AI tools and MCP tools that wrap these routes (see MCP).