Skip to main content

Direct Ingestion

Overview​

The Edge Agent is the easiest way to get machine data into Haltless, but it is not the only way. If you already have a data pipeline, a historian, or a script that produces readings, you can send them yourself. There are two options:

  1. Push readings over HTTP: your system calls POST /api/v1/ingest with an API key. Best when your data lives behind your own software.
  2. Let Haltless poll a source: you register an OPC-UA or Modbus source and Haltless reads from it on a schedule. Best when readings live on a network-reachable industrial endpoint and you'd rather not run software next to it.

This guide covers both. Either way, readings belong to a System: the connection they come from. A System owns every tag it sends, and each tag is assigned to a machine.

Option 1: Push readings with the ingest API​

The endpoint​

MethodPOST
URLhttps://api.haltless.io/api/v1/ingest
AuthX-API-Key header, with the ingest scope
Success207 Multi-Status

You'll need an API key. A key created with the default scopes can send readings. See Managing API Keys to create one.

Before you send: a System​

Readings are accepted only for a System Haltless knows. Create one in either of two ways:

  • Register a machine on its own (POST /api/v1/machines with a protocol and no system_id, or Machines → Add machine in the dashboard). Haltless creates a System with the same identifier and routes every tag it sends to that machine. This is the quickest start for one machine.
  • Create a System (POST /api/v1/systems, or the Systems page), then add machines to it and assign its tags to them. Use this when one connection, such as a PLC, serves several machines.

See Systems and Machines.

Request body​

Send a batch of readings under a readings array. Each reading is one metric sample from one System:

FieldTypeRequiredDescription
system_identifierstringYes¹The identifier of the System the reading comes from (1–255 chars, case-sensitive)
machine_identifierstringYes¹The same thing under its earlier name; still accepted
timestampstring (ISO 8601)YesWhen the sample was taken. At most 5 minutes in the future and 3,650 days in the past. A timestamp without a timezone is read as UTC
metric_namestringYesThe metric being reported, for example temperature (1–255 chars)
valuenumberYesA finite numeric value (not NaN or infinity)
unitstringYesThe unit the value is in, for example celsius, bar, or mm/s (1–50 chars)
raw_tagstringNoThe source tag or address the value came from (up to 500 chars)
idempotency_keystring (UUID)NoA stable UUID that identifies this reading for de-duplication (see below)

¹ Send one of the two, or both if they agree.

Each reading's tag is its raw_tag, or its metric_name when it has none. The tag decides which machine the reading belongs to:

  • A tag already assigned to a machine is stored on that machine and monitored.
  • A tag the System has not sent before is created unassigned, unless the System routes everything to its one machine (a machine registered on its own). Readings of an unassigned tag are stored but raise no alerts until someone assigns the tag on the System's page; assigning it brings those readings along.

A single request may carry up to 1,000 readings. For higher volume, send multiple batches; ingest is limited to 600 requests per minute from each network address.

Example request​

curl -X POST "https://api.haltless.io/api/v1/ingest" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"readings": [
{
"system_identifier": "PUMP-01",
"timestamp": "2026-08-05T10:30:00Z",
"metric_name": "temperature",
"value": 73.2,
"unit": "celsius"
},
{
"system_identifier": "PUMP-01",
"timestamp": "2026-08-05T10:30:00Z",
"metric_name": "vibration",
"value": 2.7,
"unit": "mm/s"
}
]
}'

Response​

The endpoint returns 207 Multi-Status: a batch can be partially accepted, so the response reports exactly what happened.

{
"accepted_count": 1,
"rejected_count": 1,
"errors": [
{
"index": 1,
"machine_identifier": "PUMP-99",
"error": "System identifier not found for this company: PUMP-99"
}
],
"unassigned_count": 0
}
FieldMeaning
accepted_countNumber of readings newly stored by this request
rejected_countNumber of readings that could not be stored
errorsOne entry per rejected reading, with its index in your batch, the System identifier it named (in the field machine_identifier), and a human-readable error
unassigned_countAccepted readings whose tag is not assigned to a machine yet: stored, but not monitored until the tag is assigned

A reading is rejected this way when its System identifier is not one of your company's active Systems. Check the identifier (it is case-sensitive), or create the System first.

A reading that fails validation is different: a non-finite value, a missing unit, or a timestamp outside the window above makes the whole request fail with 422, and nothing in it is stored. The error names the offending reading's position. Fix or drop it and send the batch again. See Errors.

What happens to accepted readings​

Every accepted reading is processed the same way, whether it arrives via the Edge Agent or this endpoint:

  • Units are normalized to your workspace's configured preferences before storage, for the units Haltless can convert (for example fahrenheit to celsius, psi to bar, in/s to mm/s). Other units are stored as sent.
  • Anomaly detection runs on readings of assigned tags, which can raise alerts.
  • Live subscribers are notified in real time; see WebSocket Integration.

