Datasets
GET /api/v1/charts/:dataset returns the aggregated data behind TrackMyMachines' dashboards as rows.
There are thirteen datasets, one per underlying query — between them they cover every figure a dashboard draws. They are not charts: a chart is a way of drawing one of these, and the same rows are drawn a dozen ways in the app. So a dataset takes only the parameters that change which rows come back — the window, the bucket, the grouping, the filters — and every row carries every column that query produces. You read the column you want; there is no parameter for picking one figure out of a row.
The path still says /charts/ and the parameter is still called chart_type. Both are kept so existing integrations and connector definitions keep working. The value is a dataset name.
Permissions
| Endpoint | Method | Read-only Access | Read-write Access | Description |
|---|---|---|---|---|
/api/v1/charts/:dataset | GET | ✅ | ✅ | Retrieve the rows of a dataset |
Required role: read-only or read-write.
The datasets
| dataset | what it measures |
|---|---|
downtime | Downtime per bucket and asset, split by reason, category or kind |
downtime_pareto | The same downtime, ranked worst-first with a running total |
utilisation | The machine-time ledger: uptime, downtime by kind, powered-off time, and the two utilisation percentages |
energy | Energy consumption in kWh |
idle_energy | Energy consumed while machines sat idle, with the idle time behind it |
parts | Good, scrap and total parts, with the scrap share |
oee | Availability, performance, quality and OEE, with the inputs behind each |
job_oee | One row per job run in the period, on the machine that ran it |
downtime_stops | Stop statistics — stops, total, mean stop, MTBF, MTTR, top reason — per reason, category or machine |
downtime_events | The downtime event log: one row per stop |
cycle_times | Shortest, median and longest cycle per machine and cycle |
work_orders | Work orders against target: made, scrapped, progress, status, due date |
power | Average power in kW per bucket and asset (at most 7 days) |
Query parameters
Every parameter is optional — each dataset has its own defaults, listed with it below.
from,to— ISO8601 start and end, read in your organisation's timezone when they carry no offset. Must be provided together. A bare dateto(2026-08-16) means the whole of that day. An unparseable value, atobeforefrom, or a window longer than five years (seven days onpower) returns422.range— a named window resolved server-side in your organisation's timezone:today,yesterday,this_week,last_week,this_month,last_month,this_quarter,last_quarter,this_year,last_year,last_7_days,last_14_days,last_30_days,last_90_days,last_365_days, orrolling_<n>_<days|weeks|months|years>. Mutually exclusive withfrom/to; an unknown or forward-looking token returns422with the allowed list.bucket— the time grain of a row:hour,day,week,month,quarter, ormax(one bucket spanning the whole window). Each dataset lists the ones it offers; anything else is coerced and reported incoercion_notes.group_by— the entity grain of a row:none(the whole organisation),factory,area,machine,shift_pattern, andjobonoee. Same coercion rule.grain— ondowntimeanddowntime_pareto:reason,categoryorkind; ondowntime_stops:reason,categoryormachine. Refused on a dataset that has none, and a value outside the dataset's own list is a 422 naming the ones it takes.offset,limit— paging (see Paging).limitis at most 500, the default.factory_names[],area_names[],machine_names[]— scope by site, area or machine name (machine names also match foreign ids). Levels intersect. Unknown or ambiguous names return422with the candidates, never a silent guess; a site or area with no machines is a422, not the whole organisation.machine_foreign_ids[]— machine foreign ids to filter by. A single bare value (machine_foreign_ids=CNC-01) works too; an unknown id matches nothing.shift_ids[]— ondowntime,downtime_pareto,downtime_stops,downtime_eventsandutilisation: which slices of time count, as shift pattern names plus the reserved wordsall(every pattern, including ones added later) andout_of_shift. Defaults to["all"];["all","out_of_shift"]is 24/7.oeeandjob_oeerefuse it — OEE is measured per job, which runs straight through a shift handover — and so do the datasets that don't slice by shift.activity_reason_category_names[],activity_reason_names[]— on the four downtime datasets: only these categories / reasons.downtime_types[]— ondowntimeanddowntime_pareto: only these kinds,unproductive,productive,discounted.include_discounted— on the four downtime datasets:truecounts discounted time (planned maintenance, holidays, closures). Off by default.search— free-text over the dataset's own identifiers, onoee(part or work-order number),job_oee,cycle_times(cycle or program name) andwork_orders(work-order or part number).
A parameter a dataset does not take is refused with 422, naming it — whether it is unknown, a filter that dataset's query cannot honour, or a misspelling. Nothing is silently dropped: a 200 with the whole organisation in it, labelled as your selection, is the failure this rule exists to prevent. The GET /api/v1/hierarchy, /api/v1/shift_patterns and /api/v1/reason_categories endpoints list the names the filters take.
Internal database ids (machine_ids, activity_reason_category_ids, …) are refused — only names and foreign / external identifiers cross the API boundary.
Parameters that are refused
These are chart settings, and sending one returns 422 with the list of columns the rows carry instead. They are refused rather than ignored, because a request that silently returned the default variant is how a caller ends up reading 0–1 fractions as second-durations.
| parameter | what to do instead |
|---|---|
measure, basis, column | Read the column you want — every row carries all of them |
orientation, template | Presentation only; the API returns rows |
compare, compare_to, previous_from, previous_to | Issue two requests with different windows |
Response shape
Every dataset returns the same envelope. filter_applied echoes the resolved filter — defaults and coercions included — so you always see the window and grain the data actually covers, and columns describes what is in each row.
{
"dataset": "utilisation",
"title": "Utilisation",
"columns": [
{ "name": "entity_name" },
{ "name": "uptime_seconds", "label": "Spindle Uptime", "format": "duration", "unit": "seconds" },
{ "name": "percentage", "label": "Share of available time", "format": "percent", "unit": "fraction (0-1)" }
],
"filter_applied": {
"from": "2025-05-20T00:00:00+00:00",
"to": "2025-05-27T00:00:00+00:00",
"bucket": "day",
"group_by": "machine",
"shift_ids": ["all"]
},
"coercion_notes": [],
"warnings": [],
"row_count": 1,
"total_row_count": 1,
"offset": 0,
"limit": 500,
"next_offset": null,
"truncated": false,
"rows": [
{
"entity_name": "CNC-01",
"from": "2025-05-20T00:00:00+00:00",
"to": "2025-05-21T00:00:00+00:00",
"uptime_seconds": 21600,
"downtime_seconds": 5400,
"offtime_seconds": 1800,
"productive_downtime_seconds": 3600,
"unproductive_downtime_seconds": 1800,
"discounted_downtime_seconds": 0,
"unaccounted_downtime_seconds": 0,
"total_seconds": 28800,
"percentage": 0.875,
"spindle_percentage": 0.75,
"shift_type": "in_shift",
"machine_foreign_id": "CNC-01"
}
]
}
Three things to read every time:
coercion_notes— a bucket or grouping the dataset does not offer is corrected, not refused, and each correction is listed here with the reason. A caller that asked for hourly data and silently got daily would report the wrong thing.columns— the label, format and unit of every column, from the aggregator's own catalog. Percent values are 0–1 fractions everywhere in this API — every dataset, every column — and durations are integer seconds.next_offset— responses carry at most 500 rows. When there are more,truncatedistrue,next_offsetsays where the next page starts and awarningsentry says how many rows there are in all.
When the request is grouped by machine, each row carries machine_foreign_id so results can be joined back to the ids you filtered by. Rows never contain internal database ids.
Columns with an organisation target (utilisation and OEE percentages) carry it as target in columns on the MCP describe_chart answer and in every envelope — the figure the dashboard's KPI cards colour against, as a fraction.
Paging
Rows come back in a fixed order per dataset (each dataset lists it below), so offset N is the same row on every call over the same data. Pass the previous response's next_offset as offset until it is null:
curl "https://your-org.trackmymachines.app/api/v1/charts/downtime_events?range=last_month&offset=500" \
-H "Authorization: Bearer YOUR_API_KEY"
The array endpoints (/events, /machine_activities, /jobs) take the same offset and limit; their body stays an array, so the next page is in the X-Next-Offset response header (absent on the last page) beside X-Total-Count.
Rate limit
Every API key or OAuth token may make 60 requests a minute across /api/v1/* and /mcp. The 61st returns 429 Too Many Requests with a Retry-After header (seconds). Separately, one IP address may make at most 300 requests a minute across the same paths, whatever credentials they carry. The limit is counted per server instance, so it is a floor, not a guarantee of throughput.
Comparing two periods
There is no compare parameter: request the two windows separately. To compare against an average of N periods, request the N periods as one window and divide durations, counts and kWh by N; percentages and other rates need no divisor — the pooled figure is already the time-weighted mean, which weights a quiet week less than a busy one, as it should.
Example request
curl -X GET "https://your-org.trackmymachines.app/api/v1/charts/utilisation?range=last_week&bucket=day&group_by=machine" \
-H "Authorization: Bearer YOUR_API_KEY"
Each dataset
The column tables below are generated from the aggregators' own column catalogs. A blank label/format/unit means the column is an identity rather than a measured figure — a name, a timestamp, or the bucket a row covers.
downtime
Downtime per bucket and asset, split by individual reason, reason category, or kind of downtime. Every row carries both the hours and the share of that asset's own scheduled time — the "% of scheduled time" view of the dashboards is a column here, not a mode.
- Defaults:
range=this_week,bucket=max,group_by=machine - Buckets:
hour,day,week,month,quarter,max - Groupings:
none,factory,area,machine,shift_pattern grain:reason(default) ·category·kind
| column | what it is | format | unit |
|---|---|---|---|
entity_name | |||
bucket | |||
duration_seconds | Downtime | duration | seconds |
activity_reason_name | present at grain=reason | ||
activity_reason_category_name | present at grain=category and grain=kind | ||
percentage | Downtime as a share of the asset's scheduled time | percent | fraction (0-1) |
downtime_cost | Cost of downtime at your organisation's hourly rate; null when no rate is set | currency |
Rows are ordered by bucket, entity_name, category, reason. percentage is each row's downtime over that entity's own denominator — scheduled seconds in shift, elapsed minus scheduled out of shift, elapsed calendar otherwise, picked by the shift filter. Assets with no downtime appear at 0, and assets with no usable denominator appear with percentage: null rather than at 0 — a machine that cannot be measured must not read as a clean one.
grain=kind includes discounted time (planned maintenance, holidays, closures), because the kinds are its subject. Every other grain excludes it unless you pass include_discounted=true.
downtime_pareto
The same downtime, ranked worst-first with a running total: the few causes behind most of the loss.
- Defaults:
range=this_week,bucket=max,group_by=none - Buckets:
maxonly — a Pareto ranks the whole period - Groupings:
noneonly — a Pareto ranks across the organisation grain:reason(default) ·category·kind
| column | what it is | format | unit |
|---|---|---|---|
activity_reason_name | |||
duration_seconds | Downtime | duration | seconds |
percent | Share of downtime | percent | fraction (0-1) |
cumulative_percent | Running share | percent | fraction (0-1) |
downtime_cost | Cost of downtime; null without a rate | currency |
Rows are ranked worst-first, ties by name.
utilisation
The machine-time ledger per bucket and asset: spindle uptime, productive and unproductive downtime, discounted and unaccounted time, powered-off time, and the two utilisation percentages computed from them.
- Defaults:
range=last_7_days,bucket=day,group_by=none - Buckets:
hour,day,week,month,quarter,max - Groupings:
none,factory,area,machine,shift_pattern
| column | what it is | format | unit |
|---|---|---|---|
entity_name | |||
from | |||
to | |||
uptime_seconds | Spindle Uptime | duration | seconds |
downtime_seconds | Downtime | duration | seconds |
offtime_seconds | Powered Off | duration | seconds |
productive_downtime_seconds | Productive Downtime | duration | seconds |
unproductive_downtime_seconds | Unproductive Downtime | duration | seconds |
discounted_downtime_seconds | Discounted Downtime | duration | seconds |
unaccounted_downtime_seconds | Unaccounted Downtime | duration | seconds |
total_seconds | Total Time | duration | seconds |
percentage | Share of available time | percent | fraction (0-1) |
spindle_percentage | Spindle Utilisation | percent | fraction (0-1) |
shift_type |
The two percentages are two definitions of the same question. spindle_percentage is spindle uptime over total time; percentage is uptime plus productive downtime over non-discounted time. uptime_seconds is the numerator of the first and uptime_seconds + productive_downtime_seconds of the second.
energy
Energy consumption in kWh per bucket and asset.
- Defaults:
range=this_week,bucket=week,group_by=machine - Buckets:
hour,day,week,month,quarter,max - Groupings:
none,factory,area,machine— energy is not tied to shift patterns
| column | what it is | format | unit |
|---|---|---|---|
entity_name | |||
bucket | |||
energy_kwh | Energy | float | kWh |
idle_energy
Energy consumed while machines sat idle — powered, not cutting — with the idle time behind it.
- Defaults:
range=this_week,bucket=day,group_by=machine - Buckets:
hour,day,week,month,quarter,max - Groupings:
none,factory,area,machine— idle energy is not tied to shift patterns
| column | what it is | format | unit |
|---|---|---|---|
entity_name | |||
bucket | |||
idle_seconds | Idle Time | duration | seconds |
idle_energy_kwh | Idle Energy | float | kWh |
parts
Good, scrap and total parts over the whole window, per asset, with the scrap share. Operator-confirmed quantities where a job was confirmed, live counts everywhere else.
- Defaults:
range=this_week,bucket=max,group_by=none - Buckets:
maxonly — part counts are totalled over the whole window - Groupings:
none,factory,area,machine
| column | what it is | format | unit |
|---|---|---|---|
entity_name | |||
good_parts | Good Parts | integer | |
total_parts | Total Parts | integer | |
scrap_parts | Scrap Parts | integer | |
scrap_rate | Scrap Rate | percent | fraction (0-1) |
scrap_rate is null when nothing was produced.
oee
Availability, performance, quality and their product, per bucket and entity, with the raw inputs behind each factor. All four factors ride every row: there is no factor parameter, you read the column you want.
- Defaults:
range=this_week,bucket=max,group_by=none - Buckets:
hour,day,week,month,quarter,max - Groupings:
none,factory,area,machine,shift_pattern,job
| column | what it is | format | unit |
|---|---|---|---|
entity_name | |||
bucket | |||
from | |||
to | |||
availability | Availability | percent | fraction (0-1) |
performance | Performance | percent | fraction (0-1) |
quality | Quality | percent | fraction (0-1) |
oee | OEE | percent | fraction (0-1) |
operating_seconds | Operating Time — availability's numerator | duration | seconds |
scored_operating_seconds | Operating Time (Performance) — performance's denominator: operating time on jobs with a computable ideal | duration | seconds |
planned_seconds | Planned Production Time — availability's denominator | duration | seconds |
ideal_seconds | Ideal Run Time — performance's numerator | duration | seconds |
good_parts | Good Parts | integer | |
total_parts | Total Parts | integer | |
scrap_parts | Scrap Parts | integer | |
quantities_confirmed | Includes Confirmed Quantities — true when any slice used operator-confirmed counts | boolean |
search narrows the runs by part number or work-order identifier. total_parts and good_parts are finished parts, prorated across sub-operations, so they can differ from parts by less than one per job. A window with no production returns no rows, not a row of nulls.
job_oee
One row per job run in the period, on the machine that ran it: the three OEE factors, the part counts behind them and the cycle times.
- Defaults:
range=this_week,bucket=max,group_by=machine - Buckets:
maxonly — Job OEE covers each job in full, the period is never sub-divided - Groupings:
machineonly — always one row per job, on the machine that ran it searchmatches: part number, work-order identifier or operation name
| column | what it is | format | unit |
|---|---|---|---|
external_id | |||
eventable_name | |||
machine_name | |||
started_at | |||
ended_at | |||
part_count | Total Parts — confirmed once the job is signed off, measured until then | integer | |
scrap_count | Scrap Parts | integer | |
expected_part_count | Expected Parts | integer | |
machine_part_count | Measured Parts | integer | |
confirmed_part_count | Confirmed Parts | integer | |
availability | Availability | percent | fraction (0-1) |
performance | Performance | percent | fraction (0-1) |
quality | Quality | percent | fraction (0-1) |
oee | OEE | percent | fraction (0-1) |
ideal_seconds | Ideal Run Time | duration | seconds |
expected_cycle_time | Expected Cycle Time | duration | seconds |
median_button_to_button | Median Button to Button | duration | seconds |
quantities_confirmed | Quantities Confirmed | boolean |
downtime_stops
Stop statistics over the whole window — whole stops, which downtime (clipped, merged hours) cannot count.
- Defaults:
range=this_week;bucket=maxandgroup_by=noneonly grain:reason(default) ·category·machine— what one row is- Ordered by
total_secondsdescending, thenentity_name
| column | what it is | format | unit |
|---|---|---|---|
entity_name | the reason, category or machine | ||
category_name | the reason's category (grain=reason) | ||
stops | Stops | integer | |
total_seconds | Downtime | duration | seconds |
mean_stop_seconds | Mean stop | duration | seconds |
share | Share of the downtime in these rows | percent | fraction (0-1) |
mtbf_seconds | MTBF: running time between stops (grain=machine) | duration | seconds |
mttr_seconds | MTTR: the mean stop | duration | seconds |
share_of_planned | Downtime as a share of the machine's planned time (grain=machine) | percent | fraction (0-1) |
top_reason_name | the reason it stopped for most (grain=machine) |
downtime_events
The downtime event log: one row per stop touching the window, newest first, with the seconds of it inside the window and the shift selection. Paged in the query — read next_offset.
- Defaults:
range=this_week - Ordered by
started_atdescending, then machine and reason
| column | what it is | format | unit |
|---|---|---|---|
machine_name | |||
started_at | |||
seconds | Downtime inside the window and the shift selection | duration | seconds |
reason_name | |||
category_name |
cycle_times
Cycle time per machine and cycle: a toleranced cycle timed by its own events, a machine program by the gap between consecutive part counts inside one run of it.
- Defaults:
range=this_week; one row per machine and cycle searchmatches the cycle or program name
| column | what it is | format | unit |
|---|---|---|---|
machine_name | |||
cycle_name | |||
cycles | Cycles timed | integer | |
minimum_seconds | Shortest cycle | duration | seconds |
median_seconds | Median cycle | duration | seconds |
maximum_seconds | Longest cycle | duration | seconds |
work_orders
One row per work order due in the window or worked in it. Work orders belong to the organisation, not to a machine, so there is no machine filter.
- Defaults:
range=this_week; ordered bydue_date, thenwork_order searchmatches the work-order or part number
| column | what it is | format | unit |
|---|---|---|---|
work_order | |||
external_id | |||
part_name | |||
quantity_expected | Target quantity | integer | |
quantity_made | Quantity made | integer | |
quantity_scrapped | Quantity scrapped | integer | |
progress | Made as a share of target (not capped at 1) | percent | fraction (0-1) |
status | planned, released, in_progress, completed, cancelled | ||
due_date | |||
overdue | Past due and not finished | boolean | |
machine_names | the machines that ran it |
power
Average electrical power per bucket and asset: the bucket's kWh over its hours, offline minutes counting as 0 kW. The one-minute source is kept for a week, so a window may be at most 7 days (named ranges included).
- Defaults:
range=today,bucket=hour,group_by=machine - Buckets:
hour,day,max; groupings:none,factory,area,machine
| column | what it is | format | unit |
|---|---|---|---|
entity_name | |||
bucket | |||
avg_power | Average Power | float | kW |
Renamed chart types
Until 2026 this endpoint published nineteen chart types — nineteen names for eight of these queries, six of them the same utilisation query drawn differently and four the same OEE row with a different figure lifted out. They now answer with a 404 that names the dataset and the parameters that replace them:
old chart_type | dataset | parameters |
|---|---|---|
downtime_by_asset | downtime | group_by=machine (or factory / area) |
downtime_by_reason | downtime | group_by=none |
downtime_analysis | utilisation | the five-way split is columns of every row |
downtime_hours | utilisation | read unproductive_downtime_seconds |
utilisation_over_time | utilisation | bucket=day (or week / month) |
utilisation_ranking | utilisation | bucket=max&group_by=machine, then sort by percentage |
shift_breakdown | utilisation | bucket=max&group_by=shift_pattern |
calendar | utilisation | bucket=day&group_by=factory, then read percentage |
total_parts | parts | read total_parts |
good_parts | parts | read good_parts |
scrap_rate | parts | read scrap_rate |
oee_availability | oee | read the availability column |
oee_performance | oee | read the performance column |
oee_quality | oee | read the quality column |
downtime_pareto, energy, idle_energy, oee and job_oee are dataset names already and keep working unchanged — though oee now returns every factor on the row, and no value field.
Older names still in circulation — machine_utilisation, effective_utilisation, machine_hours, effective_hours, downtime_reason_hours, downtime_reason_percent, the _category_ and _horizontal variants, downtime_reason_pareto — were retired earlier and return a plain 404. machine_utilisation and effective_utilisation are utilisation (read spindle_percentage and percentage); the downtime reason family is downtime at the matching grain; downtime_reason_pareto is downtime_pareto.
What else changed
- There is no
valuefield, and noreturns. Every dataset returns rows. The four OEE factors and the three part counts were single-figure chart types; they are columns. formatandunitmoved from the envelope tocolumns. A dataset row carries several formats at once — hours and a share, four factors and their inputs — which one envelope-level format could never describe.chart_typein the envelope is nowdataset.
Error responses
401 Unauthorized— missing, invalid, expired, or wrong-organisation API key.404 Not Found— the name is not a dataset. The body carriesdatasets(every name) and, for a retired chart type, the dataset and parameters that replace it.422 Unprocessable Entity— an unknown parameter (withparameters, the ones this dataset takes); a filter the dataset cannot honour (the message names the datasets that take it);from/tounparseable, inverted, too long or provided without the other;rangeunknown or sent alongsidefrom/to; an unknown or ambiguous site / area / machine / reason / category name (withcandidates); a refused presentation or comparison parameter (withcolumns);limitabove 500.429 Too Many Requests— over 60 requests a minute on this credential; waitRetry-Afterseconds.