Documentation

How Ncore is put together, and how to get a plant running on it. If something here does not match what the application does, the application is right and this is a bug — tell us.

Using Ncore

The shape of it

Ncore has four things in it, and everything else is a view onto them.

Equipment
the machines, and the sites and departments they stand in. Every record in the system eventually points at one of these.
Templates
a maintenance procedure written once: the steps, the readings to take, the checks to tick, and how often it comes round.
Work orders
a template on a particular day, for a particular machine, in front of a particular group. This is what an operator actually fills in.
Records
what a completed work order leaves behind — readings, faults, photographs, inspection entries and service history, all attached to the machine.

Setting a plant up is therefore: enter the equipment, write the templates, point them at the equipment, and let the schedule run. The rest of this page is that in order.

Equipment first

Start with the register, because templates bind to it. Under Equipment, add each machine with a code, a name and a type. The code is the identifier you will use everywhere else, including from the API — make it the one already painted on the machine or held in your other systems.

Sites and departments group machines. A site is a physical plant; a department is what it belongs to organizationally. Both are filters everywhere in the product, so it is worth getting them right at the start.

Each machine gets an asset passport: its identity, documents, readings over time, faults, and service history in one place. Open it from the register by clicking the plate.

Equipment can be nested. A boiler can be the parent of its burner and its feedwater pump, so a fault on a child rolls up to the thing an operator would call the machine.

Writing a procedure

A template is the SOP that used to live in a binder. Under Templates, create one and build it on the canvas as a flow of steps.

A step is one of a few kinds:

  • a reading — a number, with a unit and optionally a spec range it is judged against;
  • a check — a yes/no or pass/fail the operator ticks;
  • a choice — one of a fixed set of answers;
  • free text — for a description of a fault or a note;
  • a photograph — for the things a number cannot record.

Each step can be bound to a piece of equipment, which is what makes the resulting record attach to the right machine rather than to the round as a whole. A round that walks four machines produces readings against four machines.

Steps can be grouped into sections, and a section can be bound to equipment as a whole — the usual shape for a round that repeats the same six checks on each of five pumps.

How the day’s work appears

A template carries its own schedule: daily, weekly, monthly or yearly, at a time you set.

At the plant’s own midnight — its timezone, not ours — the work orders for that day are generated and put in front of the group the template is bound to. Nobody types a schedule.

If a day is missed because the system was unreachable, catch-up generation fills it in rather than leaving a hole in the record.

Ad-hoc work orders can be created by hand at any time, from Manage work orders, for the jobs that were not planned.

Doing the round

An operator signs in on whatever browser they have, sees the work orders for their group today, and works down them step by step.

Readings that fall outside their spec range are flagged as the number is entered, not discovered later in a report. A step can raise a fault, which marks the machine and opens a record against it.

What is signed is kept: each completed step carries who did it and when. That attribution is the point of the product — it is what makes the record evidence rather than a note.

Work orders can be viewed as a list or as paper sheets. The sheets exist because a work order is a form, and people who have filled in the paper version for twenty years find the drawn one easier to read. It is a display choice and changes nothing about the data.

Inspection records

Statutory periodic inspections are kept separately from routine maintenance, because they are asked for separately.

An inspection record carries the finding, the evidence, the dates, and who carried it out. Under Inspection records you can filter by machine, by site and by period, and export the result.

This is the screen to open when an inspector asks what was done to a machine and when.

Reports

Reports answer the questions a plant is actually asked: what was done on time, what is still open, how late it is, what is down, how long it has been down, and how often a given machine fails.

  • Compliance — completion against what was scheduled.
  • Overdue — what is late, and by how much.
  • Faults and availability — downtime and MTBF, per machine and per site.
  • Out of spec — readings that fell outside their range.
  • Data quality — where records are thin, so you know how much to trust the rest.
  • Company and departments — the same figures rolled up across sites.

Every report can be read for one site or across the whole company, and every one exports.

People, groups and access

There are two roles: administrator and operator. Administrators configure the system; operators do the work in front of them.

