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:
/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_suiteis 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 emptydataand oneunavailablesource with the message "This section is temporarily unavailable." Always checksourcesbefore trusting an emptydata.HTTPExceptions (auth, validation) keep their real status. basisisaccrualorcashand follows the business's default accounting basis. There is no query parameter to change it. It is set onsummary,statements,managerial-pl,fluxandcash-forecast.- Short-lived cache.
summary,statements,managerial-pl,benchmarksandvaluationare cached for 180 seconds, andunit-economics,landed-cogsandshipping-analysisfor 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-agingand the other sections are not cached. - Date parameters are
YYYY-MM-DD. For the dated sections, send bothstart_dateandend_dateor neither; a lone date is ignored and the default window is used.vendor-spendis the exception: it accepts either date on its own and fills in the other from its default. An inverted range is a400VALIDATION_101onstatements,flux,budget-vs-actual,vendor-spendandboard-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.
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):
{
"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}]}
}
}kpishas six entries:revenue,gross_profit,net_income,cash,open_apandshopify_sales.delta_pctcompares with the prior month.cash_sourceisplaidwhen synced bank balances are used andledgerotherwise;cash_ledger_centsandcash_reconciliation_noteappear when the two differ.flagscan contain "Overdue AP" (warning) and "Cash coverage" (critical, when cash is below AP due in the next 30 days).pulse.weekly_series(only withinclude_series) holdsrevenue,cogs,gross_profit,opexandnet_income, each a list of 13{week_start, week_end, amount_cents}, oldest first.pulse.anomaliesflags 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 hasid,metric_key,current_cents,baseline_mean_cents,delta_cents,delta_pct,z_score,severity(critical,warningorinfo) andnarrative.pulse.healthhasrunway_months(null when the business is not burning cash),ar_days,ap_daysand*_statustraffic lights (ok,warning,critical,info), plusbooks_status,books_close_state(closed,noneorunavailable) andbooks_last_close_at(end date of the latest closed or locked accounting period).pulse.actionsis a ranked list of at most 12 (id,urgency,title,why,route,kindofanomaly,aporflag).pulse.readiness.is_readyis false only for an empty tenant;stepslists setup nudges. The AI narration is skipped for a tenant that is not ready.narration_statusisavailable,fallback(deterministic text) orfailed.
Cash flow forecast
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.
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_monthsis 0 to 12 (0 is the same month).pay_daysis 1 to 4 distinct days of 1 to 31;31means 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
400VALIDATION_705, and a ledger that is not in this business is400VALIDATION_706. Ledgers you leave out keep their rows. - The response is
{"items": [...]}of the rows you touched, withid,ledger_id,ledger_name,lag_months,pay_days(sorted) andupdated_at. GET .../cash-forecast/timinglists 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_appliedand the cash that falls outside the window infixed_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.
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.
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.
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
configyou send; it does not read your current UI state. Put the one-time items you want frozen inone_time_items_snapshot. If the list is empty, the snapshot is computed with the live one-time items but none are recorded inconfig. config.overridesomitted ornullsaves the working override set (what the Reorder Cash Planner does);[]saves none (what the live Cash Flow Forecast tab does). Duplicate cells, orweeksoutside 1 to 52, return400. The version also stores the forecast switchesinclude_projected_replenishment,include_unapproved_apandnet_bills_against_budget, and the detail response returnsoverrides,overrides_capturedand those switches.overrides_capturedisfalsefor a version saved before overrides were stored.GET .../versionslists summaries (no config or snapshot),GET .../versions/{version_id}returns the full detail, andPATCHchangesnameanddescriptiononly. The configuration and snapshot are immutable: save a new version for a new scenario.DELETEremoves the version and the overrides pinned to it.
Re-apply a version
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.
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.
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 withrows.rows: one per ledger withsection,bucket,ledger_id,label,total_cents,periods(cents by period key) andis_mapped.buckets(key,total_cents),bucket_definitions(the active buckets) andbucket_classifications.contribution_margin:cm1_centsandcm2_cents, each from the ledgers you picked for that builder, or gross profit and net income when none are picked, plusbuilders.cash_flow_outflows:debt_centsandinventory_centsfrom thecash_outflow_debtandcash_outflow_inventorybuilders.
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
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).
{
"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
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
valuationreturnscap_table(the cap table summary),inputs(monthly_revenue_centsfor the current month and arevenue_multipleof 2.5),illustrative_valuation_cents(monthly revenue times 12 times the multiple) and adisclaimer. It is illustrative, not an appraisal.benchmarksreturnsmetricsfor net margin (unitofpercent) and cash coverage (unitofcents), each with a textbenchmark, and asource_notesaying the ranges are directional.alertsreturnsrules(id,name,rule_type,threshold_value,threshold_direction,is_enabled,last_checked_at,last_triggered_at,last_value,currently_breached) andtriggered, the enabled rules that are breached right now.currently_breachedresets when the metric recovers, unlikelast_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.
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/memotakes no body, only the optionalstart_dateandend_datequery parameters, and returnstitle,memo_markdown,generated_at,statusandsources.statusisavailablewhen the AI wrote the memo; on any internal failure the route returns a200withstatusoffallbackand a short "Board memo unavailable" text instead of an error.GET .../board-deck/pdf(same two query parameters) streamsboard-deck-summary-YYYYMMDD.pdf(application/pdf).POST .../board-deck/memo/exportturns 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_atandformat(pdfdefault, orhtml). An empty memo is400VALIDATION_002; another format is400VALIDATION_001. The response is the file with aContent-Dispositionfilename 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-scheduleand 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) andbudget-vs-actualare 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-deckandcash-forecastpayloads; the API Reference has the response models, but note the sections use a free-formdataobject. - Frontend-only behavior such as the Scenario Lab prompt box, and the AI tools and MCP tools that wrap these routes (see MCP).