Reports & Digests

Read the numbers behind the shop — sales, payments, tax, commission, parts usage and the daily and weekly Smart Digest — with a read-only key.

Read this before you quote a number

  • Money units differ by report. The Money chip on each endpoint says whether its fields are integer cents or decimal dollars. Do not assume cents.
  • Dates are shop-local YYYY-MM-DD, inclusive at both ends, in the shop’s timezone.
  • Cached reports come from stored per-day rows kept current as invoices and payments change; they do not compute today live. If an answer must be exact, say which report it came from and its date range.
  • If a response carries truncated: true the result was capped; narrow the range instead of presenting it as complete.

Reports

get /report-aggregates/daily

Get raw daily report aggregates

Returns the stored per-day report rows for a date range, exactly as the platform keeps them: revenue by category, cost of sales, taxes, payments by method, invoice counts, customers and per-technician stats for each shop-local day. Use it when you need to build your own totals; the other /report-aggregates endpoints are summaries built from these rows. Days with no activity have no row and are omitted. There is no pagination: every row in the range comes back in one response. Both dates are required. Dates are shop-local calendar days (the shop's time zone from Settings; America/Chicago if none is set), written YYYY-MM-DD and inclusive at both ends. Validation: each date must be a real calendar day in YYYY-MM-DD form, must fall between 2000-01-01 and 2100-12-31, and startDate must be on or before endDate. Freshness (cached mode): the figures come from stored per-day rows that are updated as invoices, counter sales and payments change, and an overnight job (07:00 UTC, covering the last 45 days) recomputes them to correct any drift. This endpoint does not add a live calculation for today: today's numbers are whatever the stored row holds, so use mode=live to force a fresh recompute. All money fields end in Cents and are integer cents. Each row's technicianStats omits commissionCents and commissionRatePercent unless the caller holds reports:profit or attendance:financials.

Permission: reports:operational Money: cents everywhere (every money field is an integer *Cents field; paymentsByMethod values are cents).

Parameters

NameInTypeDescription
startDate *querystring dateFirst day of the range (inclusive), shop-local YYYY-MM-DD.
endDate *querystring dateLast day of the range (inclusive), shop-local YYYY-MM-DD.
curl "https://api.bigshopai.com/v1/report-aggregates/daily?startDate=2026-10-01&endDate=2026-10-09" \
  -H "Authorization: ApiKey bsk_live_..."

Response fields (first 3 levels)

FieldTypeDescription
aggregates[] *object[]Stored daily rows in ascending date order. No cursor: every row in the range is returned in one response.
aggregates[].tenantIdstringShop (tenant) id.
aggregates[].date *string dateShop-local calendar day this row covers.
aggregates[].laborRevenueCentsintegerLabor revenue on invoices dated this day. Integer cents.
aggregates[].partsRevenueCentsintegerParts revenue (work-order parts plus counter-sale parts). Integer cents.
aggregates[].tiresRevenueCentsintegerTire revenue. Integer cents.
aggregates[].subcontractsCentsintegerSubcontract / sublet revenue. Integer cents.
aggregates[].shopSuppliesCentsintegerShop supplies / materials revenue. Integer cents.
aggregates[].feesCentsintegerFee revenue (excludes EPA and catalog service items). Integer cents.
aggregates[].epaCentsintegerEPA / environmental fee, its own bucket since 2026-07. Absent on older rows (read as 0). Integer cents.
aggregates[].serviceItemsCentsintegerCatalog service items bucket. Absent on older rows (read as 0). Integer cents.
aggregates[].discountsCentsintegerDiscounts applied. Integer cents.
aggregates[].taxesCentsintegerSales tax billed on the day's invoices (work orders plus counter sales). Integer cents.
aggregates[].totalRevenueCents *integerInvoice grand total for the day (work orders plus counter sales). Card surcharge is NOT included. Integer cents.
aggregates[].laborCostCentsintegerLabor cost of sales. Absent on rows written before cost enrichment (read as 0). Integer cents.
aggregates[].partsCostCentsintegerParts cost of sales. Absent on older rows. Integer cents.
aggregates[].tiresCostCentsintegerTire cost of sales. Absent on older rows. Integer cents.
aggregates[].materialsCostCentsintegerShop-supplies / materials cost. Absent on older rows. Integer cents.
aggregates[].subcontractsCostCentsintegerSubcontract cost. Absent on older rows. Integer cents.
aggregates[].surchargeCentsintegerCard surcharge collected on the day's invoices (pass-through, outside totalRevenueCents). Absent on older rows. Integer cents.
aggregates[].paidSurchargeCentsintegerCard surcharge on fully paid invoices. Absent on older rows. Integer cents.
aggregates[].salesOrderRevenueCentsintegerCounter-sale (sales order) revenue. A SUBSET of totalRevenueCents, and its lines are also inside the category buckets above. Integer cents.
aggregates[].salesOrderTaxesCentsintegerCounter-sale portion of taxesCents. Integer cents.
aggregates[].paidSalesOrderRevenueCentsintegerCounter-sale revenue on paid sales orders. Integer cents.
aggregates[].salesOrderCostCentsintegerCounter-sale cost of goods. Absent on older rows. Integer cents.
aggregates[].unitemizedRevenueCentsintegerInvoiced revenue that produced no category line at all (a total with nothing itemized). Inside totalRevenueCents but outside the category buckets. Integer cents.
aggregates[].paidLaborRevenueCentsintegerLabor revenue on fully paid invoices. Integer cents.
aggregates[].paidPartsRevenueCentsintegerParts revenue on fully paid invoices. Integer cents.
aggregates[].paidTiresRevenueCentsintegerTire revenue on fully paid invoices. Integer cents.
aggregates[].paidSubcontractsCentsintegerSubcontract revenue on fully paid invoices. Integer cents.
aggregates[].paidShopSuppliesCentsintegerShop supplies revenue on fully paid invoices. Integer cents.
aggregates[].paidFeesCentsintegerFee revenue on fully paid invoices. Integer cents.
aggregates[].paidEpaCentsintegerEPA fee on fully paid invoices. Absent on older rows. Integer cents.
aggregates[].paidServiceItemsCentsintegerService items on fully paid invoices. Absent on older rows. Integer cents.
aggregates[].paidDiscountsCentsintegerDiscounts on fully paid invoices. Integer cents.
aggregates[].paidTaxesCentsintegerTax on fully paid invoices. Integer cents.
aggregates[].paidTotalRevenueCentsintegerGrand total of fully paid invoices. Integer cents.
aggregates[].paymentTotalCents *integerPayments received on this day, net of refunds and voids. Integer cents.
aggregates[].laborHoursTotalnumberTotal billed labor hours on invoiced orders that day, including lines with no technician named; declined work excluded. Absent on older rows. Differs from the sum of technicianStats[].laborHours, which counts only hours attributed to a named technician.
aggregates[].invoiceCount *integerInvoices dated this day (invoiced work orders plus counter sales).
aggregates[].paidInvoiceCountintegerOf those, fully paid.
aggregates[].unpaidInvoiceCountintegerOf those, not fully paid.
aggregates[].paymentCountintegerPayment transactions received that day (refund rows and voided payments are not counted).
aggregates[].paymentsByMethodobjectNet payments by method, in integer cents. Keys are the payment-method labels as recorded (free-form, e.g. Cash, Check, Credit Card; Other when none was recorded).
aggregates[].uniqueCustomerCountintegerDistinct customers on the day's invoices and payments. Always exact.
aggregates[].customerIds[]string[]Persisted customer id list. Capped to keep the row under the storage limit; see customerIdsTruncated.
aggregates[].customerIdsTruncatedbooleanTrue when customerIds was capped and is not exhaustive.
aggregates[].uniqueVehicleCountintegerDistinct vehicles on the day's invoices. Absent on older rows.
aggregates[].vehicleIds[]string[]
aggregates[].technicianStatsobjectPer-technician stats for the day, keyed by technician (user) id.
aggregates[].updatedAtstring date-timeWhen the row was last written.
count *integerNumber of rows returned.
period *object
period.startDate *string dateFirst day of the range (shop-local, inclusive).
period.endDate *string dateLast day of the range (shop-local, inclusive).

Errors

  • 400 — Either an invalid date range (error 'Invalid date range' with a message) or a missing parameter (error 'startDate and endDate are required', no message).
  • 403 — Caller lacks the required permission.
  • 500 — Unexpected error.
get /report-aggregates/sales-summary

Get sales summary

Answers 'how much did the shop invoice and collect over this period, and where did it come from?' Returns revenue split by labor, parts, tires, subcontracts, shop supplies, fees, EPA and service items, with discounts, tax and card surcharge, for all invoices and for fully paid invoices only, plus payments received (by method), unique customers, average repair order, counter-sale revenue, and daily trend series. Work orders and counter sales are both included. Invoices count on their invoiced date and payments on the date received. Dates are shop-local calendar days (the shop's time zone from Settings; America/Chicago if none is set), written YYYY-MM-DD and inclusive at both ends. Validation: each date must be a real calendar day in YYYY-MM-DD form, must fall between 2000-01-01 and 2100-12-31, and startDate must be on or before endDate. When omitted, startDate defaults to the first day of the current month and endDate to today, both in shop time. Freshness (cached mode): the figures come from stored per-day rows that are updated as invoices, counter sales and payments change, and an overnight job (07:00 UTC, covering the last 45 days) recomputes them to correct any drift. This endpoint does not add a live calculation for today: today's numbers are whatever the stored row holds, so use mode=live to force a fresh recompute. Money fields are DOLLARS (decimal numbers), not cents; they are cents divided by 100 and not rounded, so round to 2 decimals for display. No cursor pagination: trendData is limited to the last 30 days of the range (trendDataTruncated says when it was clipped) and trendDaily to 3000 days (trendDailyTruncated).

Permission: reports:operational Money: dollars for every money field (allInvoices, paidInvoices, payments, trendData, trendDaily, averageRepairOrder). Counts are plain integers. This is the opposite of the integer-cents convention used elsewhere in the API.

Parameters

NameInTypeDescription
startDatequerystring dateFirst day of the range (inclusive), shop-local YYYY-MM-DD. Defaults to the first day of the current month in shop time.
endDatequerystring dateLast day of the range (inclusive), shop-local YYYY-MM-DD. Defaults to today in shop time.
modequery"cached" | "live"
default "cached"
cached (default) reads the stored per-day aggregates. live recomputes from raw invoices and payments for the range without writing anything; it is slower (its payment lookup reads the shop's whole payment history, whatever the range). Any other value is treated as cached.
curl "https://api.bigshopai.com/v1/report-aggregates/sales-summary?startDate=2026-10-01&endDate=2026-10-09" \
  -H "Authorization: ApiKey bsk_live_..."

Response fields (first 3 levels)

FieldTypeDescription
summary *object
summary.period *object
summary.period.startDate *string dateFirst day of the range (shop-local, inclusive).
summary.period.endDate *string dateLast day of the range (shop-local, inclusive).
summary.allInvoices *objectTotals across every invoice dated in the range. Money fields are DOLLARS.
summary.allInvoices.laborRevenuenumberLabor revenue. Dollars as a decimal number (cents divided by 100, not rounded).
summary.allInvoices.partsRevenuenumberParts revenue (includes counter-sale parts). Dollars as a decimal number (cents divided by 100, not rounded).
summary.allInvoices.tiresRevenuenumberTire revenue. Dollars as a decimal number (cents divided by 100, not rounded).
summary.allInvoices.subcontractsnumberSubcontract revenue. Dollars as a decimal number (cents divided by 100, not rounded).
summary.allInvoices.shopSuppliesnumberShop supplies revenue. Dollars as a decimal number (cents divided by 100, not rounded).
summary.allInvoices.feesnumberFee revenue. Dollars as a decimal number (cents divided by 100, not rounded).
summary.allInvoices.epanumberEPA / environmental fee. Dollars as a decimal number (cents divided by 100, not rounded).
summary.allInvoices.serviceItemsnumberCatalog service items. Dollars as a decimal number (cents divided by 100, not rounded).
summary.allInvoices.subtotalnumberSum of the eight category lines above. Dollars as a decimal number (cents divided by 100, not rounded).
summary.allInvoices.discountsnumberDiscounts. Dollars as a decimal number (cents divided by 100, not rounded).
summary.allInvoices.postDiscountSubtotalnumbersubtotal minus discounts. Dollars as a decimal number (cents divided by 100, not rounded).
summary.allInvoices.taxesnumberSales tax. Dollars as a decimal number (cents divided by 100, not rounded).
summary.allInvoices.surchargenumberCard surcharge collected (pass-through; not included in total). Dollars as a decimal number (cents divided by 100, not rounded).
summary.allInvoices.totalnumberInvoice grand total (work orders plus counter sales). Card surcharge is not included. Dollars as a decimal number (cents divided by 100, not rounded).
summary.allInvoices.invoiceCountintegerNumber of invoices.
summary.allInvoices.laborCostnumberLabor cost of sales. Dollars as a decimal number (cents divided by 100, not rounded).
summary.allInvoices.partsCostnumberParts cost of sales. Dollars as a decimal number (cents divided by 100, not rounded).
summary.allInvoices.tiresCostnumberTire cost of sales. Dollars as a decimal number (cents divided by 100, not rounded).
summary.allInvoices.subcontractsCostnumberSubcontract cost of sales. Dollars as a decimal number (cents divided by 100, not rounded).
summary.allInvoices.shopSuppliesCostnumberShop supplies cost (the day's materials cost). Dollars as a decimal number (cents divided by 100, not rounded).
summary.paidInvoices *objectSame breakdown restricted to fully paid invoices. Money fields are DOLLARS.
summary.paidInvoices.laborRevenuenumberLabor revenue. Dollars as a decimal number (cents divided by 100, not rounded).
summary.paidInvoices.partsRevenuenumberParts revenue (includes counter-sale parts). Dollars as a decimal number (cents divided by 100, not rounded).
summary.paidInvoices.tiresRevenuenumberTire revenue. Dollars as a decimal number (cents divided by 100, not rounded).
summary.paidInvoices.subcontractsnumberSubcontract revenue. Dollars as a decimal number (cents divided by 100, not rounded).
summary.paidInvoices.shopSuppliesnumberShop supplies revenue. Dollars as a decimal number (cents divided by 100, not rounded).
summary.paidInvoices.feesnumberFee revenue. Dollars as a decimal number (cents divided by 100, not rounded).
summary.paidInvoices.epanumberEPA / environmental fee. Dollars as a decimal number (cents divided by 100, not rounded).
summary.paidInvoices.serviceItemsnumberCatalog service items. Dollars as a decimal number (cents divided by 100, not rounded).
summary.paidInvoices.subtotalnumberSum of the eight category lines above. Dollars as a decimal number (cents divided by 100, not rounded).
summary.paidInvoices.discountsnumberDiscounts. Dollars as a decimal number (cents divided by 100, not rounded).
summary.paidInvoices.postDiscountSubtotalnumbersubtotal minus discounts. Dollars as a decimal number (cents divided by 100, not rounded).
summary.paidInvoices.taxesnumberSales tax. Dollars as a decimal number (cents divided by 100, not rounded).
summary.paidInvoices.surchargenumberCard surcharge collected (pass-through; not included in total). Dollars as a decimal number (cents divided by 100, not rounded).
summary.paidInvoices.totalnumberInvoice grand total (work orders plus counter sales). Card surcharge is not included. Dollars as a decimal number (cents divided by 100, not rounded).
summary.paidInvoices.invoiceCountintegerNumber of invoices.
summary.payments *object
summary.payments.totalnumberPayments received in the range, net of refunds and voids. Dollars as a decimal number (cents divided by 100, not rounded).
summary.payments.countintegerPayment transactions.
summary.payments.byMethodobjectNet payments by method label.
summary.uniqueCustomers *integerDistinct customers across the range (union of the per-day customer id lists).
summary.salesOrderRevenuenumberCounter-sale revenue. It is inside allInvoices.total AND inside the category lines; do not add it on top. Dollars as a decimal number (cents divided by 100, not rounded).
summary.unitemizedRevenuenumberRevenue on invoices that produced no category line (total with nothing itemized). Inside total, outside the category lines. Dollars as a decimal number (cents divided by 100, not rounded).
summary.averageRepairOrder *numberallInvoices.total divided by invoiceCount; 0 when none. Not rounded to cents. Dollars as a decimal number (cents divided by 100, not rounded).
summary.trendData[] *object[]Daily series for the LAST 30 days of the range, zero-filled for days with no activity.
summary.trendData[].datestring dateShop-local day.
summary.trendData[].invoicednumberInvoice total that day. Dollars as a decimal number (cents divided by 100, not rounded).
summary.trendData[].paymentsnumberPayments received that day (net of refunds and voids). Dollars as a decimal number (cents divided by 100, not rounded).
summary.trendData[].customersintegerDistinct customers that day.
summary.trendDataTruncated *booleanTrue when the range is longer than 30 days and trendData holds only the most recent 30.
summary.trendDataDays *integerNumber of points in trendData.
summary.requestedRangeDays *integerDays in the requested range.
summary.trendDaily[] *object[]Rich per-day series across the WHOLE range, days with data only (no zero-fill), ascending.
summary.trendDaily[].datestring dateShop-local day.
summary.trendDaily[].invoicednumberInvoice grand total that day. Dollars as a decimal number (cents divided by 100, not rounded).
summary.trendDaily[].paymentsnumberPayments received that day. Dollars as a decimal number (cents divided by 100, not rounded).
summary.trendDaily[].profitnumberInvoice total minus labor, parts and tire cost only. The total includes sales tax and excludes materials and subcontract cost, so this mirrors the app's trend chart rather than accounting profit. Dollars as a decimal number (cents divided by 100, not rounded).
summary.trendDaily[].customersintegerDistinct customers.
summary.trendDaily[].ordersintegerInvoice count.
summary.trendDaily[].paidInvoicesintegerFully paid invoice count.
summary.trendDaily[].paidRevenuenumberGrand total of fully paid invoices. Dollars as a decimal number (cents divided by 100, not rounded).
summary.trendDaily[].laborRevenuenumberLabor revenue. Dollars as a decimal number (cents divided by 100, not rounded).
summary.trendDaily[].laborProfitnumberLabor revenue minus labor cost. Dollars as a decimal number (cents divided by 100, not rounded).
summary.trendDaily[].partsRevenuenumberParts revenue with tires folded in. Dollars as a decimal number (cents divided by 100, not rounded).
summary.trendDaily[].partsProfitnumberParts-plus-tire revenue minus parts-plus-tire cost. Dollars as a decimal number (cents divided by 100, not rounded).
summary.trendDaily[].laborHoursnumberBilled labor hours summed from technician stats, rounded to 0.1.
summary.trendDaily[].billableHoursnumberSame value as laborHours.
summary.trendDaily[].avgPerOrdernumberInvoice total divided by invoice count; 0 when there are no invoices. Dollars as a decimal number (cents divided by 100, not rounded).
summary.trendDailyTruncated *booleanTrue when more than 3000 days of data existed and only the most recent 3000 are returned.
dataMode *"cached" | "live"Which source produced the numbers.

Errors

  • 400 — Invalid date range. message is one of: 'Dates must be real calendar days, written YYYY-MM-DD.', 'Dates must fall between 2000 and 2100.', 'The start date must be on or before the end date.'
  • 403 — Caller lacks the required permission.
  • 500 — Unexpected error.
get /report-aggregates/payments

Get payments summary

Answers 'how much money came in over this period, by what method, and on which days?' Returns total payments received, the number of payment transactions, totals by payment method, and a per-day list. Amounts are net of refunds and voided payments; work-order and counter-sale payments are both included, each on the day it was received. Dates are shop-local calendar days (the shop's time zone from Settings; America/Chicago if none is set), written YYYY-MM-DD and inclusive at both ends. Validation: each date must be a real calendar day in YYYY-MM-DD form, must fall between 2000-01-01 and 2100-12-31, and startDate must be on or before endDate. When omitted, startDate defaults to the first day of the current month and endDate to today, both in shop time. Freshness (cached mode): the figures come from stored per-day rows that are updated as invoices, counter sales and payments change, and an overnight job (07:00 UTC, covering the last 45 days) recomputes them to correct any drift. This endpoint does not add a live calculation for today: today's numbers are whatever the stored row holds, so use mode=live to force a fresh recompute. Money fields are DOLLARS (decimal numbers), not cents. dailyTotals lists only days that have a stored row (no zero-fill).

Permission: reports:operational Money: dollars for total, byMethod values and dailyTotals[].total; counts are integers. Not integer cents.

Parameters

NameInTypeDescription
startDatequerystring dateFirst day of the range (inclusive), shop-local YYYY-MM-DD. Defaults to the first day of the current month in shop time.
endDatequerystring dateLast day of the range (inclusive), shop-local YYYY-MM-DD. Defaults to today in shop time.
modequery"cached" | "live"
default "cached"
cached (default) reads the stored per-day aggregates. live recomputes from raw invoices and payments for the range without writing anything; it is slower (its payment lookup reads the shop's whole payment history, whatever the range). Any other value is treated as cached.
curl "https://api.bigshopai.com/v1/report-aggregates/payments?startDate=2026-10-01&endDate=2026-10-09" \
  -H "Authorization: ApiKey bsk_live_..."

Response fields (first 3 levels)

FieldTypeDescription
summary *object
summary.period *object
summary.period.startDate *string dateFirst day of the range (shop-local, inclusive).
summary.period.endDate *string dateLast day of the range (shop-local, inclusive).
summary.total *numberPayments received in the range, net of refunds and voids. Dollars as a decimal number (cents divided by 100, not rounded).
summary.count *integerPayment transactions (refund rows and voided payments not counted).
summary.byMethod *objectNet payments by method label as recorded (free-form).
summary.dailyTotals[] *object[]One entry per day that has a stored row, ascending. Days with no stored row are omitted.
summary.dailyTotals[].date *string dateShop-local day the payments were received.
summary.dailyTotals[].total *numberNet payments that day. Dollars as a decimal number (cents divided by 100, not rounded).
summary.dailyTotals[].count *integerPayment transactions that day.
dataMode *"cached" | "live"Which source produced the numbers.

Errors

  • 400 — Invalid date range. message is one of: 'Dates must be real calendar days, written YYYY-MM-DD.', 'Dates must fall between 2000 and 2100.', 'The start date must be on or before the end date.'
  • 403 — Caller lacks the required permission.
  • 500 — Unexpected error.
get /report-aggregates/tax-collected

Get sales tax collected

Answers 'how much sales tax did the shop collect, at which rates, and what was taxable versus exempt?' Returns, separately for work orders and for counter sales, taxable sales, tax billed, tax received, outstanding tax, a breakdown per tax rate with a drill-down list of the orders behind each rate, a non-taxable section (including resale-certificate and exempt-organization sales), and a parts / labor / subcontract / other split. basis=accrual (default) reports by invoice date: tax billed, with received and outstanding figures pro-rated by how much of each invoice has been paid. basis=cash reports by payment date: only money actually received in the window, whatever period its invoice belongs to, so nothing is outstanding. Dates are shop-local calendar days (the shop's time zone from Settings; America/Chicago if none is set), written YYYY-MM-DD and inclusive at both ends. Validation: each date must be a real calendar day in YYYY-MM-DD form, must fall between 2000-01-01 and 2100-12-31, and startDate must be on or before endDate. When omitted, startDate defaults to the first day of the current month and endDate to today, both in shop time. The figures come from a per-order tax ledger kept current as invoices and payments change. If the ledger for the requested range looks incomplete (fewer than 95% of the expected orders, or rows in an older format) the request rebuilds it first and sets builtOnFirstView to true; that request is slower, and this GET therefore writes data. This endpoint has no mode parameter (a mode value is ignored). The orders drill-down is capped at 250 rows per rate tier and for the non-taxable list; ordersTruncated says when it was clipped, and totals are always exact. Order rows include the customer name. Money fields end in Cents and are integer cents; rates are percentages.

Permission: reports:operational Money: cents everywhere (*Cents fields, integers). Percent fields (taxRatePercent, effectiveTaxRatePercent) are percentages, not fractions. No dollar fields.

Parameters

NameInTypeDescription
startDatequerystring dateFirst day of the range (inclusive), shop-local YYYY-MM-DD. Defaults to the first day of the current month in shop time.
endDatequerystring dateLast day of the range (inclusive), shop-local YYYY-MM-DD. Defaults to today in shop time.
basisquery"accrual" | "cash"
default "accrual"
Accounting basis. accrual (default) = by invoice date; cash = by payment date. Case-insensitive; any other value is treated as accrual.
curl "https://api.bigshopai.com/v1/report-aggregates/tax-collected?startDate=2026-10-01&endDate=2026-10-09" \
  -H "Authorization: ApiKey bsk_live_..."

Response fields (first 3 levels)

FieldTypeDescription
period *object
period.startDate *string dateFirst day of the range (shop-local, inclusive).
period.endDate *string dateLast day of the range (shop-local, inclusive).
basis *"accrual" | "cash"Accounting basis of the figures.
workOrder *object
workOrder.taxableSalesCents *integerTaxable sales across all rate tiers. Integer cents.
workOrder.totalTaxCollectedCents *integerTax billed (accrual) / received (cash). Integer cents.
workOrder.totalTaxReceivedCents *integerTax actually received. Integer cents.
workOrder.nonTaxableSalesCents *integerNet sales on non-taxable orders. Integer cents.
workOrder.partsBilledCentsintegerParts billed. Integer cents.
workOrder.partsReceivedCentsintegerParts received. Integer cents.
workOrder.partsOutstandingCentsintegerParts outstanding. Integer cents.
workOrder.laborBilledCentsintegerLabor billed. Integer cents.
workOrder.laborReceivedCentsintegerLabor received. Integer cents.
workOrder.laborOutstandingCentsintegerLabor outstanding. Integer cents.
workOrder.taxBilledCentsintegerSame as totalTaxCollectedCents. Integer cents.
workOrder.taxReceivedCentsintegerSame as totalTaxReceivedCents. Integer cents.
workOrder.taxOutstandingCentsintegerTax billed minus received, floored at 0. Always 0 on cash basis. Integer cents.
workOrder.effectiveTaxRatePercent *number | nullTax collected divided by taxable sales, in percent rounded to 2 decimals; null when there are no taxable sales.
workOrder.surchargeReceivedCentsintegerCash basis only: card surcharge in the payments received (outside sales and tax). Integer cents.
workOrder.breakdown[] *object[]One bucket per distinct tax rate, ascending by rate.
workOrder.breakdown[].taxRatePercent *numberTax rate of this tier, in percent.
workOrder.breakdown[].taxableAmountCents *integerTaxable sales at this rate. Integer cents.
workOrder.breakdown[].taxCollectedCents *integerTax billed at this rate (cash basis: received). Integer cents.
workOrder.breakdown[].taxReceivedCents *integerTax received at this rate. Integer cents.
workOrder.breakdown[].taxOutstandingCentsintegerTax billed but not received. Integer cents.
workOrder.breakdown[].partsBilledCentsintegerParts billed. Integer cents.
workOrder.breakdown[].partsReceivedCentsintegerParts received. Integer cents.
workOrder.breakdown[].laborBilledCentsintegerLabor billed. Integer cents.
workOrder.breakdown[].laborReceivedCentsintegerLabor received. Integer cents.
workOrder.breakdown[].rateInferredbooleanCash basis only: true when every order in the tier had its rate back-calculated.
workOrder.breakdown[].orderCount *integerOrders in the tier (exact).
workOrder.breakdown[].orders[] *object[]Drill-down rows, capped at 250 per tier.
workOrder.breakdown[].ordersTruncated *booleanTrue when the tier has more than 250 orders and orders is clipped. Totals are always exact.
workOrder.nonTaxable *object
workOrder.nonTaxable.orderCount *integerOrders that carried no tax (exact).
workOrder.nonTaxable.salesCents *integerNet sales on those orders. Integer cents.
workOrder.nonTaxable.taxExemptCount *integerOf those, orders flagged tax exempt.
workOrder.nonTaxable.laborBilledCentsintegerLabor billed on non-taxable orders. Integer cents.
workOrder.nonTaxable.laborReceivedCentsintegerLabor received on non-taxable orders. Integer cents.
workOrder.nonTaxable.resaleBilledCentsintegerResale-certificate (CRT-61) sales, net of labor. Integer cents.
workOrder.nonTaxable.resaleReceivedCentsintegerReceived portion. Integer cents.
workOrder.nonTaxable.exemptOrgBilledCentsintegerExempt-organization sales, net of labor. Integer cents.
workOrder.nonTaxable.exemptOrgReceivedCentsintegerReceived portion. Integer cents.
workOrder.nonTaxable.orders[] *object[]Drill-down rows, capped at 250.
workOrder.nonTaxable.ordersTruncated *booleanTrue when orders is clipped at 250.
workOrder.categoriesobjectPer-category taxable / non-taxable split. other is the remainder (fees, shop supplies, EPA, service items) and is always treated as non-taxable. On cash basis the subcontract split is zero (the remainder lands in other).
workOrder.categories.partsobject
workOrder.categories.laborobject
workOrder.categories.subcontractobject
workOrder.categories.otherobject
workOrder.ordersTruncated *booleanTrue when any tier or the non-taxable list was clipped at 250 orders.
salesOrder *object
salesOrder.taxableSalesCents *integerTaxable sales across all rate tiers. Integer cents.
salesOrder.totalTaxCollectedCents *integerTax billed (accrual) / received (cash). Integer cents.
salesOrder.totalTaxReceivedCents *integerTax actually received. Integer cents.
salesOrder.nonTaxableSalesCents *integerNet sales on non-taxable orders. Integer cents.
salesOrder.partsBilledCentsintegerParts billed. Integer cents.
salesOrder.partsReceivedCentsintegerParts received. Integer cents.
salesOrder.partsOutstandingCentsintegerParts outstanding. Integer cents.
salesOrder.laborBilledCentsintegerLabor billed. Integer cents.
salesOrder.laborReceivedCentsintegerLabor received. Integer cents.
salesOrder.laborOutstandingCentsintegerLabor outstanding. Integer cents.
salesOrder.taxBilledCentsintegerSame as totalTaxCollectedCents. Integer cents.
salesOrder.taxReceivedCentsintegerSame as totalTaxReceivedCents. Integer cents.
salesOrder.taxOutstandingCentsintegerTax billed minus received, floored at 0. Always 0 on cash basis. Integer cents.
salesOrder.effectiveTaxRatePercent *number | nullTax collected divided by taxable sales, in percent rounded to 2 decimals; null when there are no taxable sales.
salesOrder.surchargeReceivedCentsintegerCash basis only: card surcharge in the payments received (outside sales and tax). Integer cents.
salesOrder.breakdown[] *object[]One bucket per distinct tax rate, ascending by rate.
salesOrder.breakdown[].taxRatePercent *numberTax rate of this tier, in percent.
salesOrder.breakdown[].taxableAmountCents *integerTaxable sales at this rate. Integer cents.
salesOrder.breakdown[].taxCollectedCents *integerTax billed at this rate (cash basis: received). Integer cents.
salesOrder.breakdown[].taxReceivedCents *integerTax received at this rate. Integer cents.
salesOrder.breakdown[].taxOutstandingCentsintegerTax billed but not received. Integer cents.
salesOrder.breakdown[].partsBilledCentsintegerParts billed. Integer cents.
salesOrder.breakdown[].partsReceivedCentsintegerParts received. Integer cents.
salesOrder.breakdown[].laborBilledCentsintegerLabor billed. Integer cents.
salesOrder.breakdown[].laborReceivedCentsintegerLabor received. Integer cents.
salesOrder.breakdown[].rateInferredbooleanCash basis only: true when every order in the tier had its rate back-calculated.
salesOrder.breakdown[].orderCount *integerOrders in the tier (exact).
salesOrder.breakdown[].orders[] *object[]Drill-down rows, capped at 250 per tier.
salesOrder.breakdown[].ordersTruncated *booleanTrue when the tier has more than 250 orders and orders is clipped. Totals are always exact.
salesOrder.nonTaxable *object
salesOrder.nonTaxable.orderCount *integerOrders that carried no tax (exact).
salesOrder.nonTaxable.salesCents *integerNet sales on those orders. Integer cents.
salesOrder.nonTaxable.taxExemptCount *integerOf those, orders flagged tax exempt.
salesOrder.nonTaxable.laborBilledCentsintegerLabor billed on non-taxable orders. Integer cents.
salesOrder.nonTaxable.laborReceivedCentsintegerLabor received on non-taxable orders. Integer cents.
salesOrder.nonTaxable.resaleBilledCentsintegerResale-certificate (CRT-61) sales, net of labor. Integer cents.
salesOrder.nonTaxable.resaleReceivedCentsintegerReceived portion. Integer cents.
salesOrder.nonTaxable.exemptOrgBilledCentsintegerExempt-organization sales, net of labor. Integer cents.
salesOrder.nonTaxable.exemptOrgReceivedCentsintegerReceived portion. Integer cents.
salesOrder.nonTaxable.orders[] *object[]Drill-down rows, capped at 250.
salesOrder.nonTaxable.ordersTruncated *booleanTrue when orders is clipped at 250.
salesOrder.categoriesobjectPer-category taxable / non-taxable split. other is the remainder (fees, shop supplies, EPA, service items) and is always treated as non-taxable. On cash basis the subcontract split is zero (the remainder lands in other).
salesOrder.categories.partsobject
salesOrder.categories.laborobject
salesOrder.categories.subcontractobject
salesOrder.categories.otherobject
salesOrder.ordersTruncated *booleanTrue when any tier or the non-taxable list was clipped at 250 orders.
builtOnFirstView *booleanTrue when this request had to rebuild the tax ledger for the range before answering (slower than usual).

Errors

  • 400 — Invalid date range. message is one of: 'Dates must be real calendar days, written YYYY-MM-DD.', 'Dates must fall between 2000 and 2100.', 'The start date must be on or before the end date.'
  • 403 — Caller lacks the required permission.
  • 500 — Unexpected error.
get /report-aggregates/commission

Get technician commission report

Answers 'what did each technician bill and earn in commission over this period?' Returns per technician the billed labor hours, labor revenue, jobs completed, commission rate and commission earned, plus shop totals, sorted by commission (highest first). Commission is taken from what was stored on each day's invoices at the time (using the technician's commission rate then); if a technician has a rate and labor revenue but stored commission of 0, it is recomputed as revenue times rate. Labor discounts do not reduce commission. Dates are shop-local calendar days (the shop's time zone from Settings; America/Chicago if none is set), written YYYY-MM-DD and inclusive at both ends. Validation: each date must be a real calendar day in YYYY-MM-DD form, must fall between 2000-01-01 and 2100-12-31, and startDate must be on or before endDate. When omitted, startDate defaults to the first day of the current month and endDate to today, both in shop time. Freshness (cached mode): the figures come from stored per-day rows that are updated as invoices, counter sales and payments change, and an overnight job (07:00 UTC, covering the last 45 days) recomputes them to correct any drift. This endpoint does not add a live calculation for today: today's numbers are whatever the stored row holds, so use mode=live to force a fresh recompute. Money fields are DOLLARS (decimal numbers), not cents; they are re-derived from cents with floating-point arithmetic, so round to 2 decimals for display. Rates are percentages. A mechanicId that matches nobody returns an empty list and zero totals, not a 404. This is pay data: callers who hold only reports:operational get 403.

Permission: reports:profit or attendance:financials Money: dollars for laborRevenue, commission, totalLaborRevenue, totalCommission; commissionRatePercent and averageCommissionRate are percentages; hours are decimal hours. Not integer cents.

Parameters

NameInTypeDescription
startDatequerystring dateFirst day of the range (inclusive), shop-local YYYY-MM-DD. Defaults to the first day of the current month in shop time.
endDatequerystring dateLast day of the range (inclusive), shop-local YYYY-MM-DD. Defaults to today in shop time.
modequery"cached" | "live"
default "cached"
cached (default) reads the stored per-day aggregates. live recomputes from raw invoices and payments for the range without writing anything; it is slower (its payment lookup reads the shop's whole payment history, whatever the range). Any other value is treated as cached.
mechanicIdquerystringRestrict the report to one technician (user id, exact match).
curl "https://api.bigshopai.com/v1/report-aggregates/commission?startDate=2026-10-01&endDate=2026-10-09" \
  -H "Authorization: ApiKey bsk_live_..."

Response fields (first 3 levels)

FieldTypeDescription
report *object
report.period *object
report.period.startDate *string dateFirst day of the range (shop-local, inclusive).
report.period.endDate *string dateLast day of the range (shop-local, inclusive).
report.summary *object
report.summary.totalLaborRevenue *numberLabor revenue attributed to the listed technicians. Dollars as a decimal number (cents divided by 100, not rounded).
report.summary.totalCommission *numberSum of commission across the listed technicians. Dollars as a decimal number (cents divided by 100, not rounded).
report.summary.totalHours *numberBilled labor hours across the listed technicians.
report.summary.totalJobs *integerJobs completed across the listed technicians.
report.summary.averageCommissionRate *numbertotalCommission divided by totalLaborRevenue, in percent; 0 when there is no labor revenue.
report.mechanics[] *object[]Sorted by commission, highest first.
report.mechanics[].mechanicId *stringTechnician (user) id.
report.mechanics[].mechanicName *stringTechnician name; Unknown when none was recorded.
report.mechanics[].laborHours *numberBilled labor hours.
report.mechanics[].laborRevenue *numberLabor revenue attributed to the technician. Dollars as a decimal number (cents divided by 100, not rounded).
report.mechanics[].jobsCompleted *integerJobs completed.
report.mechanics[].commissionRatePercent *numberMost recent non-zero commission rate seen in the range, in percent.
report.mechanics[].commission *numberCommission earned. If stored commission is 0 but a rate and labor revenue exist, it is recomputed as revenue times rate. Dollars as a decimal number (cents divided by 100, not rounded).
dataMode *"cached" | "live"Which source produced the numbers.

Errors

  • 400 — Invalid date range. message is one of: 'Dates must be real calendar days, written YYYY-MM-DD.', 'Dates must fall between 2000 and 2100.', 'The start date must be on or before the end date.'
  • 403 — Caller lacks the required permission.
  • 500 — Unexpected error.
get /report-aggregates/part-usage

Get part usage totals

Answers 'which parts does the shop actually use, how many, and when was each last used?' Returns one entry per part with the quantity used on invoiced work orders over the last 180 days and over all recorded history, the latest unit cost, and the last-used date; it feeds inventory-health and parts-analytics reports. Parts are keyed by part number (or description when there is no part number). Only part lines on invoiced work orders count: tire and shop-supply lines and counter sales are not in these totals. Takes no date parameters. If startDate or endDate are sent they are validated by the shared date check (and can produce a 400) but are otherwise ignored. The totals are rebuilt once a night from a per-invoice usage ledger (the same 07:00 UTC job as the daily aggregates), so today's invoices do not appear until the next rebuild; updatedAt shows when the newest rows were written. There is no pagination and no limit: every part is returned in one response, so the payload grows with the size of the shop's parts history. totalCostCents is a unit cost in integer cents, not a total.

Permission: reports:operational Money: cents (totalCostCents, integer). It is a per-unit cost despite the name. Quantities are plain numbers.
curl "https://api.bigshopai.com/v1/report-aggregates/part-usage" \
  -H "Authorization: ApiKey bsk_live_..."

Response fields (first 3 levels)

FieldTypeDescription
parts[] *object[]Every part with recorded usage, unordered. No cursor and no limit parameter.
parts[].key *stringPart key: the part number, or the description when there is no part number, upper-cased and trimmed.
parts[].partNumber *stringPart number; empty string when the part has none.
parts[].description *stringLatest description seen.
parts[].category *stringLatest category seen; Parts when none.
parts[].qtyUsed180 *numberQuantity on invoiced work orders dated within the last 180 days, measured from the date of the nightly rebuild (UTC date).
parts[].qtyUsedAll *numberQuantity on all invoiced work orders held in the part-usage ledger.
parts[].totalCostCents *integerDespite its name this is NOT a running total: it is the latest non-zero UNIT cost seen on an invoice (0 if none). Integer cents.
parts[].lastUsedDate *stringMost recent invoice date the part was used (YYYY-MM-DD); empty string when unknown.
updatedAt *string | null date-timeLatest rebuild timestamp across the rows; null when there are no rows.
count *integerNumber of parts returned.

Errors

  • 400 — Invalid date range. message is one of: 'Dates must be real calendar days, written YYYY-MM-DD.', 'Dates must fall between 2000 and 2100.', 'The start date must be on or before the end date.'
  • 403 — Caller lacks the required permission.
  • 500 — Unexpected error.
get /reports/customer-aging

Customer aging (accounts receivable) as of a day

Answers: who owes the shop money, and how overdue is it? Returns one row per customer with their unpaid invoice balances split into Current (not yet due), 1-30, 31-60, 61-90 and Over 90 days past due, as the books stood at the end of the chosen day in the shop's time zone. mode=asOf (default) replays invoices and dated payments, so it works for any past day and for today (pass today's date for the current picture); generatedAt is the request time. mode=snapshot returns the end-of-day row stored by the scheduled snapshot job; if no snapshot was stored for that day you get HTTP 200 with rows: [] and meta.snapshotFound=false (not an error). Use GET /reports/customer-aging/snapshots to see which days exist. An invoice is due 30 days after its invoice date unless the work order has its own due date. Only invoiced work orders that have a customer id are included; counter sales and invoices without a customer are not read. UNITS: every amount in rows is DOLLARS (decimal), but meta.untraceablePaidCents is cents.

Permission: reports:operational

Parameters

NameInTypeDescription
modequery"asOf" | "snapshot"
default "asOf"
asOf replays history; snapshot reads the stored end-of-day row. Any value other than snapshot is treated as asOf.
asOf *querystring dateShop-local day, YYYY-MM-DD. Required. Must not be later than today in the shop's time zone.
curl "https://api.bigshopai.com/v1/reports/customer-aging" \
  -H "Authorization: ApiKey bsk_live_..."

Response fields (first 3 levels)

FieldTypeDescription
mode *"asOf" | "snapshot"
asOf *string dateEcho of the requested day.
generatedAt *string date-timeasOf mode: request time. snapshot mode: when the stored snapshot was written (or request time when no snapshot exists).
rows[] *object[]
rows[].customerId *string
rows[].customer *stringCustomer name; Unknown when the work order carries none.
rows[].current *numberBalance not yet past due. DOLLARS as a decimal number (not cents).
rows[].days30 *numberBalance 1-30 days past due. DOLLARS as a decimal number (not cents).
rows[].days60 *numberBalance 31-60 days past due. DOLLARS as a decimal number (not cents).
rows[].days90 *numberBalance 61-90 days past due. DOLLARS as a decimal number (not cents).
rows[].over90 *numberBalance more than 90 days past due. DOLLARS as a decimal number (not cents).
rows[].total *numberSum of the five buckets. DOLLARS as a decimal number (not cents).
rows[].invoiceCount *integerOpen invoices contributing to this row.
rows[].invoices[]object[]Present in mode=asOf; ABSENT in mode=snapshot (snapshots store bucket totals only).
rows[].invoices[].workOrderIdstring
rows[].invoices[].invoiceNumberstringinvoiceNumber, else orderNumber, else the work-order id.
rows[].invoices[].invoiceDatestring date-timeInstant the invoice is considered issued: invoicedAt, else invoiceDate, else createdAt.
rows[].invoices[].dueDatestring date-timeThe work order's own dueDate if set, otherwise invoice date + 30 calendar days.
rows[].invoices[].daysPastDueintegerWhole shop-local calendar days past the due date, clamped to 0 (never negative in this object).
rows[].invoices[].totalAmountnumberInvoice total (current value, not as of the date). DOLLARS as a decimal number (not cents).
rows[].invoices[].paidAmountnumberPayments applied on or before the as-of day, net of refunds dated on or before it. DOLLARS as a decimal number (not cents).
rows[].invoices[].balancenumbertotalAmount - paidAmount (always > 0 here; settled invoices are omitted). DOLLARS as a decimal number (not cents).
rows[].invoices[].bucket"Current" | "1-30" | "31-60" | "61-90" | "Over 90"
rows[].invoices[].vehiclestring
rows[].invoices[].unitNumberstring
rows[].invoices[].vinstring
rows[].invoices[].archivedboolean
meta *object
meta.reconstructed *booleantrue for asOf (replayed from invoices + dated payments); false for snapshot.
meta.snapshotFoundbooleansnapshot mode only. false means the nightly job did not store a row for that day; rows is then [].
meta.truncatedbooleansnapshot mode only. true when the stored snapshot kept only the largest balances to fit the 350 KB item cap.
meta.untraceablePaidCentsintegerPaid amount on in-scope invoices that have a header paid total but no dated payment rows (imported/migrated invoices); treated as paid from issue date. NOTE: this one field is CENTS while rows are dollars. Integer cents.
meta.untraceableInvoiceCountintegerInvoices contributing to untraceablePaidCents (asOf mode only).

Errors

  • 400 — {"error":"asOf must be a YYYY-MM-DD date"} (missing/malformed) or {"error":"asOf cannot be in the future"}.
  • 403 — Forbidden. The caller's role lacks reports:operational.
  • 500 — Server error. Body is {"error": "Failed to load historical aging"}; internal detail is logged, not returned.
get /reports/customer-aging/snapshots

List days that have a stored aging snapshot

Answers: for which days can I read the stored (audit-grade) receivables snapshot? Returns the dates, newest first. Use one of them as asOf with mode=snapshot on GET /reports/customer-aging.

Permission: reports:operational
curl "https://api.bigshopai.com/v1/reports/customer-aging/snapshots" \
  -H "Authorization: ApiKey bsk_live_..."

Response fields (first 3 levels)

FieldTypeDescription
dates[] *string date[]Days for which a stored end-of-day aging snapshot exists, newest first.

Errors

  • 403 — Forbidden. The caller's role lacks reports:operational.
  • 500 — Server error. Body is {"error": "Failed to list snapshots"}; internal detail is logged, not returned.
get /reports/weekly-trend

Week-over-week revenue and profit trend

Answers: how is the shop trending week to week? Returns up to weeks weekly data points (Monday-Sunday in the shop's time zone), oldest to newest: revenue, labor and parts revenue and profit, labor hours, jobs, customers and invoice count. These are pre-computed rollups, not live. The nightly rollup (00:00 UTC) recomputes the current and the previous week; older weeks only change on a backfill. So the current week reflects the last nightly run, not this minute. UNITS: all money is integer cents. (A stale code comment says dollars; the payload is cents.) revenueCents includes tax; partsRevenueCents/partsProfitCents include tires.

Permission: reports:operational

Parameters

NameInTypeDescription
weeksqueryinteger
default 12, range 1–52
How many most-recent weeks to return. Values above 52 are clamped to 52, below 1 to 1, and non-numeric input falls back to 12.
curl "https://api.bigshopai.com/v1/reports/weekly-trend" \
  -H "Authorization: ApiKey bsk_live_..."

Response fields (first 3 levels)

FieldTypeDescription
weeklyTrend[] *object[]Oldest to newest. Weeks with no stored rollup row are simply absent, so fewer than weeks items can come back.
weeklyTrend[].weekStart *string dateMonday of the week, shop-local.
weeklyTrend[].weekEnd *string dateSunday of the week, shop-local.
weeklyTrend[].revenueCents *integerTotal invoiced revenue INCLUDING tax (net-of-tax gross + taxes). Integer cents.
weeklyTrend[].laborRevenueCents *integerLabor revenue. Integer cents.
weeklyTrend[].partsRevenueCents *integerParts revenue PLUS tires revenue. Integer cents.
weeklyTrend[].laborProfitCents *integerLabor revenue minus labor cost. Labor cost is hours x each technician's hourly cost rate, so this embeds the shop's technician wage cost in aggregate. Integer cents.
weeklyTrend[].partsProfitCents *integerParts profit PLUS tires profit (revenue - cost). Integer cents.
weeklyTrend[].laborHours *numberBillable labor hours for the week, rounded to 2 dp.
weeklyTrend[].jobs *integerEquals invoiceCount (the rollup sets jobsCompleted = invoiceCount).
weeklyTrend[].customers *integerDistinct customers invoiced in the week.
weeklyTrend[].invoiceCount *integerInvoices in the week.

Errors

  • 403 — Forbidden. The caller's role lacks reports:operational.
  • 500 — Server error. Body is {"error": "Failed to get weekly trend"}; internal detail is logged, not returned.

Digests

get /digests

List daily or weekly digests

Lists the shop's AI morning briefings, newest first, without the full text: headline, date, how it was produced, delivery status and any thumbs rating. A daily digest covers the previous completed business day in the shop's time zone and a weekly digest covers the last completed Monday-to-Sunday week (dated by its Monday). Digests exist only for shops that have the feature switched on: an hourly job writes one after the shop's send hour (5-9 am shop time, default 6), so a shop that never enabled it gets an empty list. Each digest is created once and not updated as the day goes on (regenerating overwrites it). Admin role required; API keys carry it. Pagination: none. The response holds the most recent limit digests only (1-60, default 30); there is no cursor and older digests cannot be reached through this endpoint once they fall outside the newest 60. Fetch a specific older one with GET /digests/{date}.

Permission: shop admin role (API keys qualify) Money: No money fields on this endpoint.

Parameters

NameInTypeDescription
periodquery"daily" | "weekly"
default "daily"
weekly selects the weekly digests (keyed by the Monday of the covered week). Any other value, or omitting it, selects daily digests.
limitqueryinteger
default 30, range 1–60
How many digests to return, newest first. Clamped to 1-60; a missing, non-numeric or 0 value gives the default of 30.
curl "https://api.bigshopai.com/v1/digests" \
  -H "Authorization: ApiKey bsk_live_..."

Response fields (first 3 levels)

FieldTypeDescription
digests[] *object[]Newest first.
digests[].businessDate *string dateDaily: the shop-local day the digest covers (the completed previous day). Weekly: the Monday of the covered week.
digests[].period *"daily" | "weekly"Cadence.
digests[].weekEnd *string | null dateWeekly only: the Sunday of the covered week; null for daily.
digests[].headline *stringOne-line headline (<= 120 chars).
digests[].renderMode *"llm" | "fallback"llm = model-written narrative that passed number validation; fallback = deterministic template used instead.
digests[].status *"generated" | "delivered" | "failed"Lifecycle state.
digests[].feedbackScore *"up" | "down"Owner thumbs rating, if given.
digests[].createdAt *string date-timeWhen the digest row was created.

Errors

  • 401 — Caller's tenant could not be resolved.
  • 403 — Caller is not an admin.
  • 500 — Unexpected error.
get /digests/{date}

Get one digest

Returns one full digest: the written briefing (headline, summary, bullets, wins, concerns, things to watch, recommended actions, callouts) and the data snapshot it was built from (revenue, labor, parts, payments, gross profit, open work, receivables and so on, each with comparisons to yesterday, last week or the prior four weeks). Every figure in the narrative is copied from a pre-formatted display string in the snapshot; the model never computes numbers, and if validation fails a deterministic template is used (renderMode = fallback). Money in the snapshot is integer cents, each with a matching display string. Keys inside narrative and snapshot are snake_case. For daily digests {date} is the covered day. For weekly digests (period=weekly) {date} must be the Monday of the covered week; any other date returns 404 because the lookup is by exact key. {date} must match YYYY-MM-DD; it is not checked for being a real calendar day, so an impossible date such as 2026-02-31 simply returns 404. A digest that does not exist, or that belongs to another shop, returns 404. Admin role required; API keys carry it. The digest is a fixed record created once; it does not change as the day goes on.

Permission: shop admin role (API keys qualify) Money: cents (integer *cents fields inside snapshot) plus pre-formatted *_display strings in US dollars with thousands separators (e.g. $4,832.50). No dollar-number fields.

Parameters

NameInTypeDescription
date *pathstring dateBusiness date, YYYY-MM-DD. Daily: the covered day. Weekly: the Monday of the covered week.
periodquery"daily" | "weekly"
default "daily"
weekly selects the weekly digests (keyed by the Monday of the covered week). Any other value, or omitting it, selects daily digests.
curl "https://api.bigshopai.com/v1/digests/2026-10-09" \
  -H "Authorization: ApiKey bsk_live_..."

Response fields (first 3 levels)

FieldTypeDescription
businessDate *string dateDaily: the shop-local day the digest covers (the completed previous day). Weekly: the Monday of the covered week.
period *"daily" | "weekly"Cadence.
weekEnd *string | null dateWeekly only: the Sunday of the covered week; null for daily.
headline *stringOne-line headline (<= 120 chars).
renderMode *"llm" | "fallback"llm = model-written narrative that passed number validation; fallback = deterministic template used instead.
status *"generated" | "delivered" | "failed"Lifecycle state.
feedbackScore *"up" | "down"Owner thumbs rating, if given.
createdAt *string date-timeWhen the digest row was created.
narrative *Parsed narrative; null if the stored JSON could not be parsed.
snapshot *Parsed snapshot; null if the stored JSON could not be parsed.
feedbackComment *string | nullOwner's feedback comment, if any.
deliveredAt *string | null date-timeWhen the digest was delivered in-app.
emailedAt *string | null date-timeWhen the digest email was sent.

Errors

  • 401 — Caller's tenant could not be resolved.
  • 403 — Caller is not an admin.
  • 404 — No digest for that date and period (also returned for a path that is not a YYYY-MM-DD date).
  • 500 — Unexpected error.

Dashboard

get /dashboard/trend-aggregates

Get daily trend aggregates

Answers 'how did invoicing and payments trend day by day over this range?' Returns one entry per shop-local day that has a stored aggregate: invoice count and revenue (total, labor, parts, tires, counter sales), the same for fully paid invoices, payments received, unique customers, and per-technician stats. It reads the same stored per-day rows as /report-aggregates/daily, converted for charting. Both dates are required. Unlike the /report-aggregates endpoints this one does NOT run the shared date validation: dates are not checked for format or range, startDate after endDate is not rejected up front, and the 2000-2100 bounds do not apply. Send real YYYY-MM-DD dates with startDate <= endDate. Freshness: reads stored daily rows, which are updated as invoices and payments change and recomputed by the overnight job (07:00 UTC, last 45 days); there is no live recalculation here, and there is no period parameter. No pagination: the endpoint issues a single database query and does not follow a continuation key, so a very wide range could be cut off at the first ~1 MB of rows with no truncated flag. Keep ranges to a few months. Money units are mixed: all top-level revenue and payment fields are DOLLARS (decimal numbers), but money inside technicianStats (laborRevenueCents, commissionCents) is still integer CENTS. technicianStats omits commissionCents and commissionRatePercent unless the caller holds reports:profit or attendance:financials.

Permission: dashboard:read Money: MIXED: dollars for totalRevenue, laborRevenue, partsRevenue, tiresRevenue, paid*, salesOrderRevenue, paymentTotal; integer cents for technicianStats[*].laborRevenueCents and commissionCents; commissionRatePercent is a percentage; counts are integers.

Parameters

NameInTypeDescription
startDate *querystring dateFirst day of the range (inclusive), shop-local YYYY-MM-DD. Not validated by this endpoint.
endDate *querystring dateLast day of the range (inclusive), shop-local YYYY-MM-DD. Not validated by this endpoint.
curl "https://api.bigshopai.com/v1/dashboard/trend-aggregates?startDate=2026-10-01&endDate=2026-10-09" \
  -H "Authorization: ApiKey bsk_live_..."

Response fields (first 3 levels)

FieldTypeDescription
trendData[] *object[]One entry per day that has a stored row, ascending. Days with no stored row are omitted.
trendData[].date *string dateShop-local day.
trendData[].invoiceCount *integerInvoices that day.
trendData[].totalRevenue *numberInvoice grand total. Dollars as a decimal number (cents divided by 100, not rounded).
trendData[].laborRevenuenumberLabor revenue. Dollars as a decimal number (cents divided by 100, not rounded).
trendData[].partsRevenuenumberParts revenue. Dollars as a decimal number (cents divided by 100, not rounded).
trendData[].tiresRevenuenumberTire revenue. Dollars as a decimal number (cents divided by 100, not rounded).
trendData[].paidInvoiceCountintegerFully paid invoices.
trendData[].paidTotalRevenuenumberGrand total of fully paid invoices. Dollars as a decimal number (cents divided by 100, not rounded).
trendData[].paidLaborRevenuenumberLabor revenue on fully paid invoices. Dollars as a decimal number (cents divided by 100, not rounded).
trendData[].paidPartsRevenuenumberParts revenue on fully paid invoices. Dollars as a decimal number (cents divided by 100, not rounded).
trendData[].salesOrderRevenuenumberCounter-sale revenue (a subset of totalRevenue). Dollars as a decimal number (cents divided by 100, not rounded).
trendData[].paidSalesOrderRevenuenumberCounter-sale revenue on paid sales orders (subset of paidTotalRevenue). Dollars as a decimal number (cents divided by 100, not rounded).
trendData[].paymentTotal *numberPayments received that day. Dollars as a decimal number (cents divided by 100, not rounded).
trendData[].paymentCountintegerPayment transactions that day.
trendData[].uniqueCustomerCountintegerDistinct customers that day.
trendData[].technicianStats *objectPer-technician stats keyed by technician id. UNLIKE the fields above, money inside here is still in integer CENTS (laborRevenueCents, commissionCents).
startDate *stringEcho of the startDate parameter.
endDate *stringEcho of the endDate parameter.
daysWithData *integerEntries with at least one invoice or payment.

Errors

  • 400 — Either date is missing.
  • 403 — Caller lacks the required permission.
  • 500 — Query failed (for example startDate after endDate).
get /dashboard/metrics

Get dashboard metrics

The numbers behind the shop dashboard: revenue, average job, payments received, gross profit and margin, technician utilization, a day-by-day trend, customers and vehicles served, all for a date range and compared with the previous period of the same length; plus right-now shop-floor counts (open jobs, vehicles in the shop, overdue, delayed, stale, waiting for parts), unpaid invoices, the top customers and invoices owing money, collection health, and low-stock part counts. Each group has its own freshness, and freshness in the response says how old each one is. Period figures (revenue, payments, profit, utilization, trend, served counts) come from the stored per-day rows, kept current as invoices and payments change and recomputed overnight; there is no live recalculation, and isStale is always false. Floor counters are updated as work orders change and recounted hourly (:10 past the hour). Unpaid totals are updated as invoices change and re-anchored nightly. paymentHealth and collections come from last night's receivables snapshot and cover ALL outstanding receivables, not just the range. Parts counts come from the latest inventory snapshot. Date range: pass BOTH startDate and endDate (YYYY-MM-DD, shop-local, inclusive) for a custom range. If either is missing or malformed, or startDate is after endDate, the range is silently replaced by the current month to date; there is no 400 for that. Calendar-impossible dates are not rejected either (2026-02-31 is accepted and rolls over). The only range error is more than 400 days (400 'Date range too long'). The previous period is the same number of days immediately before startDate. The shop's time zone is NOT read from settings here; it comes from the timezone parameter (default America/Chicago) and only decides what 'today' is for the default range. The stored rows themselves are already bucketed by the shop's own time zone. If the shop's history has not been backfilled (stored rows start after its first invoice) the response has building = true and metrics = null. Money units are mixed: every metric is DOLLARS (decimal numbers), except trendDaily.days[*], which is integer CENTS (fields ending in Cents). Tax: periodRevenue, avgPerJob, counterRevenue and the trend's invoicedCents include sales tax; grossProfit.revenue excludes it. The other query parameters you may see used by the app (recalculate) are ignored.

Permission: dashboard:read Money: MIXED. Every metric is DOLLARS (decimal numbers, Math.round(cents)/100): periodRevenue, avgPerJob, prevPeriodRevenue, counterRevenue, paymentsReceived, grossProfit.*, unpaidInvoicesAmount, techUtilization.effectiveLaborRate (dollars per hour), paymentHealth.total*, collections.*. EXCEPTION: trendDaily.days[*].*Cents are integer CENTS. Percent fields (revenuePctChange, grossMarginPct, utilizationPct, collectionRate...) are whole-number percentages.

Parameters

NameInTypeDescription
startDatequerystring dateFirst day of a custom range (inclusive), shop-local YYYY-MM-DD. Used only together with endDate; otherwise ignored. Format-checked only, not checked for being a real calendar day.
endDatequerystring dateLast day of a custom range (inclusive), shop-local YYYY-MM-DD. Used only together with startDate; must be on or after it or both are ignored. The range may span at most 400 days or the request fails with 400.
timezonequerystring
default "America/Chicago"
IANA time zone name used to decide today's date for the default (month-to-date) range, and echoed in trendDaily.timezone. Default America/Chicago. Not validated: an unknown name silently falls back to UTC for the day calculation. Send the shop's time zone.
periodquerystring
default "month"
Echoed back as period in the response (default 'month'). It does not change which dates are used; use startDate and endDate.
curl "https://api.bigshopai.com/v1/dashboard/metrics?startDate=2026-10-01&endDate=2026-10-09" \
  -H "Authorization: ApiKey bsk_live_..."

Response fields (first 3 levels)

FieldTypeDescription
building *booleanTrue when the stored daily rows do not reach back to the shop's first invoice (history not yet backfilled). metrics is then null and no card, including the floor counters, is served; the shop owner must run the history backfill.
coverage *object
coverage.earliestAggregateDay *string | null dateEarliest stored daily row for the shop.
coverage.earliestInvoiceDay *string | null dateEarliest realized invoice day.
metrics *Dashboard cards; null when building is true.
freshness *objectAge of each underlying source. The per-metric updatedAt values are request time and say nothing about data age.
freshness.aggregatesThrough *string | null date-timeLatest write time among the stored daily rows used (current and previous period). Null when none.
freshness.floorRecountedAt *string | null date-timeLast hourly recount of the floor counters.
freshness.floorUpdatedAt *string | null date-timeLast write to the floor/unpaid state row.
freshness.unpaidAsOf *string | null dateShop-local day of the AR snapshot that last re-anchored the unpaid totals.
freshness.arAsOf *string | null dateDay of the AR snapshot behind paymentHealth and collections.
freshness.inventoryAsOf *string | null dateDay of the inventory snapshot behind lowStockItems and totalParts.
isStale *booleanAlways false (hard-coded).
lastUpdated *string date-timeTime of this request.
period *stringEcho of the period query parameter, default 'month'. Does not affect the numbers.

Errors

  • 400 — Range longer than 400 days.
  • 403 — Caller lacks dashboard:read.
  • 500 — Unexpected error.

Inventory

get /inventory/parts-purchasing-report

Parts purchasing ledger (opening, purchases, usage, closing)

Answers: what did the shop buy, use and hold in a period, and how did inventory value move? Default response is a shop-wide summary (opening value, purchases, usage with revenue and profit, reserved/held change, closing value, a reconciliation residual and cost changes) plus one ledger row per part, tire, fluid or shop item that had activity. Dates are shop-local days (inclusive both ends, shop time zone default America/Chicago). All money is integer cents; quantities are units. The same route has drill-down modes selected by query parameters (precedence: view=daily, then partId, then bucket; otherwise the list): partId returns that part's raw transactions, bucket returns transactions across parts, view=daily returns a part x day pivot. The detail modes are paginated with page/limit and report hasMore. Opening/closing come from end-of-day inventory snapshots; where a snapshot is missing the value is null, or live (flagged openingIsLive/closingIsLive) when the window touches today. Cost, price and profit are returned to any caller that passes the read gate; there is no per-field cost masking.

Permission: inventory:read OR parts:read

Parameters

NameInTypeDescription
startDate *querystring dateFirst shop-local day, YYYY-MM-DD. Required (presence only is validated; format is not).
endDate *querystring dateLast shop-local day (inclusive), YYYY-MM-DD. Required.
searchquerystringCase-insensitive substring match on part number or part name (list, bucket and daily modes).
categoryquerystringList mode: single category, exact case-insensitive match against each row's partCategory. Bucket/daily modes: comma-separated exact, case-sensitive values.
partIdquerystringReturn this item's raw transactions instead of the list (an inventory part id or a material id).
bucketquery"inbound" | "outbound" | "reserved"Return transactions across parts counted in this bucket.
viewquery"daily"daily returns the part x shop-day pivot.
pagequeryinteger
default 1, range 1–
Detail modes only (partId/bucket). 1-based.
limitqueryinteger
default 200, range 1–500
Detail modes only (partId/bucket). Page size, capped at 500.
vendorquerystringBucket/daily modes only. Comma-separated vendor names (case-insensitive).
typesquerystringBucket/daily modes only. Comma-separated transaction types.
sourcesquerystringBucket/daily modes only. Comma-separated reference types (work_order, purchase_order, manual, ...).
curl "https://api.bigshopai.com/v1/inventory/parts-purchasing-report?startDate=2026-10-01&endDate=2026-10-09" \
  -H "Authorization: ApiKey bsk_live_..."

Errors

  • 400 — {"error":"startDate and endDate are required (YYYY-MM-DD)"}.
  • 403 — Forbidden. The caller's role lacks inventory:read (or parts:read).
  • 500 — Unhandled server error: {"error":"Internal Server Error","message":"An unexpected error occurred"}.
get /inventory/parts-activities

Parts activity log (every stock movement and part edit in a period)

Answers: who changed inventory, when, and why? Lists quantity movements (purchases received, adjustments, reservations, usage on work orders and counter sales, returns) and part edit/create events over a date range, newest first, with KPI stats, filter facets and the same opening/closing inventory cards as the purchasing report. Pass groupBy to get subtotalled groups instead of rows (not paginated), or view=lifecycle to collapse each part's events per order. Dates are shop-local days (inclusive). All money is integer cents. Pagination is page/limit (not a cursor). Each source stream is read up to 5000 rows; when that cap is hit truncated is true and the list and stats are partial.

Permission: inventory:read OR parts:read

Parameters

NameInTypeDescription
startDate *querystring dateFirst shop-local day, YYYY-MM-DD. Required.
endDate *querystring dateLast shop-local day (inclusive). Required.
partIdquerystringSingle-part view (part or material id). Looks that item up directly.
activityTypequerystringComma-separated activity types (case-insensitive). Valid values appear in facets.types.
userIdquerystringOnly events performed by this user id.
sourcequerystringComma-separated reference types, e.g. work_order, manual, purchase_order, ai_upload, release_queue (case-insensitive).
vendorquerystringComma-separated vendor names (case-insensitive).
categoryquerystringComma-separated part categories (case-insensitive).
invoiceNumberquerystringExact vendor invoice number.
poNumberquerystringExact PO number.
searchquerystringCase-insensitive substring over part number/name, work order number, customer, invoice #, PO #, vendor and user name.
pagequeryinteger
default 1, range 1–
1-based page.
limitqueryinteger
default 50, range 1–200
Page size, clamped to 1-200.
groupByquery"vendor" | "category" | "activityType" | "source" | "performedBy" | "invoice" | "po"Return subtotalled groups instead of rows. Unknown values are ignored.
thenByquery"vendor" | "category" | "activityType" | "source" | "performedBy" | "invoice" | "po"Second grouping level (with groupBy).
viewquery"lifecycle"lifecycle: one row per part per order, with the events nested in children. Ignored with groupBy.
curl "https://api.bigshopai.com/v1/inventory/parts-activities?startDate=2026-10-01&endDate=2026-10-09" \
  -H "Authorization: ApiKey bsk_live_..."

Errors

  • 400 — {"error":"startDate and endDate are required (YYYY-MM-DD)"}.
  • 403 — Forbidden. The caller's role lacks inventory:read (or parts:read).
  • 500 — Unhandled server error: {"error":"Internal Server Error","message":"An unexpected error occurred"}.

Vendors

get /vendors/ap-report

Vendor accounts payable report (what the shop owes vendors)

Answers: how much does the shop owe its vendors, how overdue is it, and what was bought, returned and held in core deposits? One call returns vendor invoices with net-30 aging as of TODAY (shop-local), purchase orders, part returns, core-deposit lines and core write-offs. Computed live on every request from purchase orders, ad-hoc vendor invoices, vendors and core write-offs. A PO counts as an invoice only if it has a vendor invoice number. All money is integer cents. Lists are newest-first and cut at 5000 rows each; truncated is an ARRAY of the source names that hit a cap (empty = complete).

Permission: vendors:read
curl "https://api.bigshopai.com/v1/vendors/ap-report" \
  -H "Authorization: ApiKey bsk_live_..."

Response fields (first 3 levels)

FieldTypeDescription
asOf *string dateToday in the shop's time zone. Always today: the endpoint takes NO date parameter.
timeZone *stringIANA zone used for all day labels (default America/Chicago).
vendors[] *object[]
vendors[].id *string
vendors[].name *string
vendors[].contactPhonestring
vendors[].contactEmailstring
vendors[].accountNumberstring
invoices[] *object[]Newest invoice date first. Capped at 5000 rows (see truncated).
invoices[].id *stringpo-<poId> for invoices billed through a purchase order, adhoc-<id> for PO-less vendor invoices.
invoices[].vendorId *string
invoices[].vendorName *string
invoices[].invoiceNumber *stringVendor invoice number(s); several are joined with , .
invoices[].poIdstring
invoices[].poNumberstring
invoices[].date *string dateShop-local invoice day.
invoices[].dueDate *string dateAlways date + 30 days (fixed net-30 in code).
invoices[].daysPastDue *integerDays past dueDate as of today (shop-local). NEGATIVE when not yet due (not clamped, unlike the customer aging endpoint).
invoices[].agingBucket *"current" | "days30" | "days60" | "days90" | "over90"current = not yet due; days30 = 1-30 past due; days60 = 31-60; days90 = 61-90; over90 = more than 90.
invoices[].totalCents *integerInteger cents.
invoices[].paidCents *integerEquals totalCents when paid, else 0 (no partial payments at this level). Integer cents.
invoices[].balanceCents *integerInteger cents.
invoices[].status *"paid" | "unpaid"
invoices[].paidOnstring dateShop-local day the bill was marked paid, when recorded.
invoices[].kind *"po" | "adhoc"
invoices[].fileNamestring
invoices[].itemCountinteger
invoices[].notesstring
purchaseOrders[] *object[]Newest first. Capped at 5000 rows.
purchaseOrders[].id *string
purchaseOrders[].number *string
purchaseOrders[].vendorId *string
purchaseOrders[].vendorName *string
purchaseOrders[].status *string
purchaseOrders[].isPaid *boolean
purchaseOrders[].totalCostCents *integerInteger cents.
purchaseOrders[].partCountinteger
purchaseOrders[].receivedPartCountinteger
purchaseOrders[].invoiceNumbers[]string[]
purchaseOrders[].createdAtstring date-time
purchaseOrders[].daystring dateShop-local day of createdAt.
purchaseOrders[].orderedDatestring
purchaseOrders[].receivedDatestring
purchaseOrders[].fulfilledDatestring
returns[] *object[]Capped at 5000 rows.
returns[].id *string
returns[].vendorId *string
returns[].vendorName *string
returns[].purchaseOrderIdstring
returns[].purchaseOrderNumberstring
returns[].partName *string
returns[].partNumberstring
returns[].quantity *number
returns[].reasonstring
returns[].status *"pending" | "approved" | "completed" | "rejected"
returns[].refundAmountCents *integerInteger cents.
returns[].createdAtstring
returns[].daystring date
returns[].notesstring
cores[] *object[]Capped at 5000 rows.
cores[].idstring
cores[].vendorIdstring
cores[].vendorNamestring
cores[].purchaseOrderIdstring
cores[].purchaseOrderNumberstring
cores[].partNumberstring
cores[].partNamestring
cores[].depositDaystringShop-local day the deposit was received; '' when the line holds no deposit.
cores[].creditDaystringShop-local day the credit was recorded; '' when none.
cores[].depositUnitsnumber
cores[].depositCentsintegerInteger cents.
cores[].creditUnitsnumber
cores[].creditCentsintegerInteger cents.
cores[].openUnitsnumberDeposit units no credit has matched yet.
cores[].openCentsintegerInteger cents.
cores[].unmatchedCreditUnitsnumber
cores[].unmatchedCreditCentsintegerInteger cents.
cores[].writtenOffUnitsnumber
cores[].writtenOffCentsintegerInteger cents.
coreWriteOffs[] *object[]
coreWriteOffs[].idstring
coreWriteOffs[].vendorIdstring
coreWriteOffs[].vendorNamestring
coreWriteOffs[].partNumberstring
coreWriteOffs[].unitsnumber
coreWriteOffs[].amountCentsintegerWhat the closed deposits cost when written off. Integer cents.
coreWriteOffs[].reasonstring
coreWriteOffs[].createdBystring
coreWriteOffs[].createdAtstring date-time
coreWriteOffs[].daystring date
truncated[] *string[]NOT a boolean. Names of the sources that hit a read/row cap (any of: purchase orders, vendor invoices, vendors, core write-offs, invoices, part returns, core lines). Empty array = complete. When non-empty the figures are a floor.

Errors

  • 403 — Forbidden. The caller's role lacks vendors:read.
  • 500 — Unhandled server error: {"error":"Internal Server Error","message":"An unexpected error occurred"}.

Customers

get /customers/{customerId}/unpaid-invoices

A customer's open (unpaid) invoices

Answers: what does this customer currently owe, invoice by invoice? Lists the customer's invoiced work orders and counter sales that still have a balance, oldest first. Archived invoices that are still unpaid are included. Balances are computed from the Payments table (net of refunds), with declined services excluded, so they match what a statement would show. Money is returned twice: integer cents (*Cents, canonical) and dollar decimals (total, paid, balance). Dates for startDate/endDate are shop-local, compared against the invoice date (invoiced date, else created); invoices with no date are included. If the customer has more than 1500 open invoices only the 1500 most recent are evaluated and truncated is true; narrow with startDate/endDate. An unknown or other-shop customer id returns 200 with an empty list, not 404.

Permission: customers:read

Parameters

NameInTypeDescription
customerId *pathstringCustomer id.
startDatequerystring dateOnly invoices dated on/after this shop-local day (YYYY-MM-DD, inclusive).
endDatequerystring dateOnly invoices dated on/before this shop-local day (inclusive).
curl "https://api.bigshopai.com/v1/customers/<customerId>/unpaid-invoices?startDate=2026-10-01&endDate=2026-10-09" \
  -H "Authorization: ApiKey bsk_live_..."

Response fields (first 3 levels)

FieldTypeDescription
invoices[] *object[]Oldest invoiced date first.
invoices[].id *stringWork order or counter-sale id.
invoices[].orderNumber *string
invoices[].orderNamestringTitle, truncated to 60 chars.
invoices[].descriptionstring
invoices[].vehiclestring
invoices[].vehicleIdstring
invoices[].vehicleVinstring
invoices[].vehicleUnitNumberstring
invoices[].poNumberstring
invoices[].customerNamestring
invoices[].datestringCreation timestamp.
invoices[].invoicedDatestring
invoices[].dueDatestring
invoices[].totalAmountCents *integerActive total (declined services excluded; shop-supplies cap applied). Integer cents.
invoices[].paidAmountCents *integerNet of refunds, computed from the Payments table (not the work-order header). Integer cents.
invoices[].balanceCents *integerAlways > 0 in this list. Integer cents.
invoices[].totalnumberDollar twin of totalAmountCents. DOLLARS as a decimal number (not cents).
invoices[].paidnumberDollar twin of paidAmountCents. DOLLARS as a decimal number (not cents).
invoices[].balancenumberDollar twin of balanceCents. DOLLARS as a decimal number (not cents).
invoices[].statusstring
invoices[].archivedbooleanArchived but still unpaid invoices ARE included.
invoices[].sourceType *"wo" | "so"wo = work order invoice, so = counter sale.
truncated *booleantrue when the customer had more than 1500 open invoices and only the 1500 most recent were evaluated; narrow with startDate/endDate.

Errors

  • 403 — Forbidden. The caller's role lacks customers:read.
  • 500 — {"error":"Failed to list unpaid invoices"}.
get /customers/{customerId}/statements

Statements created for a customer

Answers: what statements has the shop built for this customer and where do they stand? Each statement bundles several invoices; every read re-resolves the bundled invoices live (total, paid, balance and the payments applied), so numbers are current rather than as-created. Newest first. Money is returned as integer cents plus dollar twins (totalAmount, paidAmount, totalDue). status is recomputed to Paid when nothing is due or Partial when something was paid, otherwise the stored status (Draft when created). There is no pagination and no truncation flag. Payment rows include card brand/last4, check numbers and bank names.

Permission: customers:read

Parameters

NameInTypeDescription
customerId *pathstringCustomer id.
curl "https://api.bigshopai.com/v1/customers/<customerId>/statements" \
  -H "Authorization: ApiKey bsk_live_..."

Response fields (first 3 levels)

FieldTypeDescription
statements[] *object[]Newest first. No pagination or truncation flag.
statements[].id *string
statements[].customerId *string
statements[].statementNumber *stringST- prefixed.
statements[].invoiceIds[] *string[]
statements[].totalAmountCents *integerSum of invoice totals, recomputed live on every read. Integer cents.
statements[].paidAmountCents *integerRecomputed live. Integer cents.
statements[].totalDueCents *integerSum of POSITIVE invoice balances only (an overpaid invoice does not offset another's due). Integer cents.
statements[].creditAppliedCentsintegerInteger cents.
statements[].totalAmountnumberDOLLARS as a decimal number (not cents).
statements[].paidAmountnumberDOLLARS as a decimal number (not cents).
statements[].totalDuenumberDOLLARS as a decimal number (not cents).
statements[].status *stringDraft when created; recomputed on read to Paid (nothing due) or Partial (something paid). A statement that has been sent may carry other values - not enumerated.
statements[].includeInvoicesboolean
statements[].startDatestring
statements[].endDatestring
statements[].notesstring
statements[].invoices[]object[]Live-resolved at read time, not the stored copy. Only present/refreshed when invoiceIds is non-empty.
statements[].invoices[].idstring
statements[].invoices[].orderNumberstring
statements[].invoices[].orderNamestring
statements[].invoices[].descriptionstring
statements[].invoices[].vehiclestring
statements[].invoices[].vehicleIdstring
statements[].invoices[].vehicleVinstring
statements[].invoices[].vehicleUnitNumberstring
statements[].invoices[].customerNamestring
statements[].invoices[].datestring
statements[].invoices[].invoicedDatestring
statements[].invoices[].dueDatestring
statements[].invoices[].totalAmountCentsintegerInteger cents.
statements[].invoices[].paidAmountCentsintegerInteger cents.
statements[].invoices[].balanceCentsintegerCan be NEGATIVE (overpayment). Integer cents.
statements[].invoices[].creditedFromOverpaymentCentsintegerInteger cents.
statements[].invoices[].creditAppliedCentsintegerPortion paid with store credit. Integer cents.
statements[].invoices[].totalnumberDOLLARS as a decimal number (not cents).
statements[].invoices[].paidnumberDOLLARS as a decimal number (not cents).
statements[].invoices[].balancenumberDOLLARS as a decimal number (not cents).
statements[].invoices[].statusstring
statements[].invoices[].sourceType"wo" | "so"
statements[].invoices[].payments[]object[]
statements[].payments[]object[]
statements[].payments[].idstring
statements[].payments[].invoiceIdstring
statements[].payments[].orderNumberstring
statements[].payments[].datestring
statements[].payments[].methodstring
statements[].payments[].amountCentsintegerInteger cents.
statements[].payments[].amountnumberDollar twin of amountCents. DOLLARS as a decimal number (not cents).
statements[].payments[].referencestring
statements[].payments[].checkNumberstring
statements[].payments[].bankNamestring
statements[].payments[].referenceNumberstring
statements[].payments[].cardLast4string
statements[].payments[].cardBrandstring
statements[].payments[].notesstring
statements[].payments[].isRefundboolean
statements[].payments[].statusstring
statements[].payments[].bulkPaymentIdstring
statements[].payments[].bulkPaymentKindstring
statements[].payments[].orderNumbers[]string[]
statements[].sentAtstringSet when the statement was sent to the customer.
statements[].sentViastring
statements[].createdAtstring date-time
statements[].updatedAtstring date-time

Errors

  • 403 — Forbidden: lacks customers:read, or the role is explicitly denied customers:financials. Body is PERMISSION_DENIED.
  • 404 — {"error":"Customer not found"} - unknown id OR a customer in another shop (cross-tenant reads return 404).
  • 500 — {"error":"Failed to list statements"}.

Payments

get /payments

List payments received (work orders and counter sales)

Answers: what money came in, and when? Returns payment records for the whole shop (work-order invoices and counter sales merged), optionally restricted to a date range. Amounts are integer cents. PAGING: cursor-based, but with cursor/nextCursor (opaque base64), NOT lastEvaluatedKey. To fetch everything in a period, pass startDate+endDate and keep passing the returned nextCursor until hasMore is false. limit is a minimum target rather than a hard maximum: pages stop at a database page boundary, so a page can return more than limit rows. The payment day used for the date filter is date, else paymentDate, else createdAt (shop-local when timezone is given, otherwise the UTC date prefix). Without a date range the rows are NOT the newest payments: work-order payments are stored under PAYMENT#<uuid>, so the first page is an arbitrary slice. Always pass a date range for reporting. Rows are the stored payment records, including payer details such as Zelle contact and check/bank details, and createdByName (the employee who recorded it).

Permission: payments:read

Parameters

NameInTypeDescription
startDatequerystring dateFirst day (inclusive), YYYY-MM-DD. Compared as a string against the payment day label.
endDatequerystring dateLast day (inclusive), YYYY-MM-DD.
timezonequerystringIANA zone used to turn timestamped payments into a day for the date filter. Without it the UTC date prefix is used, so evening payments can land on the next day.
limitqueryinteger
default 100, range 1–
Target number of in-range rows per page (no upper bound enforced; default 100). Pages may exceed it.
cursorquerystringOpaque cursor from a previous response's nextCursor. A malformed cursor is silently ignored and the listing restarts from the beginning (risk of duplicate rows).
curl "https://api.bigshopai.com/v1/payments?startDate=2026-10-01&endDate=2026-10-09" \
  -H "Authorization: ApiKey bsk_live_..."

Response fields (first 3 levels)

FieldTypeDescription
payments[] *object[]Merged work-order + counter-sale payments, sorted newest-first by createdAt WITHIN this page only.
payments[].id *stringUUID, or manual_<Idempotency-Key> for idempotent creates.
payments[].tenantIdstring
payments[].workOrderIdstringWork order OR counter-sale id the payment is applied to.
payments[].customerIdstring | null
payments[].orderNumberstring
payments[].method *stringFree-form string, not validated server-side (e.g. cash, card, check, ach, zelle, credit).
payments[].amountCents *integerTotal collected = principal + card surcharge. Can be negative for adjustments. Integer cents.
payments[].surchargeAmountCentsintegerCard-surcharge part of amountCents; absent when none. Integer cents.
payments[].surchargeFeenumberSurcharge percent applied.
payments[].appliedToInvoiceCentsintegerPrincipal actually applied when an overpayment was split to store credit; absent otherwise. Integer cents.
payments[].creditedToCustomerCentsintegerOverpayment excess parked as store credit; absent otherwise. Integer cents.
payments[].statusstringcompleted on create; also refunded, voided, pending, failed elsewhere in the API.
payments[].manualPaymentDetailsobjectCard brand/last4 for payments run on the shop's own terminal.
payments[].stripePaymentDetailsobjectPresent on online (Stripe) payments.
payments[].referenceNumberstring | null
payments[].checkNumberstring | null
payments[].bankNamestring | null
payments[].zelleEmailstring | nullPayer PII.
payments[].zellePhonestring | nullPayer PII.
payments[].notesstring | null
payments[].createdBystring
payments[].createdByNamestringEmployee who recorded the payment.
payments[].createdAt *stringPayment timestamp (may be backdated for migrated payments).
payments[].paymentDatestringLegacy/edited alias of createdAt.
payments[].datestringLegacy alias.
payments[].createdAtOriginalstringPresent once an admin edited the date.
payments[].updatedAtstring
payments[].refundedAmountCentsintegerPresent on refunded payments. Integer cents.
payments[].refundedAtstring
payments[].amountnumberLEGACY rows only: dollar amount. Prefer amountCents. DOLLARS as a decimal number (not cents).
total *integerNumber of payments on THIS page (not the all-time or in-range total).
nextCursorstringOpaque base64 cursor. Pass back as cursor to continue. Absent on the last page.
hasMore *booleantrue when nextCursor is present.

Errors

  • 403 — Forbidden. The caller's role lacks payments:read.
  • 500 — {"error":"Internal Server Error"}.

Shop Financials

get /shop-financials/months

List months that have shop financials

Answers: which months have overhead/expense and income bookkeeping, and what are their totals? Returns one record per month (newest first) with total expenses, total income, net, and entry counts. These are the shop-entered Shop Financials figures (rent, utilities, insurance, payroll as booked, etc.), not work-order revenue. All money is integer cents. Totals are stored on the month record and updated whenever an entry changes.

Permission: reports:profit

Parameters

NameInTypeDescription
yearqueryintegerOnly months in this calendar year (e.g. 2026). A non-numeric value is ignored (no filter).
curl "https://api.bigshopai.com/v1/shop-financials/months" \
  -H "Authorization: ApiKey bsk_live_..."

Response fields (first 3 levels)

FieldTypeDescription
months[] *object[]Newest month first. Not paginated.
months[].monthKey *string
months[].year *integer
months[].month *integer
months[].status *"open" | "closed" | "archived"Declared type. Whether closed is ever stored on this row (vs the separate Period Close record) was not verified.
months[].totalExpensesCents *integerAll expenses booked to the month, including any under the default 'Payroll' category. Integer cents.
months[].totalIncomeCents *integerInteger cents.
months[].netCents *integerIncome minus expenses; may be negative. Integer cents.
months[].expenseCount *integer
months[].incomeCount *integer
months[].notesstring
months[].createdAtstring date-time
months[].updatedAtstring date-time

Errors

  • 401 — {"error":"Unauthorized"} when the tenant cannot be resolved from the authorizer context.
  • 403 — {"error":"You do not have permission to view shop financials."} - NOTE this route does not use the PERMISSION_DENIED body.
  • 500 — {"error":"Something went wrong loading shop financials."} (or Could not complete the accounting request. from the accounting router).
get /shop-financials/months/{month}

One month of shop financials with all expense and income lines

Answers: what overhead did the shop book in this month and what other income did it record? Returns the month record plus every expense line (vendor, category, amount, paid status, linked bank transaction), income line (with linked counter-sale status) and recorded payment/receipt transaction. These are shop-entered or bank-promoted entries - not derived from work orders. All money is integer cents. The month must be YYYY-MM; a month that was never created returns 404.

Permission: reports:profit

Parameters

NameInTypeDescription
month *pathstringMonth key, YYYY-MM. Any other shape does not match this route and returns 404 'Shop Financials endpoint not found'.
curl "https://api.bigshopai.com/v1/shop-financials/months/<month>" \
  -H "Authorization: ApiKey bsk_live_..."

Response fields (first 3 levels)

FieldTypeDescription
month *object
month.monthKey *string
month.year *integer
month.month *integer
month.status *"open" | "closed" | "archived"Declared type. Whether closed is ever stored on this row (vs the separate Period Close record) was not verified.
month.totalExpensesCents *integerAll expenses booked to the month, including any under the default 'Payroll' category. Integer cents.
month.totalIncomeCents *integerInteger cents.
month.netCents *integerIncome minus expenses; may be negative. Integer cents.
month.expenseCount *integer
month.incomeCount *integer
month.notesstring
month.createdAtstring date-time
month.updatedAtstring date-time
expenses[] *object[]All expenses for the month (drained fully).
expenses[].id *string
expenses[].monthKey *string
expenses[].category *stringFree text; default categories include Payroll, Rent/Mortgage, Insurance - Workers Comp, Utilities - Electric, ...
expenses[].vendorstring
expenses[].description *string
expenses[].amountCents *integerInteger cents.
expenses[].frequency"monthly" | "weekly" | "one-time" | "quarterly" | "annual"
expenses[].startDatestring
expenses[].endDatestring
expenses[].paymentStatus"paid" | "unpaid" | "scheduled"Promoted to paid when the expense is linked to a cleared bank transaction.
expenses[].paymentMethodstring
expenses[].notesstring
expenses[].isFixedboolean
expenses[].isTaxDeductibleboolean
expenses[].costCenterstring
expenses[].receiptExtractedboolean
expenses[].sourceTransactionIdstringSet when promoted from a bank transaction.
expenses[].sourceTransactionobjectAdded at read time for bank-linked expenses.
expenses[].sourceTransaction.transactionIdstring
expenses[].sourceTransaction.datestringDay the money left the account.
expenses[].sourceTransaction.namestringMerchant/bank description.
expenses[].sourceTransaction.amountCentsintegerSigned cents; an expense's source is negative. Integer cents.
expenses[].sourceTransaction.accountNamestring | null
expenses[].createdAtstring date-time
expenses[].updatedAtstring date-time
income[] *object[]All income entries for the month.
income[].id *string
income[].monthKey *string
income[].customerIdstring
income[].customerNamestring
income[].descriptionstring
income[].category *string
income[].amountCents *integerInteger cents.
income[].frequency"monthly" | "weekly" | "one-time"
income[].paymentStatus"unpaid" | "partially_paid" | "paid"Promoted from the linked counter sale at read time; never demoted.
income[].invoiceIdstring
income[].invoiceNumberstringCounter-sale number, e.g. SO-000123.
income[].notesstring
income[].saleStatusstring | nullLinked counter sale status (read-time enrichment).
income[].salePaidCentsintegerAmount paid on the linked counter sale (read-time enrichment). Integer cents.
income[].createdAtstring date-time
income[].updatedAtstring date-time
transactions[] *object[]Recorded payments/receipts against expenses and income.
transactions[].id *string
transactions[].monthKey *string
transactions[].type *"expense_payment" | "income_receipt"
transactions[].referenceId *stringExpense or income id.
transactions[].amountCents *integerInteger cents.
transactions[].date *string
transactions[].methodstring
transactions[].notesstring
transactions[].createdAtstring date-time

Errors

  • 401 — {"error":"Unauthorized"}.
  • 403 — {"error":"You do not have permission to view shop financials."}.
  • 404 — {"error":"Month not found"} (no such month for this shop) or {"error":"Shop Financials endpoint not found","details":{path,method}} (malformed month).
  • 500 — {"error":"Something went wrong loading shop financials."}.

Technicians & Labor

get /work-orders/technician-labor-summary

Per-technician completed labor hours, revenue and cost

Answers: how many labor hours did each technician complete in this period, what did that labor bill, and what did it cost the shop? Per technician: hours, labor revenue, jobs, hours per day and a line-by-line breakdown. LABOR ONLY (parts are not attributed to technicians). basis decides which date puts a labor line in the period: labor-completed (the day the line was completed) or invoiced (the day its work order was invoiced). The shop's own setting (Settings > Labor, billableHoursBasis) is the default; you may request either value via basis. Days are shop-local. EMPLOYEE PAY DATA: laborCostCents, commissionCents and breakdown[].costCents reveal what the shop pays each technician (hourly cost rate x hours; flat-weekly staff prorated) and their commission. The server removes these fields for callers without reports:profit or attendance:financials; API keys authenticate as admin and always receive them. All money is integer cents; hours are decimal. The read is capped at 5000 work orders; truncated: true means totals are partial.

Permission: workorders:read + reports:operational

Parameters

NameInTypeDescription
startDate *querystring dateFirst shop-local day, YYYY-MM-DD. Required (a longer ISO timestamp is accepted: only the first 10 characters are used).
endDate *querystring dateLast shop-local day (inclusive). Required.
basisquery"labor-completed" | "invoiced"Which date decides the period. Omitted or any other value falls back to the shop's billableHoursBasis setting (default labor-completed).
curl "https://api.bigshopai.com/v1/work-orders/technician-labor-summary?startDate=2026-10-01&endDate=2026-10-09" \
  -H "Authorization: ApiKey bsk_live_..."

Response fields (first 3 levels)

FieldTypeDescription
technicians[] *object[]Sorted by laborRevenueCents descending.
technicians[].technicianId *string
technicians[].technicianName *string
technicians[].laborHours *numberCompleted labor hours attributed to this technician in the window.
technicians[].laborRevenueCents *integerLabor billed for those hours (labor only; parts excluded by design). Integer cents.
technicians[].laborCostCentsintegerPAY DATA. What the shop pays for those hours (hourly cost rate x hours; weekly-flat staff are prorated). OMITTED for callers without reports:profit or attendance:financials. Integer cents.
technicians[].commissionCentsintegerPAY DATA. Commission earned on those hours at the technician's labor-commission %. OMITTED for callers without reports:profit or attendance:financials. Integer cents.
technicians[].jobsCompleted *integerDistinct work orders contributing.
technicians[].dailyHours *objectHours keyed by the shop-local day (YYYY-MM-DD) each line counted on under the basis.
technicians[].breakdown[] *object[]One entry per completed labor line (can be large).
technicians[].breakdown[].orderId *string
technicians[].breakdown[].orderNumber *stringInvoice number, else order number, else id.
technicians[].breakdown[].isInvoiced *boolean
technicians[].breakdown[].status *string
technicians[].breakdown[].date *stringInvoiced/created timestamp of the work order.
technicians[].breakdown[].customerName *string
technicians[].breakdown[].vehicleLabel *string
technicians[].breakdown[].description *stringLabor line description.
technicians[].breakdown[].hours *numberThis technician's share of the line's hours.
technicians[].breakdown[].revenueCents *integerThis technician's share of the line's labor revenue (net of discount). Integer cents.
technicians[].breakdown[].costCentsintegerPAY DATA. Hours x the technician's hourly cost rate in force on the line's date (or the stored line cost). Dividing by hours reveals the rate. OMITTED for callers without reports:profit / attendance:financials; API keys (role admin) always receive it. Integer cents.
startDate *string date
endDate *string date
basis *"labor-completed" | "invoiced"Which date decides the period: the day each labor line was completed, or the day its work order was invoiced.
workOrdersScanned *integer
truncated *booleantrue when the 5000-work-order read cap was hit; totals are then partial.

Errors

  • 400 — {"error":"startDate and endDate are required (YYYY-MM-DD)"}.
  • 403 — Forbidden. The caller's role lacks workorders:read / workorders:read-all.
  • 500 — Unexpected server error: {"error":"Internal Server Error","message":"An unexpected error occurred"}.