ERP and MES integration

How a management system (ERP), a manufacturing execution system (MES) or a line PLC creates, edits, starts and deletes the work orders of an RTIndustry 2.0 workstation and reads their progress.

Contents Three channels Activation Authentication Creating a work order Reading progress Editing Changing status Deleting The returned work order Errors Examples Exchange table MQTT From version 1

Three equivalent channels

ChannelWhen to use it
REST APINew integrations, from any language.
Database exchange table (writewo / readwo)ERPs that already wrote to the version 1 database, or software that prefers to write to a table.
MQTTMessaging systems, SCADA, IoT gateways.

All channels apply the same rules: data validation, a maximum queue of 15 work orders to do, only one work order in progress, license status. Every operation, even a rejected one, is logged on the workstation together with the channel it came from, which helps support.

Activation

The channels are enabled in Settings › Work order integration, reserved for the administrator. The key for REST and MQTT is generated in the same section.

REST API: authentication

Every request must include the header:

X-Api-Key: rti_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

The key is shown only once: RTIndustry keeps only its fingerprint, so a lost key cannot be recovered but must be regenerated. Regenerating or revoking it invalidates it immediately.

SituationResponse
No key generated503 API not active
Header missing or wrong key401
Initial setup not completed409
License read-only or blocked403 on writes (reads work)

Creating a work order — POST /workorders

{
  "erp_id": "MO-2026/0001",
  "description": "DN80 steel flange",
  "qty_requested": 50,
  "item_code": "FL-DN80",
  "program_name": "O1001",
  "program_number": "1001",
  "customer_code": "C-0042",
  "customer_name": "Sample customer",
  "order_number": "ORD-778",
  "operator_name": "Operator 1",
  "target_time": 30,
  "target_energy": 150,
  "due_date": "2026-10-15",
  "notes": "High priority",
  "extra": { "receipt": "DN-77", "wo_value": 1250.50, "spare1": "batch A" }
}
FieldRequiredTypeNotes
erp_idYestext (max 100: letters, numbers, . _ / -)Unique identifier of the work order in the management system
descriptionYestext
qty_requestedYesinteger ≥ 1Required parts
item_code, program_name, program_number, customer_code, customer_name, order_number, operator_name, notesNotext (max 500)
target_timeNointeger ≥ 0Target cycle time (seconds per part)
target_energyNointeger ≥ 0Target energy (Wh per part)
due_dateNoYYYY-MM-DDDue date
extraNoobjectOnly receipt, wo_value, spare1…spare5, returned unchanged

Response 201 with the complete work order (see below). The work order goes into the To do column (status: "REQUESTED"); the operator starts it from the Kanban board, or the management system starts it with a status change.

Reading progress — GET /workorders

ParameterExampleMeaning
statusRUNNINGWork order status
columnin_progressrequested, in_progress, done
sourceerperp, manual (from the Kanban board), machine (created by the machine)
updatedSince2026-09-27 08:00:00Only work orders updated since this moment (machine local time)
limit, offset100, 0Pagination (maximum 500 per page)

Response: { "workorders": [ ... ], "total": 42, "limit": 100, "offset": 0 }. For incremental updates, poll every 1–5 minutes with updatedSince equal to the most recent last_update received.

Detail: GET /workorders/{erp_id}, with the erp_id URL-encoded (for example MO-2026%2F0001). 404 if it does not exist.

Editing — PUT /workorders/{erp_id}

Same fields as for creation, all optional (erp_id cannot be changed). The extra fields are merged with the existing ones. A work order in progress can be edited too, for example to increase the quantity: its status does not change.

Changing status — POST /workorders/{erp_id}/status

{ "status": "RUNNING" }

RUNNING pauses any other work order in progress (only one at a time); COMPLETED sets the end date. Response: the updated work order.

Deleting — DELETE /workorders/{erp_id}

Logical deletion: the work order remains in the internal history. 400 if it is in progress.

