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.
step_eventsThis 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 | ❌ | ✅ | Finish a job or correct its quantities, by external_id (evt_…) |
These endpoints are also served on the shared host, https://api.trackmymachines.app/api/v1/jobs, where the API key alone decides the organisation (see Base URL). To find the machine identifiers to send, use GET /api/v1/machines.
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: the current week in your organisation's timezone)to: end of the time range (a bare date means the whole of that day).fromandtogo together; a malformed or inverted window, or one longer than a year, returns422.machine_foreign_ids[]: array of machine foreign IDs to filter jobs bysearch: part number, work-order identifier or operation nameoffset,limit: paging, at most 500 jobs per call (the default). Jobs come oldest first; when there are more, theX-Next-Offsetresponse header says where the next page starts (it is absent on the last page) andX-Total-Countgives the total.
Any other parameter returns 422 naming it. 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": 0.881,
"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 0–1 fraction (0.881is 88.1%),nullwhen there is no time to measure against.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)part_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:
user_foreign_idORuser_email: the operator who ran the job. Auser_foreign_idthat doesn't match a user is created as a no-login user if your organisation allows creating users from the API, and returns404otherwise. Auser_emailmust match an existing user. Leave both out to record the job with no operator.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
Clock off a job, or correct its quantity_made / quantity_scrapped — whether the job is still running or already finished. 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 end 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.
A body without finish changes only the quantities and leaves the job's start and end alone, so correcting the quantities on a finished job does not reopen it.
In work-order mode, the work order's and operation's quantities are recalculated after the update, as they are when POST finishes a job.
A job that has been deleted in TrackMyMachines can't be updated and returns 404.
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 no key is sent (in Authorization: Bearer … or X-API-Key), 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 and PATCH require 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, or PATCH was given a job that has been deleted.
{ "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>" }