Reporting API

The Reporting API gives your reporting tools (Power BI, Excel, a spreadsheet script or your own code) the same figures as the Reports page in the portal: stock on hand, stock-outs, stock value, expiring lots, open purchase orders and stock activity, as flat, typed datasets. It is read-only, it is included on every plan (Shared, Dedicated and Enterprise) with no add-on to buy, and it works the same way on every plan.

Three things have similar names:

  • The Reports page in the portal (Reports in the portal sidebar) shows these figures on screen, with a CSV download per card.
  • The Reporting menu inside OpenBoxes is part of the OpenBoxes application itself and reads your live OpenBoxes data.
  • The Reporting API (this page) returns the Reports page's data to software, with an API key.

The scheduled CSV export is available on every plan: a zip of these datasets as CSV files every day, week or month, with a link emailed to you to download it from the portal. It uses the same CSV columns as this page, does not count against this API's allowance and needs no API key.

Power BI and Excel can read the Reporting API today with our Power Query recipe: see Power BI and Excel. Looker Studio cannot call the Reporting API; today you upload a scheduled export's CSV file to it by hand: see Looker Studio. The Looker Studio connector and the Power BI connector are coming soon: we are building them and they are not available yet, with no date. Any other tool that can call a web address with a header can read the Reporting API directly (it can answer in CSV).

Where the data comes from#

The Reporting API does not read your live OpenBoxes database. Once a day, after 04:00 in your account's time zone, we check your stock, prices, purchase orders and activity and keep a copy of the result. The Reporting API serves that copy. Every row says which daily check it comes from (captureLocalDate, capturedAt, capturedAtUtc and zone), so a figure is always "as of the daily check", never "right now". A change you make in OpenBoxes shows up after the next daily check.

History goes back as far as we have checks for your account: daily rows for the last 90 days, and monthly rows for up to 13 months. The start of your series is published per dataset (earliest in the index, seriesStart in each response).

Base URL and your API key#

The Reporting API lives on the platform API host, not on your instance subdomain:

https://api.openboxes.cloud/api/v1/reporting

Your instance subdomain does not serve the Reporting API. A request to https://{your-subdomain}.openboxes.cloud/api/v1/reporting is answered by your OpenBoxes instance with its own "Page Not Found" page (status 404, HTML, not a JSON error), and no reporting data. There is no account or subdomain parameter: the account is always the one your key belongs to.

Send an API key in the X-API-Key header, exactly as for the rest of the API (see Authentication):

curl -H "X-API-Key: $OB_API_KEY" \
  https://api.openboxes.cloud/api/v1/reporting/stock-on-hand
  • Keys go in the header, never in the URL. A request with a parameter named apiKey, api_key, key or X-API-Key, or with a whole key (obc_… followed by . and the secret) in any parameter name or value, written plainly or percent-encoded, is refused with 400 api_key_in_url and is not processed. A URL can end up in logs and browser history, so if you have put a key in one, revoke that key in the portal and create a new one.
  • Create a key with the reporting scope. Scopes are recorded and not yet enforced, so today any key bound to your account works. Once scopes are enforced (no date yet) the Reporting API will accept keys with the reporting, read, write or admin scope and refuse a key that records no scopes, so create a reporting key now.
  • A key must be bound to your account. A key created before keys were bound to an account is refused with 403 api_key_no_tenant_binding. Create a new one.
  • Only the account owner creates API keys. Invited teammates can open the API keys page, but they cannot list, create or revoke keys there.
  • A portal sign-in is not accepted: a request carrying a portal session instead of a key gets 401 api_key_required.
  • Revoking a key stops it within seconds; a key's expiry date takes effect within about 5 minutes (see Authentication).

The Reporting API answers GET (and HEAD, which is a GET without the body). Any other method is refused.

Every path must be written exactly as shown on this page. A trailing slash (/api/v1/reporting/stock-on-hand/), a percent-encoded character or an extra path segment returns an error instead of data.

Quantities by default, values when the account owner allows it#

By default a key returns quantities only. Every money field (stock value, prices, value at risk and order amounts) is present in every row but empty, and valueState says HIDDEN.

To add values to a key, the account owner turns on Include values for it: tick Include values when creating the key, or open the key's menu on the API keys page and choose Include values. The portal asks you to confirm, because anyone holding the key can then read your stock value, prices and order amounts. Only the account owner can turn this on, and only for a key they created themselves. Stop including values in the same menu turns it off again; responses stop carrying values within 30 seconds.

A key includes values only while the person who created it is still the account owner. If ownership of the account moves to someone else, keys created by the previous owner return quantities only (valuesHidden is not_owner), and the new owner can create a key of their own with Include values turned on.

Every response tells you where it stands:

