Reports
The Reports resource provides read-only analytics aggregations over your tenant’s data, fourteen report types in all. Every endpoint requires the reports.read scope.
There are two shapes of report endpoint:
- The two originals:
/reports/summaryand/reports/revenue/daily, takestart/end(YYYY-MM-DD) and return their own dedicated response shapes. - The twelve passthrough reports (sales-summary, transaction-log, tax, margins, ar-aging, eod, store-credit, discounts, inventory-valuation, sales-by-customer, sales-by-item, turnaround) return the uniform report envelope. Windowed reports take
from/to;eodtakes a single day; as-of snapshot reports reject date windows. Check the report catalog for each endpoint. These are the same reports the in-app Reports tab renders, withdatasupplied by the app’s report engine.
Money fields follow the money object convention (amount_cents, amount, currency).
List available report types
Section titled “List available report types”GET /api/v1/reportsReturns an index of the available report endpoints with their required and optional parameters.
Scope required: reports.read
curl https://app.benchkey.com/api/v1/reports \ -H "Authorization: Bearer bk_live_<tenantId>_<secret>"Node.js
Section titled “Node.js”const res = await fetch("https://app.benchkey.com/api/v1/reports", { headers: { Authorization: "Bearer bk_live_<tenantId>_<secret>" },});const index = await res.json();Response
Section titled “Response”{ "object": "list", "data": [ { "report_type": "summary", "path": "/api/v1/reports/summary", "description": "Revenue, ticket volume, estimates, and device breakdown for a date window.", "required_params": ["start", "end"], "optional_params": ["location_id"] }, { "report_type": "revenue_daily", "path": "/api/v1/reports/revenue/daily", "description": "Day-by-day revenue series for a date window.", "required_params": ["start", "end"], "optional_params": ["location_id"] }, { "report_type": "sales-summary", "path": "/api/v1/reports/sales-summary", "description": "Net revenue, COGS, refunds, gross profit, and margin for a date window.", "required_params": ["from", "to"], "optional_params": ["location_id"] } ]}The live index lists all fourteen report types with their exact required and optional parameters, use it for discovery rather than hardcoding.
The report envelope
Section titled “The report envelope”The twelve passthrough reports all return the same stable wrapper:
{ "object": "report", "report": "sales-summary", "window": { "from": "2026-06-01", "to": "2026-06-30" }, "data": { "...": "the report's own JSON shape, unchanged from the in-app report engine" }}| Field | Type | Description |
|---|---|---|
object | string | Always "report" |
report | string | Report name, e.g. "sales-summary" |
window | object | The normalized date window, { "from": null, "to": null } for as-of snapshot reports |
data | object | The report payload. The shape varies per report and is single-sourced from the in-app report engine |
Report catalog
Section titled “Report catalog”| Report | Path | What it returns | Window | location_id |
|---|---|---|---|---|
sales-summary | GET /api/v1/reports/sales-summary | Net revenue, COGS, refunds, gross profit, and margin for a date window | from + to | yes |
transaction-log | GET /api/v1/reports/transaction-log | Every payment, refund, and cash-drawer adjustment in a date window | from + to | yes |
tax | GET /api/v1/reports/tax | Tax collected by jurisdiction for a date window (cash basis) | from + to | yes |
margins | GET /api/v1/reports/margins | Profitability / cost-of-goods analysis for a date window | from + to | yes |
ar-aging | GET /api/v1/reports/ar-aging | Outstanding receivables bucketed by age | as-of snapshot | yes |
eod | GET /api/v1/reports/eod | End-of-day (Z) report for a single day: payments by method, register sessions, cash drawer | from (single day) | yes |
store-credit | GET /api/v1/reports/store-credit | Outstanding store-credit liability by customer | as-of snapshot | no |
discounts | GET /api/v1/reports/discounts | Discounts given in a date window, by tech and by day | from + to | no |
inventory-valuation | GET /api/v1/reports/inventory-valuation | On-hand inventory quantity and value | as-of snapshot | yes |
sales-by-customer | GET /api/v1/reports/sales-by-customer | Top customers by paid revenue for a date window | from + to | no |
sales-by-item | GET /api/v1/reports/sales-by-item | Top line items by paid revenue for a date window | from + to | no |
turnaround | GET /api/v1/reports/turnaround | Ticket turnaround times: distribution, WIP aging, and medians for a date window | from + to | no |
Passthrough query parameters
Section titled “Passthrough query parameters”| Parameter | Type | Description |
|---|---|---|
from | string | Window start, ISO date (YYYY-MM-DD) or epoch milliseconds. Required for windowed reports; for eod it names the single day |
to | string | Window end (inclusive), ISO date (YYYY-MM-DD) or epoch milliseconds. Required for windowed reports; for eod it is optional and must resolve to the same day as from |
location_id | integer | Filter to a single location, only on reports marked “yes” above |
Parameter rules are strict rather than silently ignored: as-of snapshot reports reject from/to (400 invalid_query), and passing location_id to a report that doesn’t support it is also a 400 invalid_query, no placebo filters.
Example: sales summary
Section titled “Example: sales summary”curl "https://app.benchkey.com/api/v1/reports/sales-summary?from=2026-06-01&to=2026-06-30" \ -H "Authorization: Bearer bk_live_<tenantId>_<secret>"const params = new URLSearchParams({ from: "2026-06-01", to: "2026-06-30" });const res = await fetch( `https://app.benchkey.com/api/v1/reports/sales-summary?${params}`, { headers: { Authorization: "Bearer bk_live_<tenantId>_<secret>" } });const { report, window, data } = await res.json();Passthrough errors
Section titled “Passthrough errors”| HTTP | Code | Description |
|---|---|---|
| 400 | invalid_query | Missing/malformed from/to, from after to, a window supplied to an as-of snapshot report, eod given two different days, or location_id on a report that doesn’t support it |
| 400 | invalid_param | location_id is not a positive integer |
| 402 | payment_required | The tenant’s plan does not include advanced reports |
| 404 | not_found | location_id does not match any location |
Get analytics summary
Section titled “Get analytics summary”GET /api/v1/reports/summaryReturns aggregated revenue, ticket volume, estimate conversion, and device breakdown for a date window. Use this for dashboard-style metrics.
Scope required: reports.read
Query parameters
Section titled “Query parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
start | string (date) | Yes | Window start date, YYYY-MM-DD |
end | string (date) | Yes | Window end date, YYYY-MM-DD (inclusive) |
location_id | string | No | Filter to a single location (must be a positive integer); returns 404 if the location does not exist |
curl "https://app.benchkey.com/api/v1/reports/summary?start=2025-01-01&end=2025-01-31" \ -H "Authorization: Bearer bk_live_<tenantId>_<secret>"Node.js
Section titled “Node.js”const params = new URLSearchParams({ start: "2025-01-01", end: "2025-01-31" });const res = await fetch( `https://app.benchkey.com/api/v1/reports/summary?${params}`, { headers: { Authorization: "Bearer bk_live_<tenantId>_<secret>" } });const report = await res.json();Response
Section titled “Response”{ "object": "report", "report_type": "summary", "period": { "start": "2025-01-01", "end": "2025-01-31", "location_id": null }, "revenue": { "total": { "amount_cents": 1250000, "amount": "12500.00", "currency": "USD" }, "tax_collected": { "amount_cents": 112500, "amount": "1125.00", "currency": "USD" }, "total_refunded": { "amount_cents": 5000, "amount": "50.00", "currency": "USD" }, "paid_invoice_count": 143, "avg_invoice": { "amount_cents": 8741, "amount": "87.41", "currency": "USD" } }, "tickets": { "total_created": 211, "total_completed": 198, "current_backlog": 37, "avg_turnaround_hours": 14.2 }, "estimates": { "total_sent": 58, "approved": 41, "declined": 9, "pending": 8, "conversion_rate": "70.7", "avg_value": { "amount_cents": 9500, "amount": "95.00", "currency": "USD" }, "approved_revenue": { "amount_cents": 389500, "amount": "3895.00", "currency": "USD" } }, "devices": [ { "device": "iPhone 14 Pro", "count": 52 }, { "device": "Samsung Galaxy S23", "count": 34 }, { "device": "iPad Air", "count": 18 } ]}Field reference
Section titled “Field reference”period
Section titled “period”| Field | Type | Description |
|---|---|---|
start | string | Window start date |
end | string | Window end date (inclusive) |
location_id | string|null | Location filter applied, or null |
revenue
Section titled “revenue”| Field | Type | Description |
|---|---|---|
total | money | Gross revenue from paid and refunded invoices |
tax_collected | money | Sum of tax on paid/refunded invoices |
total_refunded | money | Sum of refunded amounts |
paid_invoice_count | integer | Number of paid or refunded invoices |
avg_invoice | money | Average invoice value (total / paid_invoice_count); $0.00 when count is 0 |
tickets
Section titled “tickets”All ticket-linked aggregates apply the same visibility contract as GET /api/v1/tickets: hidden, soft-deleted, and billing-only tickets are excluded from every field below.
| Field | Type | Description |
|---|---|---|
total_created | integer | Tickets created in the date window (excludes hidden, soft-deleted, and billing-only tickets) |
total_completed | integer | Completion events in the window (not undone); excludes completions linked to hidden/soft-deleted/billing-only tickets |
current_backlog | integer | Active (not-removed) queue entries right now; excludes entries linked to hidden/soft-deleted/billing-only tickets |
avg_turnaround_hours | number|null | Average hours from queue entry added to removed, for tickets completed in the window; excludes hidden/soft-deleted/billing-only tickets; null if no data |
estimates
Section titled “estimates”Estimate counts also apply the ticket visibility contract, estimates linked to hidden, soft-deleted, or billing-only tickets are excluded.
| Field | Type | Description |
|---|---|---|
total_sent | integer | Estimates created in the window (excludes those linked to hidden/soft-deleted/billing-only tickets) |
approved | integer | Estimates with an approved_at timestamp |
declined | integer | Estimates with a declined_at timestamp |
pending | integer | total_sent − approved − declined |
conversion_rate | string | Approval rate as a percentage string, e.g. "70.7" |
avg_value | money | Average estimate total across all sent |
approved_revenue | money | Sum of totals for approved estimates |
devices
Section titled “devices”Array of up to 30 entries, sorted by count descending:
| Field | Type | Description |
|---|---|---|
device | string | Device label from the ticket |
count | integer | Number of tickets for this device in the window (excludes billing-only and hidden tickets) |
Errors
Section titled “Errors”| HTTP | Code | Description |
|---|---|---|
| 400 | missing_params | start or end not provided |
| 400 | invalid_date_format | Either date is not YYYY-MM-DD |
| 400 | invalid_filter | Date passes format check but is not a real calendar date (e.g. month 13, day 32) |
| 400 | invalid_date_range | start is after end |
| 400 | invalid_param | location_id is not a positive integer |
| 400 | invalid_params | A parameter was passed as a non-scalar value (e.g. array) |
| 404 | not_found | location_id does not match any location |
Get daily revenue
Section titled “Get daily revenue”GET /api/v1/reports/revenue/dailyReturns a day-by-day revenue series for a date window. Read revenue_basis before interpreting the amounts: net_of_refunds uses the settlement projection, while gross identifies the legacy invoice-based calculation. Settlement activity is dated by money events, including refunds on their own dates. A refund-only day can therefore have negative revenue. Days without qualifying activity are omitted.
Scope required: reports.read
Query parameters
Section titled “Query parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
start | string (date) | Yes | Window start date, YYYY-MM-DD |
end | string (date) | Yes | Window end date, YYYY-MM-DD (inclusive) |
location_id | string | No | Filter to a single location (must be a positive integer); returns 404 if the location does not exist |
curl "https://app.benchkey.com/api/v1/reports/revenue/daily?start=2025-01-01&end=2025-01-07" \ -H "Authorization: Bearer bk_live_<tenantId>_<secret>"Node.js
Section titled “Node.js”const params = new URLSearchParams({ start: "2025-01-01", end: "2025-01-07" });const res = await fetch( `https://app.benchkey.com/api/v1/reports/revenue/daily?${params}`, { headers: { Authorization: "Bearer bk_live_<tenantId>_<secret>" } });const report = await res.json();Response
Section titled “Response”{ "object": "report", "report_type": "revenue_daily", "revenue_basis": "net_of_refunds", "period": { "start": "2025-01-01", "end": "2025-01-07", "location_id": null }, "data": [ { "date": "2025-01-01", "revenue": { "amount_cents": 42500, "amount": "425.00", "currency": "USD" }, "tax_collected": { "amount_cents": 3825, "amount": "38.25", "currency": "USD" }, "invoice_count": 5 }, { "date": "2025-01-02", "revenue": { "amount_cents": 91000, "amount": "910.00", "currency": "USD" }, "tax_collected": { "amount_cents": 8190, "amount": "81.90", "currency": "USD" }, "invoice_count": 11 }, { "date": "2025-01-03", "revenue": { "amount_cents": -5000, "amount": "-50.00", "currency": "USD" }, "tax_collected": { "amount_cents": -450, "amount": "-4.50", "currency": "USD" }, "invoice_count": 1 } ]}data entry fields
Section titled “data entry fields”| Field | Type | Description |
|---|---|---|
date | string | Calendar date (YYYY-MM-DD) |
revenue | money | Revenue for that day on the returned revenue_basis; settlement refunds subtract from it |
tax_collected | money | Tax for that day’s qualifying activity, including refund adjustments on the settlement basis |
invoice_count | integer | Distinct invoices with qualifying money activity that day on the settlement basis; a deposit without an invoice counts its estimate or ticket target once. The legacy gross basis counts paid or refunded invoices |
The top-level revenue_basis is net_of_refunds or gross. On the settlement basis, deposits without an invoice can contribute revenue, and the same invoice can count on both its payment day and a later refund day. Qualifying payments and refunds that cancel each other out can leave a zero-total day in the series. Imported shops may use the same legacy presentation as the app’s Overview within the settlement projection.
Only the gross fallback dates invoice totals by paid_at, falling back to created_at; it does not subtract refunds. The separate analytics summary retains its documented gross revenue semantics.
Errors
Section titled “Errors”| HTTP | Code | Description |
|---|---|---|
| 400 | missing_params | start or end not provided |
| 400 | invalid_date_format | Either date is not YYYY-MM-DD |
| 400 | invalid_filter | Date passes format check but is not a real calendar date (e.g. month 13, day 32) |
| 400 | invalid_date_range | start is after end |
| 400 | invalid_param | location_id is not a positive integer |
| 400 | invalid_params | A parameter was passed as a non-scalar value (e.g. array) |
| 404 | not_found | location_id does not match any location |