The returned work order

{
  "erp_id": "MO-2026/0001", "id": "ERP-MO-2026_0001", "source": "erp",
  "status": "RUNNING", "column": "in_progress",
  "description": "DN80 steel flange", "item_code": "FL-DN80",
  "qty_requested": 50, "qty_done": 18, "qty_scrap": 1, "qty_good": 17, "qty_rework": 0,
  "target_time": 30, "cycletime_avg": 28.4, "production_time": 511,
  "mwt_total": 640, "mwt_running": 540, "mwt_paused": 60, "mwt_stopped": 0, "mwt_alarm": 0,
  "mwt_maintenance": 0, "mwt_cleaning": 0, "mwt_reworked": 0, "mwt_setup": 40,
  "energy_quantity": 2150.5, "energy_pieces": 119.5, "energy_saving": 20.3,
  "due_date": "2026-10-15", "start_date": "2026-09-27 08:02:11", "end_date": null,
  "creation_date": "2026-09-27 07:55:00", "last_update": "2026-09-27 08:13:40",
  "extra": { "receipt": "DN-77", "wo_value": 1250.5, "spare1": "batch A" }
}
FieldMeaning
qty_done / qty_scrap / qty_goodParts produced / scrapped by the operator / good
mwt_*Seconds the work order has spent in each state set by the operator (running, paused, stopped, fault, maintenance, cleaning, rework, set-up); mwt_total is the sum
production_timeSeconds of machine running time for the work order
cycletime_avgAverage cycle time (seconds per part)
energy_*Energy consumed (Wh), Wh per part, % saving compared with target_energy

Counters are saved every 5 seconds: a reading can be at most 5 seconds behind.

Errors

All errors have the form { "error": "message", ... }, with the message text in Italian.

CodeWhenDetails
400Invalid datadettagli: [{ "campo": "qty_requested", "messaggio": "..." }]: all the invalid fields are listed
400Deleting a work order in progresscodice: "in_lavorazione"
401Key missing or wrong
403License expired or blocked (writes)
404Work order not found or unknown endpoint
409erp_id already existscodice: "erp_id_duplicato"
409More than 15 work orders in “To do” on the workstationcodice: "coda_piena"
503API not active (no key)

The keys dettagli, campo, messaggio and codice and the values of codice are fixed identifiers of the API (details, field, message, code): test them as they are.

Examples

curl -X POST http://machine-pc:1880/api/erp/workorders \
  -H "X-Api-Key: $RTI_KEY" -H "Content-Type: application/json" \
  -d '{"erp_id":"MO-1001","description":"Drive shaft","qty_requested":200}'

curl "http://machine-pc:1880/api/erp/workorders?source=erp&updatedSince=2026-09-27%2008:00:00" \
  -H "X-Api-Key: $RTI_KEY"
$h = @{ "X-Api-Key" = $env:RTI_KEY }
Invoke-RestMethod -Uri "http://machine-pc:1880/api/erp/workorders/MO-1001" -Headers $h

Database exchange table

Same contract as version 1: the external system writes to writewo, RTIndustry processes the rows and updates readed; progress is read from readwo. The tables can live:

The check runs every 10 seconds (configurable).

Life cycle of a row

readed written by the ERPMeaningResult written by RTIndustry
0New work order1 and kanbanwoid = id of the work order in RTIndustry
2Work order edited (even if in progress)1
5 (new in 2.0)Delete the work order1

The work order identifier (erp_id) is the id of the writewo row. Rows with serial or workstation filled in are processed only by the matching workstation (table shared by several machines); if empty, any workstation processes them.

Changing status

-- New work order
INSERT INTO writewo (readed, description, itemcode, quantity, duedate)
VALUES (0, 'Drive shaft', 'AX-12', 200, '2026-10-15');

-- Start it
UPDATE writewo SET status = 'RUNNING', readed = 2 WHERE id = 42;

