Skip to main content

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

EndpointMethodRead-only AccessRead-write AccessDescription
/api/v1/jobsGETRetrieve jobs
/api/v1/jobs/:idGETRetrieve a single job by external_id (evt_…)
/api/v1/jobsPOSTStart a job (clock on), optionally finishing it (clock off)
/api/v1/jobs/:idPATCHUpdate 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 by GET /api/v1/jobs/:id and PATCH /api/v1/jobs/:id.
  • machine_foreign_id: machine foreign ID (or null if not set).
  • from / to / duration_seconds: job start / end / length; to and duration_seconds are null while 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 for unproductive_downtime, kept for backward-compatibility.
  • percent: utilisation as a percentage (not seconds).
  • bookable_time: time bookable against the job; currently aliases total_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's external_id (e.g. evt_8h3KdL2pQrxV) — the same identifier returned by the POST endpoint's event.id field and used by PATCH /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_name OR machine_foreign_id: display name or foreign ID of the machine
  • start: start timestamp (ISO8601)
  • user_email OR user_foreign_id: email or foreign ID of the user associated with the job
  • part_unique_identifier: unique identifier for the part
  • part_issue: issue/variant identifier for the part (e.g. "A", "B", "REVISION_1")
  • recipe_version: version of the recipe
  • step_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 quantity
  • quantity_made: quantity made
  • quantity_scrapped: quantity scrapped
  • wo_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_number becomes a required field (in addition to the other required fields above).
  • A WorkOrder is found or created from wo_number and a WorkOrderOperation is created for the given step_number if one does not already exist.
  • The response includes two extra top-level objects, work_order and work_order_operation, alongside event, step, recipe, part, and part_issue.
  • The event.type field will be work_order_operation instead of step.
  • When a finish timestamp 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>" }