valuesHidden What it means
null Values are included (valuesIncluded is true).
key Include values is off for this key.
not_owner Include values is on, but this key was not created by the current account owner.
unavailable We could not check the key's setting just now; money fields are empty and valueState is VALUES_CHECK_UNAVAILABLE. Try again in a minute.

valuesHiddenLine carries the same explanation as a sentence. If a scheduled refresh must never store empty values, add values=required to the request: when the setting cannot be checked you get 503 unavailable with Retry-After: 30 instead of a response with empty money fields. (key and not_owner still answer normally, with HIDDEN.)

Money is shown at each product's price per unit in OpenBoxes (valueLabel), without a currency symbol. Purchase-order amounts carry each order's own currency code and are never added up across currencies.

Endpoints#

Method and path Returns
GET /api/v1/reporting The dataset index: every dataset, its grains, parameters, limits and where your data starts
GET /api/v1/reporting/{dataset} Rows, as JSON or CSV
GET /api/v1/reporting/{dataset}/schema The dataset's ordered, typed field list (add grain=monthly for a dataset's monthly rows)

The datasets#

Dataset One row per Kind
summary the whole account: the Reports page's tiles as one row latest check, or date=
stock-on-hand location and product on the check day latest check, or date=
stock-history location, product and day (grain=daily) or month (grain=monthly) date range
stock-value-history day or month, for the whole account the stored series
lots-expiring location, product and lot latest check, or date=
purchase-orders-open purchase order and currency latest order check
activity location, product, day (or month) and activity category date range

summary is always exactly one row (no rows at all before your first daily check) and is never paged. Use it for totals rather than adding up rows yourself: each row's value is rounded to the cent, so adding up the rows can differ from summary.stockValue by a few cents. Open orders in summary are counted as distinct orders, while purchase-orders-open has one row per order and currency.

The index and the schema#

GET /api/v1/reporting lists every dataset with what a connector needs to build a request: its grains and defaultGrain, its parameters (name, type, allowed values, default, whether it may repeat), the page limits, the dateField to use as a date dimension, and earliest, the first day (or month) your account has data for. A dataset with daily and monthly rows also publishes earliestByGrain. The index also says whether values are included for your key.

GET /api/v1/reporting/{dataset}/schema returns the same parameters plus fields, the dataset's columns in order, each with its name, type (string, integer, decimal, boolean, date, datetime or enum, with the allowed values for an enum), whether it can be empty, whether it is a money field, and a description. Money fields are listed whether or not your key includes values: the shape of a dataset never depends on the key.

Fields are only ever added. New fields are only ever added at the end of a dataset's field list. Within schemaVersion 1 a field is never renamed, removed, reordered or given a different type. A breaking change would ship as schemaVersion 2 on new paths, announced in advance.

Parameters#

Every dataset accepts:

Parameter Values Default
format json or csv json
limit rows per page: 1 to 1,000 for JSON, 1 to 10,000 for CSV 1,000 (JSON), 10,000 (CSV)
cursor the nextCursor of the previous page; empty or absent means the first page none
locationId only this location (for purchase orders, the destination location) all locations
values required (datasets with money fields only; see above) none

A parameter a dataset does not know is refused with 400 unknown_parameter; a value that is not valid (a bad date, a limit above the maximum, a parameter given twice) with 400 invalid_parameter. Nothing is silently clamped.

Dataset Its own parameters
summary date: a day with a completed daily check in the last 90 days (default: the latest check)
stock-on-hand date (as summary), productId, state (below)
stock-history grain (daily or monthly, default daily), from, to, productId
stock-value-history grain (daily, monthly or all, default daily)
lots-expiring date (as summary), withinDays (0 to 365, default: the expiry window of your Stock alerts panel), includeExpired (true or false, default false)
purchase-orders-open status (open, draft or all, default open, which leaves out drafts), overdue (true returns overdue orders only; false applies no filter)
activity grain (daily or monthly, default daily), from, to, category (repeat it for up to 10 categories)

A date with no completed daily check returns 404 no_check_on_date; leave date out to get the latest check.

state on stock-on-hand is named after the Reports page tiles:

state= Rows
all (default) every product at every location
stock_out at zero with a minimum set
at_zero_no_minimum at zero with no minimum set
below_minimum below their minimum, including stock-outs
above_maximum above their maximum
needs_attention at zero, or below a minimum

Date ranges (stock-history and activity): with grain=daily, from and to are days inside the last 90 days, at most 92 days apart; by default the last 30 days up to the latest check. A to after the latest check is read as the latest check. With grain=monthly, any day is accepted and stands for its month; a range covers at most 13 months, and by default the last 13 months including the month in progress. A longer range is refused with 400 range_too_large.

