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 1Three equivalent channels
| Channel | When to use it |
|---|---|
| REST API | New 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. |
| MQTT | Messaging 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
- Base URL:
http://<machine-pc>:1880/api/erp(orhttps://if a certificate is installed on the PC). - Format: UTF-8 JSON for input and output.
- Dates:
YYYY-MM-DDfor the due date; dates and times returned asYYYY-MM-DD HH:mm:ssin the machine's time zone.
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.
| Situation | Response |
|---|---|
| No key generated | 503 API not active |
| Header missing or wrong key | 401 |
| Initial setup not completed | 409 |
| License read-only or blocked | 403 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" }
}
| Field | Required | Type | Notes |
|---|---|---|---|
erp_id | Yes | text (max 100: letters, numbers, . _ / -) | Unique identifier of the work order in the management system |
description | Yes | text | |
qty_requested | Yes | integer ≥ 1 | Required parts |
item_code, program_name, program_number, customer_code, customer_name, order_number, operator_name, notes | No | text (max 500) | |
target_time | No | integer ≥ 0 | Target cycle time (seconds per part) |
target_energy | No | integer ≥ 0 | Target energy (Wh per part) |
due_date | No | YYYY-MM-DD | Due date |
extra | No | object | Only 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
| Parameter | Example | Meaning |
|---|---|---|
status | RUNNING | Work order status |
column | in_progress | requested, in_progress, done |
source | erp | erp, manual (from the Kanban board), machine (created by the machine) |
updatedSince | 2026-09-27 08:00:00 | Only work orders updated since this moment (machine local time) |
limit, offset | 100, 0 | Pagination (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" }
status:RUNNING,PAUSED,STOPPED,COMPLETED,SET-UP,REWORK,MAINTENANCE,CLEANING,FAULT, orREQUESTED(back to “To do”);- alternatively
column:requested,in_progress(=RUNNING),done(=COMPLETED).
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" }
}
| Field | Meaning |
|---|---|
qty_done / qty_scrap / qty_good | Parts 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_time | Seconds of machine running time for the work order |
cycletime_avg | Average 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.
| Code | When | Details |
|---|---|---|
400 | Invalid data | dettagli: [{ "campo": "qty_requested", "messaggio": "..." }]: all the invalid fields are listed |
400 | Deleting a work order in progress | codice: "in_lavorazione" |
401 | Key missing or wrong | |
403 | License expired or blocked (writes) | |
404 | Work order not found or unknown endpoint | |
409 | erp_id already exists | codice: "erp_id_duplicato" |
409 | More than 15 work orders in “To do” on the workstation | codice: "coda_piena" |
503 | API 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:
- in the RTIndustry database, for software installed on the same PC (
readwois an always up-to-date view); - in a company MySQL database: RTIndustry connects to the given database, reads
writewoand writes progress back toreadwo. The “Create missing tables” button creates the two tables with the version 1 schema. The password is stored encrypted on the workstation.
The check runs every 10 seconds (configurable).
Life cycle of a row
readed written by the ERP | Meaning | Result written by RTIndustry |
|---|---|---|
0 | New work order | 1 and kanbanwoid = id of the work order in RTIndustry |
2 | Work order edited (even if in progress) | 1 |
5 (new in 2.0) | Delete the work order | 1 |
3+rejectedreason: rejected by the workstation (queue full, work order in progress cannot be deleted, license read-only). RTIndustry retries by itself every minute.4+rejectedreason: invalid data (for example missingdescription). The ERP corrects the row and setsreaded = 2again.
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
statuscolumn (new in 2.0, optional):RUNNING,PAUSED,COMPLETED,REQUESTED… withreaded = 2. It is a command: once executed, RTIndustry sets it back toNULL, so a later edit (for example of the quantity) does not cancel a pause decided by the operator;- version 1
columncolumn:in_progressordone. To send a work order that has already started back to “To do”, usestatus = 'REQUESTED'.
-- 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.
| Topic | Direction | Content |
|---|---|---|
{deviceSN}/read | out | Telemetry (readable without credentials) |
{deviceSN}/write | in | Work order commands (authenticated clients only) |
{deviceSN}/write/result | out | Result of each command |
{deviceSN}/wo | out | Event 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 = 0 | POST /api/erp/workorders |
UPDATE writewo ... readed = 2 (edit) | PUT /api/erp/workorders/{erp_id} |
Wait for readed = 1 | Immediate 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 readwo | GET /api/erp/workorders (+ updatedSince) |
column in writewo | POST /api/erp/workorders/{erp_id}/status |
| No equivalent | DELETE /api/erp/workorders/{erp_id} |
writewo field | API field |
|---|---|
kanbanwoid / ERP id | erp_id |
programname, programnumber | program_name, program_number |
description, itemcode | description, item_code |
quantity | qty_requested |
customername, customercode | customer_name, customer_code |
operatorname, notes | operator_name, notes |
duedate, targettime, targetenergy | due_date, target_time, target_energy |
receipt, wovalue, spare1…spare5 | extra.receipt, extra.wo_value, extra.spare1…spare5 |
client, plant, workstation, serial | Not 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).