-- Progress
SELECT id, status, qty_done, qty_scrap, mwt_running FROM readwo WHERE id = 'ERP-42';

MQTT

Built-in broker on port 1883 of the workstation.

TopicDirectionContent
{deviceSN}/readoutTelemetry (readable without credentials)
{deviceSN}/writeinWork order commands (authenticated clients only)
{deviceSN}/write/resultoutResult of each command
{deviceSN}/wooutEvent at every change of an external work order (including changes by the operator or by production)

Authentication: any username (for example erp), password = API key (the same as for REST). Wrong credentials: connection refused. A client without credentials can read the telemetry, but its commands are discarded. No client can publish to /read, /write/result or /wo.

Commands

{ "id": "c-1", "action": "create", "erp_id": "MO-1", "data": { "description": "Shaft", "qty_requested": 10 } }
{ "id": "c-2", "action": "update", "erp_id": "MO-1", "data": { "qty_requested": 12 } }
{ "id": "c-3", "action": "status", "erp_id": "MO-1", "status": "RUNNING" }
{ "id": "c-4", "action": "delete", "erp_id": "MO-1" }
{ "id": "c-5", "action": "get",    "erp_id": "MO-1" }
{ "id": "c-6", "action": "list",   "data": { "status": "RUNNING" } }

data has the same fields as the REST API; id is an identifier chosen by the sender and returned in the result.

Results and events

{ "id": "c-1", "action": "create", "erp_id": "MO-1", "ok": true,  "status": 201, "result": { "erp_id": "MO-1", "status": "REQUESTED" } }
{ "id": "c-4", "action": "delete", "erp_id": "MO-1", "ok": false, "status": 400, "error": "...", "codice": "in_lavorazione" }

{ "evento": "status", "eliminato": false, "workorder": { "erp_id": "MO-1", "status": "RUNNING", "qty_done": 3 } }

In events, evento is the type of change and eliminato (deleted) is true when the work order has been deleted.

mosquitto_pub -h machine-pc -u erp -P "$RTI_KEY" -t "RTI-XXXX/write" \
  -m '{"id":"1","action":"status","erp_id":"MO-1001","status":"RUNNING"}'
mosquitto_sub -h machine-pc -u erp -P "$RTI_KEY" -t "RTI-XXXX/write/result" -t "RTI-XXXX/wo"

From version 1

Those who cannot adapt their ERP keep using the tables. To move to the REST API:

Before (version 1, MySQL tables)Now (2.0, API)
INSERT INTO writewo (...) with readed = 0POST /api/erp/workorders
UPDATE writewo ... readed = 2 (edit)PUT /api/erp/workorders/{erp_id}
Wait for readed = 1Immediate 201 response: the work order is already in “To do”
readed = 3 (too many work orders / workstation error)409 with codice: "coda_piena" (or another message)
readed = 4 (read error)400 with the list of invalid fields in dettagli
SELECT * FROM readwoGET /api/erp/workorders (+ updatedSince)
column in writewoPOST /api/erp/workorders/{erp_id}/status
No equivalentDELETE /api/erp/workorders/{erp_id}
writewo fieldAPI field
kanbanwoid / ERP iderp_id
programname, programnumberprogram_name, program_number
description, itemcodedescription, item_code
quantityqty_requested
customername, customercodecustomer_name, customer_code
operatorname, notesoperator_name, notes
duedate, targettime, targetenergydue_date, target_time, target_energy
receipt, wovalue, spare1…spare5extra.receipt, extra.wo_value, extra.spare1…spare5
client, plant, workstation, serialNot needed: they are assigned by the workstation that receives the call

In readwo and in the API response, qty_done, qty_scrap, qty_requested, mwt_*, cycletime_avg, start_date, end_date and status have the same name; mwt_downtime no longer exists (it is the sum of mwt_stopped and mwt_alarm).

Need help with the integration? Write to assistenza@rtindustry.it. To evaluate RTIndustry with your management system: demo@rtindustry.it.