Monthly rows. A monthly row is flagged partialMonth for the month in progress and for the month where the daily history ends. For day-by-day detail in the last 90 days, use grain=daily. On stock-history monthly rows, valueLast is the value on pricesCheckedAt. On stock-value-history, monthly rows cover only the months older than the 90-day daily window, and grain=all returns the monthly rows followed by the daily rows as one series with no missing days; there, valueDate is the day the value's prices come from, not a quantity day.

The response (JSON)#

{
  "dataset": "stock-on-hand",
  "grain": "daily",
  "schemaVersion": 1,
  "state": "OK",
  "message": null,
  "captureLocalDate": "2026-09-20",
  "capturedAt": "2026-09-20T04:10:07+03:00",
  "capturedAtUtc": "2026-09-20T01:10:07Z",
  "zone": "Africa/Nairobi",
  "seriesStart": "2026-09-18",
  "monthlyThrough": null,
  "valuesIncluded": false,
  "valuesHidden": "key",
  "valuesHiddenLine": "This key returns quantities only. To add stock value, prices and order amounts, turn on Include values for this key in the portal (API, API Keys). Only the account owner can do this.",
  "valueLabel": "at each product's price per unit in OpenBoxes",
  "stale": false,
  "staleLine": null,
  "gaps": [],
  "rows": [ { "locationId": "…", "productId": "…", "quantityOnHand": 40 } ],
  "page": { "limit": 1000, "returned": 1000, "rowsTotal": 2412, "nextCursor": "eyJ2Ijoz…" }
}