Operators do not need email addresses. An administrator creates the account, and the operator signs in with a username plus the organization’s handle. Only people who need invitations and password resets need an address.

Groups are how work is routed. A template is bound to a group — Maintenance, Quality Control, a particular shift — and the work orders it generates appear for the people in it.

An organization’s data is fenced off in the database itself, not just in the application: a query cannot return another organization’s rows even if application code forgets a filter.

Getting your data out

Work orders, equipment, readings and inspection records all export as CSV from inside the application, on every plan, without asking us.

This is deliberate. A maintenance history that can only be read through the tool that made it is a liability, not an asset.

Integration

Keys and scopes

The integration API is how a SCADA system, a historian or a PLC gateway writes readings in without anybody retyping them.

Create a key under Integrations. A key is shown once, at creation — we store only a short prefix in clear and a hash of the rest, so a lost key is replaced rather than recovered.

Every key is scoped to what the integration actually needs, and can be revoked at any moment:

  • assets:read — list machines and metric names
  • readings:write — submit sensor values and counters
  • events:write — submit alarms raised by the control system
  • service:write — submit service and repair records

Send the key as a bearer token. Requests are rate limited per key; a key being used in a way that threatens the service may be revoked.

Address machines by their code — TGV-CHP1 — rather than by internal id. The code is what is written on the machine and held in your other systems, and it survives a re-import that changes ids.
GET/api/v1/whoamiany key

Check a key

Returns what the key is called and what it may do. Writes nothing — the safest first call when setting an integration up.

Example request
curl http://localhost:60872/api/v1/whoami \
  -H "Authorization: Bearer $SOP_API_KEY"
Response
{
  "name": "Boiler house gateway",
  "prefix": "5a75b874",
  "scopes": [
    "readings:write",
    "events:write"
  ],
  "expiresAt": null,
  "serverTime": "2026-08-04T09:30:00.000Z"
}
GET/api/v1/assetsassets:read

List assets

Every machine you may write to. Address assets by their code (TGV-CHP1) rather than by id — the code is what is written on the machine and in your other systems.

Example request
curl http://localhost:60872/api/v1/assets \
  -H "Authorization: Bearer $SOP_API_KEY"
Response
{
  "assets": [
    {
      "id": "eq-tgv-chp1",
      "code": "TGV-CHP1",
      "name": "CHP 1 V",
      "type": "Other",
      "department": "Toplana",
      "location": "Gornja Vežica",
      "status": "operational",
      "parentId": null
    }
  ]
}
GET/api/v1/metricsassets:read

List metric names

The metric names this installation understands, and the range each is judged against. Send one of these names and your readings are charted with a spec band and flagged when they fall outside it.

Example request
curl http://localhost:60872/api/v1/metrics \
  -H "Authorization: Bearer $SOP_API_KEY"
Response
{
  "metrics": [
    {
      "metric": "pressure",
      "label": "Discharge pressure",
      "unit": "bar",
      "min": 4,
      "max": 7,
      "hasLimits": true,
      "templates": [
        "pump-round"
      ],
      "conflicting": false
    }
  ]
}
POST/api/v1/readingsreadings:write

Submit readings and counters

Sensor values, in batches. Set kind to "counter" for totals that only ever rise, such as running hours or kWh — these are reported as a latest value and a daily rate rather than as a trend.