Idempotency and retries​

Ingestion is idempotent, so retries are safe. If a request times out or you're unsure whether it landed, send it again: Haltless will not create duplicate readings.

  • By default, a reading is identified by its System, its tag, and its timestamp. Sending the same reading twice stores it once. Send the timestamp the same way each time (for example always in UTC with Z), since 10:30:00Z and 12:30:00+02:00 are treated as different readings.
  • accepted_count reflects only readings newly stored by that call. If you replay a batch that was already accepted, you'll get accepted_count: 0: confirmation that the data is already present, not an error.
  • For full control over de-duplication, set an idempotency_key (any valid UUID) on a reading. Replays carrying the same key and the same timestamp collapse to a single stored reading, whatever their other fields. Keys are scoped to your company. This is useful when the same logical sample might be produced more than once by your pipeline.
{
"system_identifier": "PUMP-01",
"timestamp": "2026-08-05T10:30:00Z",
"metric_name": "temperature",
"value": 73.2,
"unit": "celsius",
"idempotency_key": "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
}

Because side effects (anomaly detection, live updates) run only for readings actually stored, a replay will never re-fire alerts or duplicate live events.

Option 2: Configure a backend-polled source (OPC-UA / Modbus)​

If your readings live on a network-reachable OPC-UA server or Modbus TCP device, Haltless can poll it for you, with no software to deploy on your side. Sources are configured by an admin or operator whose access covers all machines, with a user (JWT) session, from the Direct Ingestion page of the Client Portal or the API.

note

The source host must be a publicly reachable address. Devices on a private or isolated plant network are not reachable this way; for those, run the Edge Agent inside the network instead.

Create a source​

A source feeds a System, the same way an Edge Agent does: its readings are routed to machines by tag.

curl -X POST "https://api.haltless.io/api/v1/ingest-sources" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"system_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"name": "Line 1 PLC",
"protocol": "opcua",
"host": "plc.example.com",
"port": 4840,
"poll_interval_seconds": 30,
"config": {
"nodes": [
{ "node_id": "ns=2;i=1001", "metric_name": "temperature", "unit": "celsius" }
]
}
}'
FieldTypeNotes
system_idUUIDThe System these readings belong to. Older clients may send machine_id instead; it is resolved to that machine's System
namestringA label for the source (1–200 chars)
protocolstringopcua or modbus (Modbus TCP). Note the spelling: a System's own protocol uses opc_ua and modbus_tcp
hoststringPublicly reachable hostname or address
portinteger1–65535
poll_interval_secondsintegerHow often to read, 5–3600 (default 30)
is_activebooleanWhether polling is enabled (default true)
configobjectWhat to read, and credentials where required (below)

Source config​

OPC-UA (protocol: "opcua"):

KeyDescription
nodesList of nodes to read, each with node_id (for example ns=2;i=1001), metric_name, unit, and an optional raw_tag
username, passwordOptional. Used only when both are set; otherwise Haltless connects anonymously

Modbus TCP (protocol: "modbus"):

KeyDescription
registersList of registers to read (below)

Each register:

KeyDefaultDescription
addressRegister address (0-based)
register_typeholdingholding or input
data_typeuint16uint16, int16, uint32, int32, or float32. The 32-bit types span two registers
word_orderbigFor the 32-bit types: big (high word first) or little (low word first)
metric_name, unitWhat the value is
scale, offset1.0, 0.0Applied as value × scale + offset
raw_tagOptional source tag name

An unknown data_type or word_order is refused with 422 when you save the source.

Credentials placed in config are stored encrypted and are always masked when a source is read back. When you update a source, sending the masked or a blank password keeps the stored one.

Test the connection​

Before relying on a source, verify Haltless can reach it:

curl -X POST "https://api.haltless.io/api/v1/ingest-sources/SOURCE_ID/test-connection" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
{ "success": true, "message": "OPC-UA connection successful" }

A failed test always returns the same message, whatever went wrong, so check the host, the port, and that the device accepts connections from Haltless.

Manage sources​

List, read, update, and delete sources with the standard REST verbs (GET, GET /{source_id}, PUT /{source_id}, DELETE /{source_id}):

# List
curl "https://api.haltless.io/api/v1/ingest-sources" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"

# Delete
curl -X DELETE "https://api.haltless.io/api/v1/ingest-sources/SOURCE_ID" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"

An update can change name, host, port, poll_interval_seconds, is_active, and config, but not the System or the protocol. When you read a source, the response also carries last_polled_at and last_error, so you can see whether recent polls succeeded.

Readings collected from a polled source flow through the same tag routing, normalization, anomaly detection, and real-time delivery described above.

Next steps​