Jobs
The jobs endpoint allows you to retrieve, start, and finish jobs in your manufacturing process. TrackMyMachines uses this data to produce reports, calculate estimated finish times, and alert you when your machines run jobs exceeding the quoted time from your ERP system.
:::note Renamed from step_events
This endpoint used to be /api/v1/step_events, and took its parameters wrapped in a step_event key. It was renamed because it no longer only handles step events — with work orders enabled it starts and finishes work order operations too.
The old URL and the old wrapped body both still work, so existing integrations keep running, but they are deprecated. New integrations should use /api/v1/jobs with the flat body shown below.
:::
Permissions
| Endpoint | Method | Read-only Access | Read-write Access | Description |
|---|---|---|---|---|
/api/v1/jobs | GET | ✅ | ✅ | Retrieve jobs |
/api/v1/jobs/:id | GET | ✅ | ✅ | Retrieve a single job by external_id (evt_…) |
/api/v1/jobs | POST | ❌ | ✅ | Start a job (clock on), optionally finishing it (clock off) |
/api/v1/jobs/:id | PATCH | ❌ | ✅ | Update an in-flight job by external_id (evt_…) |
Endpoints
GET /api/v1/jobs
Retrieves jobs based on the provided filters.
Required role: read-only or read-write
Query parameters:
from: start of the time range for filtering jobs (default: current week)to: end of the time range for filtering jobs (default: current week)machine_foreign_ids[]: array of machine foreign IDs to filter jobs by
Internal database IDs (machine_ids) are not accepted on input — only foreign identifiers cross the API boundary.
Example request:
curl -X GET "https://your-org.trackmymachines.app/api/v1/jobs?from=2025-05-20&to=2025-05-27&machine_foreign_ids[]=MACHINE_001&machine_foreign_ids[]=MACHINE_002" \
-H "Authorization: Bearer YOUR_API_KEY"
Example response:
[
{
"id": "evt_8h3KdL2pQrxV",
"machine_foreign_id": "MACHINE_001",
"from": "2025-06-18T09:00:00Z",
"to": null,
"duration_seconds": null,
"expected_cycle_time": 45,
"expected_setup_time": 600,
"uptime": 12833,
"downtime": 1734,
"offtime": 0,
"total": 14567,
"unproductive_downtime": 971,
"productive_downtime": 763,
"discounted_downtime": 0,
"total_productive_time": 14567,
"total_unproductive_downtime": 971,
"percent": 88.1,
"bookable_time": 14567,
"cycle_count": 273,
"minimum_cycle_time": 33,
"median_cycle_time": 46,
"maximum_cycle_time": 64,
"total_in_cycle_time": 12833,
"part_count": 243,
"time_to_first_part_count": 1637024,
"minimum_time_between_part_counts": 135,
"maximum_time_between_part_counts": 1127,
"median_time_between_part_counts": 145,
"maximum_load_time": 210,
"median_load_time": 140,
"minimum_load_time": 120,
"total_part_count_time": 37568,
"total_dead_time": 4763,
"metadata": {
"quantity_expected": "15"
}
}
]
Response fields:
id: prefixed external event identifier (evt_…); the same value accepted byGET /api/v1/jobs/:idandPATCH /api/v1/jobs/:id.machine_foreign_id: machine foreign ID (ornullif not set).from/to/duration_seconds: job start / end / length;toandduration_secondsarenullwhile the job is in flight.
Utilisation metrics (all in seconds unless noted) describe how the machine spent the job's wall-clock time:
uptime: time the machine was running.downtime: total stopped time (unproductive_downtime+productive_downtime).offtime: time outside the machine's configured operating hours.total: total elapsed time covered by the job.unproductive_downtime: downtime not attributed to a productive activity.productive_downtime: downtime classified as productive (e.g. setup, tool changes).discounted_downtime: downtime excluded from utilisation calculations (e.g. planned/discounted reasons).total_productive_time: total productive time (uptime+productive_downtime).total_unproductive_downtime: alias forunproductive_downtime, kept for backward-compatibility.percent: utilisation as a percentage (not seconds).bookable_time: time bookable against the job; currently aliasestotal_productive_time.
Cycle metrics: cycle_count, minimum_cycle_time, median_cycle_time, maximum_cycle_time, total_in_cycle_time.
Part-count metrics: part_count, time_to_first_part_count, minimum_time_between_part_counts, maximum_time_between_part_counts, median_time_between_part_counts, minimum_load_time, median_load_time, maximum_load_time, total_part_count_time, total_dead_time.
All of the above are returned for each in-flight or finished job.
Internal database IDs (machine_id, numeric event IDs, eventable_type/eventable_id) are intentionally never exposed — only foreign / external identifiers cross the API boundary.
GET /api/v1/jobs/:id
Retrieves a single job by its external_id (evt_…), with the same aggregated fields as the index endpoint.
Required role: read-only or read-write
Path parameters:
id: the job'sexternal_id(e.g.evt_8h3KdL2pQrxV) — the same identifier returned by thePOSTendpoint'sevent.idfield and used byPATCH /api/v1/jobs/:id.
Example request:
curl -X GET "https://your-org.trackmymachines.app/api/v1/jobs/evt_8h3KdL2pQrxV" \
-H "Authorization: Bearer YOUR_API_KEY"
The response is a single object matching one element of the GET /jobs array.
POST /api/v1/jobs
Clock on a job, creating it in the timeline.
Clocking on a job with parts, routes, or steps that don't exist will create them in TMM automatically.
This endpoint is idempotent across requests with the same body, so you can retry requests without creating duplicate jobs.
To finish a job that's been started, send the same minimum required parameters along with the optional finish parameter.
Required role: read-write
Request body:
{
"machine_display_name": "Machine1",
"start": "2025-05-27T10:00:00Z",
"user_email": "user@example.com",
"part_unique_identifier": "PART123",
"part_issue": "A",
"recipe_version": "1",
"step_number": "10",
"step_expected_cycle_time": 120,
"step_expected_setup_time": 30,
"quantity_expected": 5,
"wo_number": "WO-123"
}
Required fields:
machine_display_nameORmachine_foreign_id: display name or foreign ID of the machinestart: start timestamp (ISO8601)user_emailORuser_foreign_id: email or foreign ID of the user associated with the jobpart_unique_identifier: unique identifier for the partpart_issue: issue/variant identifier for the part (e.g. "A", "B", "REVISION_1")recipe_version: version of the recipestep_number: step number in the recipe
Optional fields:
finish: finish timestamp (ISO8601)step_expected_cycle_time: expected cycle time (seconds)step_expected_setup_time: expected setup time (seconds)quantity_expected: expected quantityquantity_made: quantity madequantity_scrapped: quantity scrappedwo_number: work order number
Example response:
{
"event": {
"id": "evt_8h3KdL2pQrxV",
"type": "step",
"machine_foreign_id": "MACHINE_001",
"user_foreign_id": "user-ext-001",
"from": "2025-05-27T10:00:00Z",
"to": null,
"duration_seconds": null,
"metadata": {
"quantity_expected": 5,
"wo_number": "WO-123"
}
},
"step": {
"number": 10,
"expected_cycle_time": 120,
"expected_setup_time": 30
},
"recipe": {
"version": "1"
},
"part": {
"unique_identifier": "PART123"
},
"part_issue": {
"issue": "A"
}
}
The response uses the same external-only convention as /api/v1/events: the public id is the prefixed external identifier (evt_…), machines are referenced by machine_foreign_id, users by user_foreign_id, parts / recipes / steps / part issues by their natural domain keys. Internal bigint PKs and FKs are never exposed.
Work-order mode
If your organisation has work orders enabled, the same POST /api/v1/jobs endpoint operates against work order operations rather than raw steps:
wo_numberbecomes a required field (in addition to the other required fields above).- A
WorkOrderis found or created fromwo_numberand aWorkOrderOperationis created for the givenstep_numberif one does not already exist. - The response includes two extra top-level objects,
work_orderandwork_order_operation, alongsideevent,step,recipe,part, andpart_issue. - The
event.typefield will bework_order_operationinstead ofstep. - When a
finishtimestamp is included, operation and work order quantities are recalculated from the finished jobs.
No flag in the request switches modes — it is determined by the organisation's configuration.
Step-mode finish returns only the event
When you finish a job in step (non-work-order) mode — i.e. POST with a finish timestamp that closes an already-open job — the response echoes back only the event object. The step, recipe, part, and part_issue sections are omitted. Starting a step-mode job (no finish, or a finish that opens a fresh event) still returns the full hierarchy. Work-order mode echoes everything on both start and finish. This is a long-standing quirk preserved on purpose — don't rely on the nested objects being present on a step-mode close-out.
The whole write (user auto-create, part/recipe/step hierarchy, work order + operation, opening or closing the event, quantity rollups) runs in a single database transaction, so a client that retries after a mid-request failure won't find a half-written job.
PATCH /api/v1/jobs/:id
Update an in-flight job — typically to clock it off, or to record quantity_made / quantity_scrapped. The :id path parameter is the job's external_id (evt_…), the same value returned by POST as event.id.
Required role: read-write
Request body:
{
"finish": "2025-05-27T11:30:00Z",
"quantity_made": 4,
"quantity_scrapped": 1
}
Fields (all optional, but at least one should be provided):
finish: finish timestamp (ISO8601). Sets the upper bound of the job's time range.quantity_made: quantity made — merged into the job's metadata.quantity_scrapped: quantity scrapped — merged into the job's metadata.
Example request:
curl -X PATCH "https://your-org.trackmymachines.app/api/v1/jobs/evt_8h3KdL2pQrxV" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"finish": "2025-05-27T11:30:00Z", "quantity_made": 4}'
Example response:
{
"event": {
"id": "evt_8h3KdL2pQrxV",
"type": "step",
"machine_foreign_id": "MACHINE_001",
"user_foreign_id": "user-ext-001",
"from": "2025-05-27T10:00:00Z",
"to": "2025-05-27T11:30:00Z",
"duration_seconds": 5400,
"metadata": { "quantity_made": 4, "wo_number": "WO-123" }
}
}
Error handling
Errors are returned as { "error": "..." } with an appropriate HTTP status.
Authentication (401)
Returned when the Authorization: Bearer … header is missing, or the key is invalid, expired, or used against the wrong organisation's subdomain.
{ "error": "Missing or invalid Authorization header" }
{ "error": "Invalid API key" }
{ "error": "Expired API key" }
{ "error": "Wrong organisation" }
Read-only API key attempting a write (403)
POST requires a read-write key.
{ "error": "This API key does not have write permissions" }
Machine not found (404)
The machine_foreign_id / machine_display_name didn't match a machine in your organisation.
{ "error": "Machine not found" }
User not found (404)
The user_foreign_id / user_email didn't match a user, and the organisation is not configured to auto-create users from the API.
{ "error": "User not found" }
Job not found (404)
GET /jobs/:id or PATCH /jobs/:id was given an external_id (evt_…) that doesn't exist in your organisation.
{ "error": "Event not found" }
Missing required parameters (422)
One or more required fields (see POST /api/v1/jobs) were blank. The message lists which ones, in a stable order.
{ "error": "Missing required parameters: part_unique_identifier, step_number" }
Invalid timestamp (422)
The start or finish value could not be parsed as an ISO8601 timestamp.
{ "error": "Invalid start timestamp" }
{ "error": "Invalid finish timestamp" }
Record invalid (422)
A model validation failed. The message details the reason.
{ "error": "Validation failed: <details>" }