FieldTypeRequiredMeaning
assetstringyesAsset code or id, from GET /assets.
metricstringyesMetric name. Use one from GET /metrics to get limits.
valuenumberyesThe measured number.
recordedAtstringyesWhen the machine took the reading. ISO 8601.
unitstringoptionalUnit as you record it, e.g. "bar". Shown as-is.
kind"reading" | "counter"optionalDefaults to "reading". Use "counter" for cumulative totals.
idstringoptionalYour own id for this item. Resending the same id updates it instead of duplicating.
Example request
curl -X POST http://localhost:60872/api/v1/readings \
  -H "Authorization: Bearer $SOP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "readings": [
    {
      "asset": "TGV-CHP1",
      "metric": "pressure",
      "value": 6.4,
      "unit": "bar",
      "recordedAt": "2026-08-04T09:00:00Z"
    },
    {
      "asset": "TGV-CHP1",
      "metric": "pressure",
      "value": 6.9,
      "unit": "bar",
      "recordedAt": "2026-08-04T10:00:00Z"
    },
    {
      "asset": "TGV-CHP1",
      "metric": "running_hours",
      "value": 18412,
      "unit": "h",
      "kind": "counter",
      "recordedAt": "2026-08-04T10:00:00Z"
    }
  ]
}'
Response
{
  "accepted": 3,
  "unknownMetrics": [
    "running_hours"
  ]
}
POST/api/v1/eventsevents:write

Submit machine alarms

Faults and alarms raised by the control system rather than noticed by a person.

Send the alarm when it is raised, then send it again with the same "id" and an "endedAt" when it clears. The second call closes the first rather than creating a second alarm.
FieldTypeRequiredMeaning
assetstringyesAsset code or id, from GET /assets.
codestringyesThe alarm or fault code from the control system.
startedAtstringyesWhen the alarm was raised. ISO 8601.
severityinfo | warning | alarm | criticaloptionalDefaults to "warning".
messagestringoptionalHuman-readable description of the alarm.
endedAtstringoptionalWhen the alarm cleared. Leave out while it is still standing.
dataobjectoptionalAnything else you want kept with the event.
idstringoptionalYour own id for this item. Resending the same id updates it instead of duplicating.
Example request
curl -X POST http://localhost:60872/api/v1/events \
  -H "Authorization: Bearer $SOP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "events": [
    {
      "asset": "TGV-CHP1",
      "code": "E-4021",
      "severity": "alarm",
      "message": "Bearing temperature high",
      "startedAt": "2026-08-04T04:10:00Z",
      "id": "alarm-991"
    }
  ]
}'
Response
{
  "accepted": 1,
  "open": 1
}
POST/api/v1/service-recordsservice:write

Submit external maintenance

Work carried out by a contractor, or recorded in another maintenance system. Without this, an asset passport shows a machine as untouched when in fact it was serviced.

FieldTypeRequiredMeaning
assetstringyesAsset code or id, from GET /assets.
performedAtstringyesWhen the work was done. ISO 8601.
kindmaintenance | repair | inspection | calibration | installation | otheroptionalDefaults to "maintenance".
organisationstringoptionalThe firm that did the work.
performedBystringoptionalThe engineer who did it.
descriptionstringoptionalWhat was done.
costnumberoptionalWhat it cost.
currencystringoptionalThree-letter currency code, e.g. EUR.
downtimeHoursnumberoptionalHow long the machine was out of service for it.
idstringoptionalYour own id for this item. Resending the same id updates it instead of duplicating.
Example request
curl -X POST http://localhost:60872/api/v1/service-records \
  -H "Authorization: Bearer $SOP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "records": [
    {
      "asset": "TGV-CHP1",
      "performedAt": "2026-07-15T09:00:00Z",
      "kind": "repair",
      "organisation": "Tehnix d.o.o.",
      "performedBy": "M. Horvat",
      "description": "Replaced main bearing",
      "cost": 1450,
      "currency": "EUR",
      "downtimeHours": 6
    }
  ]
}'
Response
{
  "accepted": 1
}

Errors

400
Something in the batch is wrong. The response names the offending item by index and the field, and nothing in the batch was written.
401
The key is missing, malformed, unrecognised, revoked or expired. The response says which.
403
The key is valid but lacks the scope this endpoint needs. The response lists the scopes it does have.
413
The batch exceeds the item limit. Split it.
429
Too many requests for this key. Send fewer, larger batches.

Limits

  • Up to 1000 items per request.
  • 120 requests per 60 seconds, counted per key.
  • Timestamps are required, must be ISO 8601, and should be the time the machine took the reading — not the time you sent it.
Last changed 07 August 2026