(The row above is shortened; a real row carries every field of its dataset.)

  • state is OK, NO_CAPTURE (your first daily check has not run yet; message says when it will) or NO_DATA (purchase orders or activity: that part of the check has not completed yet). Both of the last two come with no rows.
  • stale is true when the purchase-order or activity check is more than two days older than the stock check, with staleLine saying so.
  • gaps (activity) lists the date ranges where we could not record activity.
  • Every field is in every row, in schema order. A field with no value is null; nothing is ever left out.
  • Rows with money fields also carry valuesIncluded, valueState, notMeasuredReason and pricesCheckedAt (the price day the row's money uses).

`valueState`#

One list of values for every dataset (each dataset's schema lists the ones it can return):

valueState Meaning
OK The money fields are filled in.
NO_PRICE The price day has no price above zero for this row (a zero price is never shown as a zero value).
NO_STOCK summary only: nothing had stock on the price day.
NOT_IN_PRICE_CHECK stock-on-hand only: the product has no row on the price day (for example it was first stocked on the check day).
NOT_MEASURED No price day applies; notMeasuredReason says why.
NOT_CAPTURED stock-value-history only: no stock was recorded for that period. Either no daily check ran, or the check ran and found no stock (then capturedAt still says when it ran).
HIDDEN Values are not included for this key.
VALUES_CHECK_UNAVAILABLE We could not check whether this key includes values.

notMeasuredReason is PRICES_NOT_YET_CHECKED (that day's price check has not run yet; it runs shortly after the stock check), PRICE_CHECK_FAILED, or NO_PRICE_CHECK_IN_7_DAYS. It is empty on monthly rows.

Money is rounded to 2 decimal places (half up); amc and monthsOfStock to 1. Decimals are written as plain numbers, never in exponent form.

Paging through a dataset#

Pages are walked with a cursor, not an offset. Ask for the first page without a cursor, then pass each response's page.nextCursor as cursor= until it is null. Keep every other parameter the same. In CSV the next cursor comes in the X-Next-Cursor response header instead (see CSV); in Power BI and Excel, page in JSON and read nextCursor from the body, as the Power Query recipe does.

cursor=""
while :; do
  page=$(curl -s -H "X-API-Key: $OB_API_KEY" \
    "https://api.openboxes.cloud/api/v1/reporting/stock-on-hand?limit=1000&cursor=$cursor")
  echo "$page" | jq -c '.rows[]'
  cursor=$(echo "$page" | jq -r '.page.nextCursor // empty')
  [ -z "$cursor" ] && break
done

A walk is pinned on its first page, so it never mixes two days even if a daily check lands halfway through: the check day (or date range), the expiry window, the total row count (rowsTotal) and whether values are included stay the same for every page.

  • If values are turned on or off for the key during a walk, the next page answers 409 values_changed. Start again without a cursor.
  • If a pinned day leaves the 90-day history before you finish, the next page answers 410 cursor_expired. Start again without a cursor.
  • A cursor only works for the request and the account it came from; anything else is 400 invalid_cursor.

Send pages one after another, not in parallel. Each of our servers handles one Reporting API request at a time for your account and queues a few more behind it. A request that finds that queue full, or waits too long, gets 429 reporting_busy with Retry-After: 5; wait and send it again. Several small reports (or several small Power BI tables) refreshing at once is fine: they take their turn. Once the allowance runs out they share it, so several large tables refreshing at once fail sooner.

CSV#

Add format=csv to get the same rows as CSV: RFC 4180, UTF-8, Content-Type: text/csv; charset=utf-8, downloaded as openboxes-<dataset>-<day>.csv. <day> is the day of the daily check the rows come from; for stock-history or activity with no rows in the range it is the end of the requested range, and before the first check (or the first activity check) it is latest. A CSV page holds up to 10,000 rows.

  • The header row is the dataset's field names in schema order, the same whether or not the key includes values. Money cells are empty when values are not included, and valueState says why.
  • true and false for booleans; an empty unquoted cell for null; "" (a quoted empty cell) for an empty text value, so an empty lot number and a missing one can be told apart; dates, times and decimals as in JSON.
  • A text cell beginning with =, +, -, @, a tab or a carriage return gets a leading apostrophe so a spreadsheet does not run it as a formula (a lot number -A12 is '-A12 in CSV and -A12 in JSON). Numbers we compute are never changed.
  • What JSON carries around the rows comes back as response headers: X-Report-State, X-Values-Hidden (when values are hidden), X-Stale: true (when set), X-Next-Cursor (absent on the last page), X-Rows-Total, X-Series-Start, X-Monthly-Through (stock-value-history) and, on activity, X-Activity-Gaps (the recorded gap ranges inside your range, as 2026-09-01/2026-09-03,2026-09-10/2026-09-10).
  • A NO_CAPTURE or NO_DATA answer is the header row only.
  • Errors are always JSON, even for a CSV request.

Your reporting allowance#

The Reporting API has its own allowance of 600 requests an hour per account, on every plan. It is separate from your plan's hourly API limit: Reporting API requests never use up your API limit, and your other API calls never use up the reporting allowance. The API add-on raises your API limit, not the reporting allowance.

It works like a bucket that holds 600 requests: you can use all 600 at once, and then one more comes back every 6 seconds, without ever holding more than 600. It is never reset on the hour. Every Reporting API response carries X-RateLimit-Bucket: reporting with X-RateLimit-Limit (600) and X-RateLimit-Remaining (how many requests you can send right now; the header can appear twice, so read the first value). X-RateLimit-Reset is also sent, but on a successful response it is simply one hour from now: it is not the moment your allowance refills, so do not wait for it. On a 429, Retry-After tells you how many seconds until the next request is allowed.

A worked example. Each page is one request, so what a refresh costs depends on the page size and format. A report that reads five datasets in JSON, each with fewer than 1,000 rows, uses at least 5 requests per refresh. If one of those datasets has 25,000 rows, it takes 25 JSON pages of 1,000 rows, so that refresh uses at least 29 requests; a tool that pages in CSV (10,000 rows a page) needs only 3 pages for it, so at least 7 in all. These are minimums: Power BI and Excel (Power Query) can run a query more than once during a single refresh, and every run costs requests again.

What counts. Each request counts against the reporting allowance, including ones we refuse (a 400 about your parameters, a 404, 405, 409 or 410 costs one request like any other). Two answers do not count: reporting_busy, and a 503 sent before any work was done (your instance is being set up or rebuilt, we could not check whether the key includes values, or our reporting service was briefly unavailable). A 503 sent after we started working on your page does count: a database query that took too long or failed, or a page that could not be built within 25 seconds. A request with an invalid key, and one refused with api_key_in_url, is refused before it reaches the allowance.

When the allowance runs out you get 429:

{
  "error": "rate_limit_exceeded",
  "bucket": "reporting",
  "message": "Reporting API limit reached: 600 requests per hour. Your other API calls are not affected. Try again after the reset time.",
  "limit": 600,
  "reset_at": 1700003600
}

with a Retry-After header. Three different 429 answers can reach you, and each tells you what to do:

  • rate_limit_exceeded with "bucket": "reporting": the hourly allowance is used up. Wait for Retry-After.
  • reporting_busy: other requests for your account are still being worked on. Wait 5 seconds and send pages one after another. This one does not count against the allowance.
  • An empty body: the platform's short-burst protection (a great many requests within one second). Wait a second and slow down.

Errors#

Errors are JSON: {"error": "<code>", "message": "<one sentence>"}. This is not the same shape as the OpenBoxes data API's errors.

Status error When
400 api_key_in_url A key was put in the URL. Revoke it and send a new one in the X-API-Key header.
400 unknown_parameter, invalid_parameter, invalid_cursor, range_too_large, invalid_format The request's parameters (see above).
400 (empty body) A path with an encoded character, such as %2F. Write the path exactly as documented.
401 (empty body) No key at all.
401 unauthorized The key is invalid, revoked or expired.
401 api_key_required A portal sign-in instead of an API key.
402 account_suspended The account is suspended. Reporting data is available again once the account is active.
403 api_key_no_tenant_binding The key is not bound to an account. Create a new key.
403 insufficient_scope Once scopes are enforced: the key's scopes do not allow this request (a reporting key sending anything but GET gets this instead of 405).
404 not_found A path not written exactly as documented, an unknown dataset, or a closed account. (A host other than api.openboxes.cloud does not serve the Reporting API at all: your instance subdomain answers with its own HTML "Page Not Found" page.)
404 no_check_on_date No completed daily check on that date.
405 method_not_allowed Not a GET or HEAD.
409 values_changed Values were turned on or off for the key during a walk. Start again without a cursor.
410 cursor_expired The walk's day has left the history. Start again without a cursor.
429 rate_limit_exceeded, reporting_busy or an empty body See Your reporting allowance.
500 internal_error Something went wrong on our side. Try again later, and contact support if it keeps happening. It counts against the allowance.
503 unavailable Reporting data could not be loaded just now, or your instance is being set up or rebuilt. Wait for Retry-After (30 seconds) and try again. Some 503s count against the allowance (see What counts).

Field reference#

In the tables below, (money) marks a field that is empty unless the key includes values. Every dataset's rows end with the day of the daily check they come from: captureLocalDate, capturedAt (in the row's time zone), capturedAtUtc and zone. For the exact, current list, ask the schema endpoint.

`summary`#

# Field Type Can be empty Description
1 locationId string yes The location filter, or empty for all locations.
2 pairsTotal integer no Products with a stock row on the check day.
3 stockOuts integer no Products at zero with a minimum set.
4 atZeroNoMinimum integer no Products at zero with no minimum set.
5 belowMinimum integer no Products below their minimum, including stock-outs.
6 aboveMaximum integer no Products above their maximum.
7 productsWithMinimum integer no Products with a minimum set.
8 productsTotal integer no Products stocked.
9 expiryWindowDays integer no The expiry window of the Stock alerts panel, in days.
10 lotsExpiringWithinWindow integer no Lots expiring within that window, expired lots excluded.
11 openOrders integer yes Open purchase orders (distinct orders). Empty until the first order check.
12 draftOrders integer yes Draft purchase orders.
13 overdueOrders integer yes Open orders whose shipment date has passed.
14 purchaseOrdersCaptureLocalDate date yes The day of the order check the order counts come from.
15 purchaseOrdersStale boolean yes True when the order check is more than two days older than the stock check.
16 pairsWithStock integer yes Products with stock on the price day.
17 pricedPairsWithStock integer yes Of those, products with a price.
18 valuesIncluded boolean no True when the money fields of this row are filled in.
19 valueState enum: OK, NO_PRICE, NO_STOCK, NOT_MEASURED, HIDDEN, VALUES_CHECK_UNAVAILABLE no Why the money fields hold what they hold. OK means they are filled in.
20 notMeasuredReason enum: PRICES_NOT_YET_CHECKED, PRICE_CHECK_FAILED, NO_PRICE_CHECK_IN_7_DAYS yes Why the value is not measured, when valueState is NOT_MEASURED.
21 pricesCheckedAt date yes The price day: the newest day in the 7 days up to the check with prices.
22 stockValue (money) decimal yes Stock value on the price day, at each product's price per unit in OpenBoxes.
23 valueOfPairsNotInSnapshot (money) decimal yes Value of priced products present on the price day but not on the check day. Empty when there is no price day (pricesCheckedAt empty).
24 pricedPairsNotInSnapshot integer yes How many products that value covers. Empty when there is no price day.
25 valueLabel string no What the value means.
26 captureLocalDate date no The check day.
27 capturedAt datetime no When the check ran, in the account's zone.
28 capturedAtUtc datetime no The same instant in UTC.
29 zone string no The time zone the check used.

`stock-on-hand`#

# Field Type Can be empty Description
1 locationId string no OpenBoxes location id.
2 locationName string yes Location name at the daily check.
3 productId string no OpenBoxes product id.
4 productCode string yes Product code at the daily check.
5 productName string yes Product name at the daily check.
6 quantityOnHand integer no Quantity on hand at the check.
7 quantityAvailable integer yes Quantity available to promise at the check.
8 minQuantity integer yes Minimum set in OpenBoxes.
9 reorderQuantity integer yes Reorder quantity set in OpenBoxes.
10 maxQuantity integer yes Maximum set in OpenBoxes.
11 stockState enum: AT_ZERO_MIN_SET, AT_ZERO_NO_MIN, BELOW_MINIMUM, OK no The row's stock state, first match wins in this order.
12 aboveMaximum boolean no True when a maximum is set and the quantity is above it.
13 daysAtZero30 integer yes Days at zero in the last 30 days checked.
14 daysChecked30 integer yes Days checked in the last 30 days.
15 daysAtZero90 integer yes Days at zero in the last 90 days checked.
16 daysChecked90 integer yes Days checked in the last 90 days.
17 insightComputedFor date yes The day the days-at-zero and months-of-stock figures describe.
18 mosState enum: OK, NOT_ENOUGH_HISTORY, TRANSFERS_ONLY, NO_USE yes Whether months of stock could be worked out.
19 amc decimal yes Average monthly issues and consumption over 3 months, 1 decimal place.
20 monthsOfStock decimal yes Months of stock, 1 decimal place, when mosState is OK.
21 underOneMonth boolean yes True when the rounded months of stock is under 1.
22 mosGapMonth date yes The month whose activity is incomplete, when there is one.
23 includesImportedHistory boolean yes True when imported history feeds the figure.
24 priced boolean no True when the product has a price above zero on the price day.
25 valuesIncluded boolean no True when the money fields of this row are filled in.
26 valueState enum: OK, NO_PRICE, NOT_IN_PRICE_CHECK, NOT_MEASURED, HIDDEN, VALUES_CHECK_UNAVAILABLE no Why the money fields hold what they hold. OK means they are filled in.
27 notMeasuredReason enum: PRICES_NOT_YET_CHECKED, PRICE_CHECK_FAILED, NO_PRICE_CHECK_IN_7_DAYS yes Why the value is not measured, when valueState is NOT_MEASURED.
28 pricesCheckedAt date yes The price day: the newest day in the 7 days up to the check with prices.
29 quantityAtPricesCheckedAt integer yes Quantity on hand on the price day.
30 unitPrice (money) decimal yes The price per unit on the price day, 2 decimal places.
31 stockValue (money) decimal yes Quantity on the price day times the price that day, 2 decimal places.
32 captureLocalDate date no The day of the daily check this row comes from.
33 capturedAt datetime no When that check ran, in the zone of the row.
34 capturedAtUtc datetime no The same instant in UTC.
35 zone string no The time zone the check used.

`stock-history`, `grain=daily`#

# Field Type Can be empty Description
1 locationId string no OpenBoxes location id.
2 locationName string yes Location name at the daily check.
3 productId string no OpenBoxes product id.
4 productCode string yes Product code at the daily check.
5 productName string yes Product name at the daily check.
6 quantityOnHand integer no Quantity on hand at the check.
7 minQuantity integer yes Minimum set in OpenBoxes.
8 maxQuantity integer yes Maximum set in OpenBoxes.
9 priceCaptured boolean no True when that day's price check ran.
10 valuesIncluded boolean no True when the money fields of this row are filled in.
11 valueState enum: OK, NO_PRICE, NOT_MEASURED, HIDDEN, VALUES_CHECK_UNAVAILABLE no Why the money fields hold what they hold. OK means they are filled in.
12 notMeasuredReason enum: PRICES_NOT_YET_CHECKED, PRICE_CHECK_FAILED yes Why the value is not measured, when valueState is NOT_MEASURED.
13 pricesCheckedAt date yes The day of the price, which is the row's own day.
14 unitPrice (money) decimal yes The price per unit that day, 2 decimal places.
15 captureLocalDate date no The day of the daily check this row comes from.
16 capturedAt datetime no When that check ran, in the zone of the row.
17 capturedAtUtc datetime no The same instant in UTC.
18 zone string no The time zone the check used.

`stock-history`, `grain=monthly`#

# Field Type Can be empty Description
1 locationId string no OpenBoxes location id.
2 locationName string yes Location name at the daily check.
3 productId string no OpenBoxes product id.
4 productCode string yes Product code at the daily check.
5 productName string yes Product name at the daily check.
6 monthStart date no The first day of the month.
7 monthSource enum: ROLLUP, DAILY, ROLLUP_AND_DAILY no Where the month comes from: the monthly rollup, the daily rows, or both for the month where the daily history ends.
8 partialMonth boolean no True for the month in progress and the month where the daily history ends.
9 lastObservedDate date no The last day in the month with a check.
10 daysObserved integer no Days folded into the row.
11 qohMin integer no Lowest quantity on hand in the month.
12 qohMax integer no Highest quantity on hand in the month.
13 qohLast integer no Quantity on hand on lastObservedDate.
14 valuesIncluded boolean no True when the money fields of this row are filled in.
15 valueState enum: OK, NO_PRICE, NOT_MEASURED, HIDDEN, VALUES_CHECK_UNAVAILABLE no Why the money fields hold what they hold. OK means they are filled in.
16 notMeasuredReason enum: PRICES_NOT_YET_CHECKED, PRICE_CHECK_FAILED yes Why the value is not measured, when valueState is NOT_MEASURED. Always empty on monthly rows.
17 pricesCheckedAt date yes The month's last day with prices.
18 unitPriceLast (money) decimal yes The price per unit on pricesCheckedAt, 2 decimal places.
19 valueLast (money) decimal yes The value on pricesCheckedAt: that day's quantity times that day's price.
20 captureLocalDate date no Equal to lastObservedDate.
21 capturedAt datetime yes Always empty on monthly rows.
22 capturedAtUtc datetime yes Always empty on monthly rows.
23 zone string yes The time zone of the check on that day, when known.

`stock-value-history`#

The same columns for every grain; grain in each row says whether it is a day or a month.

# Field Type Can be empty Description
1 grain enum: daily, monthly no Whether the row is a day or a month.
2 periodStart date no The day, or the first day of the month.
3 periodEnd date no The day, or the last day of the month. For the month where the daily rows begin, the day before the first daily row.
4 valueDate date yes The day the value's prices come from. Not a quantity day.
5 measured boolean no True when a price check applies to the period.
6 pointState enum: OK, NO_PRICE, NOT_MEASURED, NOT_CAPTURED no The point's own state, whatever the key's grant.
7 pairsWithStock integer yes Products with stock.
8 pricedPairsWithStock integer yes Of those, products with a price.
9 valuesIncluded boolean no True when the money fields of this row are filled in.
10 valueState enum: OK, NO_PRICE, NOT_MEASURED, NOT_CAPTURED, HIDDEN, VALUES_CHECK_UNAVAILABLE no Why the money fields hold what they hold. OK means they are filled in.
11 notMeasuredReason enum: PRICES_NOT_YET_CHECKED, PRICE_CHECK_FAILED yes Why the value is not measured, when valueState is NOT_MEASURED. Always empty on monthly rows.
12 pricesCheckedAt date yes Equal to valueDate when measured.
13 stockValue (money) decimal yes Stock value for the period, 2 decimal places. Empty when not measured, never zero.
14 captureLocalDate date yes Equal to valueDate.
15 capturedAt datetime yes When the check ran. Daily rows only.
16 capturedAtUtc datetime yes The same instant in UTC. Daily rows only.
17 zone string yes The time zone of the check.

`lots-expiring`#

# Field Type Can be empty Description
1 locationId string no OpenBoxes location id.
2 locationName string yes Location name at the daily check.
3 productId string no OpenBoxes product id.
4 productCode string yes Product code at the daily check.
5 productName string yes Product name at the daily check.
6 inventoryItemId string no OpenBoxes inventory item (lot) id.
7 lotNumber string yes Lot number.
8 expirationDate date no Expiry date.
9 daysToExpiry integer no Days from the check day to expiry. Negative means expired.
10 expiryBucket enum: EXPIRED, WITHIN_30, WITHIN_60, WITHIN_90, WITHIN_180, WITHIN_365 no The expiry bucket.
11 withinWindow boolean no True when 0 <= daysToExpiry <= expiryWindowDays.
12 expiryWindowDays integer no The expiry window this walk uses, in days.
13 quantityOnHand integer no Quantity on hand in the lot.
14 valuesIncluded boolean no True when the money fields of this row are filled in.
15 valueState enum: OK, NO_PRICE, NOT_MEASURED, HIDDEN, VALUES_CHECK_UNAVAILABLE no Why the money fields hold what they hold. OK means they are filled in.
16 notMeasuredReason enum: PRICES_NOT_YET_CHECKED, PRICE_CHECK_FAILED yes Why the value is not measured, when valueState is NOT_MEASURED.
17 pricesCheckedAt date yes The check day, when its own prices exist.
18 unitPrice (money) decimal yes The price per unit on the check day, 2 decimal places.
19 valueAtRisk (money) decimal yes Quantity times the price per unit, 2 decimal places.
20 captureLocalDate date no The day of the daily check this row comes from.
21 capturedAt datetime no When that check ran, in the zone of the row.
22 capturedAtUtc datetime no The same instant in UTC.
23 zone string no The time zone the check used.

`purchase-orders-open`#

# Field Type Can be empty Description
1 orderId string no OpenBoxes order id.
2 orderNumber string yes Order number.
3 name string yes Order name.
4 status string yes OpenBoxes order status.
5 isDraft boolean no True for a draft order.
6 supplierName string yes Supplier name.
7 destinationLocationId string yes Destination location id.
8 destinationName string yes Destination location name.
9 dateOrdered date yes Date ordered.
10 expectedDate date yes Expected date.
11 expectedDateSource enum: shipment, ready, none no Where the expected date comes from.
12 overdue boolean no True when the shipment date is before the check day.
13 linesOpen integer no Open lines in this currency.
14 linesUnpriced integer no Of those, lines with no price.
15 quantityOutstanding decimal no Quantity not yet received, in each.
16 quantityPerUnitMissing boolean no True when a line has no quantity per unit of measure.
17 currencyCode string yes ISO currency code. Empty means the default currency.
18 currencyLabel string no The currency code, or default currency.
19 stale boolean no True when the order check is more than two days older than the stock check.
20 valuesIncluded boolean no True when the money fields of this row are filled in.
21 valueState enum: OK, NO_PRICE, HIDDEN, VALUES_CHECK_UNAVAILABLE no Why the money fields hold what they hold. OK means they are filled in.
22 notMeasuredReason enum: PRICES_NOT_YET_CHECKED, PRICE_CHECK_FAILED yes Why the value is not measured, when valueState is NOT_MEASURED. Always empty on this dataset.
23 pricesCheckedAt date yes The day of the order check, when values apply.
24 valueNotReceived (money) decimal yes Value not yet received in this row's currency, 2 decimal places. Never add rows of different currencies.
25 captureLocalDate date no The day of the daily check this row comes from.
26 capturedAt datetime no When that check ran, in the zone of the row.
27 capturedAtUtc datetime no The same instant in UTC.
28 zone string no The time zone the check used.

`activity`, `grain=daily`#

Activity has no money fields. transfer_in and transfer_out are included (rows are per location); moves inside one location are not.

# Field Type Can be empty Description
1 locationId string no OpenBoxes location id.
2 locationName string yes Location name at the daily check.
3 productId string no OpenBoxes product id.
4 productCode string yes Product code at the daily check.
5 productName string yes Product name at the daily check.
6 activityDate date no The day of the activity.
7 category enum: received_from_supplier, returned_by_location, issued, consumed, returned_to_supplier, expired, damaged, adjusted_up, adjusted_down, count_correction_up, count_correction_down, stock_loaded_up, stock_loaded_down, stock_count, transfer_in, transfer_out no The activity category.
8 label string no The category's label on the Reports page.
9 group enum: movement, lost, adjustment, count_correction, stock_loaded, count, transfer no The category's group.
10 openboxesType string yes The OpenBoxes transaction type behind it.
11 quantity integer yes Quantity moved. Empty for count-only categories.
12 transactions integer no Transactions.
13 importedHistoryCount integer no Transactions from imported history.
14 suspect boolean no True when the figures look implausible.
15 inMonthsOfStock boolean no True when the category feeds months of stock.
16 onGapDay boolean no True when the day is in a recorded gap.
17 captureLocalDate date yes The day of the daily check this row comes from.
18 capturedAt datetime yes When that check ran, in the zone of the row.
19 capturedAtUtc datetime yes The same instant in UTC.
20 zone string yes The time zone the check used.

`activity`, `grain=monthly`#

# Field Type Can be empty Description
1 locationId string no OpenBoxes location id.
2 locationName string yes Location name at the daily check.
3 productId string no OpenBoxes product id.
4 productCode string yes Product code at the daily check.
5 productName string yes Product name at the daily check.
6 monthStart date no The first day of the month.
7 monthSource enum: ROLLUP, DAILY, ROLLUP_AND_DAILY no Where the month comes from.
8 partialMonth boolean no True for the month in progress and the month where the daily history ends.
9 category enum: received_from_supplier, returned_by_location, issued, consumed, returned_to_supplier, expired, damaged, adjusted_up, adjusted_down, count_correction_up, count_correction_down, stock_loaded_up, stock_loaded_down, stock_count, transfer_in, transfer_out no The activity category.
10 label string no The category's label on the Reports page.
11 group enum: movement, lost, adjustment, count_correction, stock_loaded, count, transfer no The category's group.
12 openboxesType string yes The OpenBoxes transaction type behind it.
13 quantity integer yes Quantity moved. Empty for count-only categories.
14 transactions integer no Transactions.
15 importedHistoryCount integer no Transactions from imported history.
16 suspect boolean no True when the figures look implausible.
17 inMonthsOfStock boolean no True when the category feeds months of stock.
18 monthHasGap boolean no True when a recorded gap overlaps the month.
19 captureLocalDate date no The month's last recorded day.
20 capturedAt datetime yes Always empty on monthly rows.
21 capturedAtUtc datetime yes Always empty on monthly rows.
22 zone string yes The time zone of the check on that day, when known.

Coming from the Reports page#

The API uses the Reports page's names, except where a flat row needs a clearer one:

Reports page Reporting API
stock-outs (at zero, minimum set) summary.stockOuts
expiry window expiryWindowDays
draft and overdue orders summary.draftOrders, summary.overdueOrders
a row's stock state stock-on-hand.stockState
value state NO_PRICES valueState NO_PRICE
a value-over-time point's state stock-value-history.pointState
monthly products with stock, priced products pairsWithStock, pricedPairsWithStock
monthly value stock-value-history.stockValue
outstanding quantity (as text) quantityOutstanding (a number)

Dedicated and Enterprise#

The Reporting API works the same way on Dedicated and Enterprise as on Shared: the same host, the same datasets, the same 600-an-hour allowance.