Reports
Sales and team analytics over a date range — order list with profit, dynamics, average deal, revenue, loss reasons, funnel conversion, forecast, team productivity and low stock.
Reports aggregate the project's orders over a period. All money figures come from order products:
line amount = sale_price × quantity × (1 + markup_percent/100) × (1 − discount_percent/100)
order amount = Σ line amount
profit = Σ (unit price after markup and discount − purchase_price) × quantity
Soft-deleted orders are never counted. Orders are assigned to a period by their creation date.
Field types follow the field notation.
Access
Every endpoint on this page requires both the projects and reports OAuth scopes and the can_view_reports
permission (the owner bypasses it). The user must be a member of the project, and the owner's subscription must be
active. Otherwise the endpoint returns 404 RES_NOT_FOUND.
Reports cover all orders of the project and include purchase prices and profit, regardless of the user's order
visibility and price permissions. Grant can_view_reports accordingly.
Common query params
Order reports accept these query parameters:
| Field | Type | Description |
|---|---|---|
| from?, to? | date | Period, YYYY-MM-DD, both inclusive. Both must be given; otherwise the current month is used |
| funnel? | uuid | Only orders currently in a status of this funnel. Default: all funnels (except Funnel Stages) |
| contacts? | string | Contact UUIDs separated by ; |
| statuses? | string | Status UUIDs separated by ; |
The period is clamped: it cannot start more than a year ago (it then starts at the beginning of that month) or end in the future. Every response echoes the period actually used:
{
"filter": {
"date": {
"from": "2026-09-01",
"to": "2026-09-30"
},
"funnel": null,
"contacts": [],
"statuses": []
},
"data": "..."
}
Orders
GET /api/v1/projects/{project.uuid}/reports/orders
Lists orders in the period with their money figures, newest first. Takes page? and limit? (1–100, default 50) in
addition to the common params.
data is an array of:
| Field | Type | Description |
|---|---|---|
| uuid | uuid | Order ID |
| serial | string | Order number |
| contact_uuid | ?uuid | Contact |
| contact_name | ?string | Contact name |
| user_uuid | ?uuid | Responsible member |
| user_name | string | Their name |
| status_uuid | ?uuid | Status |
| status_name | ?string | Status name |
| status_color | ?string | Status colour |
| status_type | ?string | new, active, done or canceled |
| order_amount | decimal | Order amount |
| purchase_amount | decimal | Σ purchase_price × quantity |
| profit | decimal | Profit |
| profit_percent | decimal | Profit as a percent of the order amount |
| potential_revenue | decimal | order_amount for new/active orders, else 0 |
| paid_amount | decimal | Amount paid |
| remaining_amount | decimal | Amount still owed; for a canceled order, minus what was paid (a refund owed) |
| created_at | ISO8601 datetime | Created |
| updated_at | ISO8601 datetime | Last changed |
Dynamics
GET /api/v1/projects/{project.uuid}/reports/orders/dynamics
Order totals per period bucket. Extra param groupBy?: day, week or month. Default: day for periods up to 31
days, week up to 90, month beyond. The value used is echoed as filter.group_by.
data is an array, oldest bucket first, with only buckets that have orders:
| Field | Type | Description |
|---|---|---|
| period | ISO8601 datetime | Start of the bucket |
| total_orders | integer | Orders created |
| done_orders | integer | Of those, now in a done status |
| canceled_orders | integer | Of those, now in a canceled status |
| total_amount | decimal | Σ order amount |
| total_profit | decimal | Σ profit |
| total_paid | decimal | Σ paid |
| avg_amount | decimal | Average order amount |
Average Deal
GET /api/v1/projects/{project.uuid}/reports/orders/avg-deal
data: {total_orders, avg_amount} over all orders in the period, whatever their status.
Revenue
GET /api/v1/projects/{project.uuid}/reports/orders/revenue
Revenue and gross profit. What counts as revenue depends on the project's
report_settings.revenue_mode:
pipeline(default): every order that is not canceled;actual: only orders in adonestatus. Open orders are reported separately aspotential_revenue.
| Field | Type | Description |
|---|---|---|
| revenue_mode | string | The mode used |
| revenue | decimal | Σ order amount of the counted orders |
| cogs | decimal | Σ purchase cost of the counted orders |
| gross_profit | decimal | revenue − cogs |
| gross_margin_percent | decimal | gross_profit / revenue × 100, 2 decimals |
| potential_revenue | ?decimal | actual mode: amount of new/active orders. null in pipeline |
| total_paid | decimal | Σ paid over all orders in the period |
| total_remaining | decimal | Σ still owed; canceled orders count only as refunds owed |
Loss Reasons
GET /api/v1/projects/{project.uuid}/reports/orders/loss-reasons
Canceled orders grouped by loss reason, biggest loss first.
{
"filter": {
"...": "..."
},
"data": {
"reasons": [
{
"loss_reason_uuid": "0199a1bb-0001-7000-8000-000000000001",
"loss_reason_name": "Too expensive",
"orders_count": 7,
"lost_revenue": 12450
},
{
"loss_reason_uuid": null,
"loss_reason_name": null,
"orders_count": 2,
"lost_revenue": 980
}
],
"total_orders": 9,
"total_lost_revenue": 13430
}
}
The entry with null uuid collects canceled orders without a reason.
Funnel Stages
GET /api/v1/projects/{project.uuid}/reports/orders/funnel-stages
Conversion through the stages of one funnel: funnel if given, else the default funnel. Takes the orders created
in the period that are now in that funnel, and follows their status history.
Stages are the funnel's new, active and done statuses in order. An order counts as having reached a stage if it
reached it or any later stage.
| Field | Type | Description |
|---|---|---|
| total_orders | integer | Orders in the cohort |
| stages[].uuid | uuid | Status |
| stages[].name | string | Status name |
| stages[].reached_count | integer | Orders that reached this stage or later |
| stages[].pct_of_first | decimal | Percent of the first stage, 1 decimal |
| stages[].pct_of_previous | ?decimal | Percent of the previous stage; null for the first |
| stages[].dropped_to_canceled | integer | Orders moved from this stage straight to a canceled status |
Forecast
GET /api/v1/projects/{project.uuid}/reports/orders/forecast
A linear trend over monthly order amounts in the period, projected 3 months ahead, plus the current open pipeline.
| Field | Type | Description |
|---|---|---|
| history[] | array | {period, total_amount} per month with orders |
| forecast[] | array | {period, predicted_amount} for the next 3 months; empty with under 2 months of history |
| trend | ?object | {slope, direction}, direction is up, down or flat; null with under 2 months |
| pipeline.amount | decimal | Amount of all orders now in new/active statuses, regardless of the period |
| pipeline.orders_count | integer | Their number |
statuses does not apply to the pipeline.
Productivity
GET /api/v1/projects/{project.uuid}/reports/productivity
Per-member results for the period, compared with the previous period of the same length. Takes only from and to.
filter also contains prev_date and revenue_mode.
data is an array, top seller first:
| Field | Type | Description |
|---|---|---|
| user_uuid | uuid | Member |
| user_name | string | Name |
| user_email | string | |
| total_orders | integer | Orders they are responsible for, created in the period |
| closed_orders | integer | Of those, in a done status |
| total_sales | decimal | Σ order amount, counted per the project's revenue mode |
| total_profit | decimal | Σ profit, same rule |
| total_paid | decimal | Σ paid |
| avg_margin_percent | decimal | total_profit / total_sales × 100 |
| total_tasks | integer | Tasks assigned to them, created in the period |
| closed_tasks | integer | Of those, done |
| prev_total_sales | decimal | total_sales in the previous period |
| prev_total_orders | integer | total_orders in the previous period |
| sales_growth | decimal | Percent change of total_sales; 100 if the previous period had none |
| orders_growth | decimal | Percent change of total_orders; 100 if the previous period had none |
Members with tasks but no orders appear with zero sales. Orders without a responsible member are not counted.
Low Stock
GET /api/v1/projects/{project.uuid}/reports/low-stock
Stock items in any warehouse that are below their min_stock (only where min_stock > 0). Takes no parameters.
{
"data": [
{
"uuid": "0199a0d0-0000-7000-8000-000000000001",
"item_uuid": "0199a2c0-7a1b-7c2d-8e3f-a0b1c2d3e4f5",
"item_type": "material",
"warehouse_uuid": "0199a0b1-3c4d-7e5f-8a9b-0c1d2e3f4a5b",
"warehouse_name": "Main warehouse",
"name": "Blackout 605",
"unit": "sm",
"quantity": 8,
"min_stock": 20,
"deficit": -12
}
]
}
deficit is quantity − min_stock, so it is negative.
Errors
| Status | Code | When |
|---|---|---|
| 403 | AUTH_INSUFFICIENT_SCOPE |
The token lacks projects or reports |
| 403 | AUTH_NO_PROJECT_ACCESS |
The user's role lacks can_view_reports |
| 404 | RES_NOT_FOUND |
The project does not exist or is not accessible |
Invalid dates or filters do not cause errors: they are ignored and the defaults apply.