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.

Option 1: Push readings with the ingest API

The endpoint

MethodPOST
URLhttps://api.haltless.io/api/v1/ingest
AuthX-API-Key header (key needs the ingest scope)
Success207 Multi-Status

You'll need an API key with the ingest scope. See Managing API Keys to create one.

Request body

Send a batch of readings under a readings array. Each reading describes one metric sample from one machine:

FieldTypeRequiredDescription
machine_identifierstringYesYour identifier for the machine, as registered in Haltless
timestampstring (ISO 8601)YesWhen the sample was taken. May be at most 5 minutes in the future
metric_namestringYesThe metric being reported, for example temperature
valuenumberYesA finite numeric value (not NaN or infinity)
unitstringYesThe unit the value is in, for example celsius
raw_tagstringNoThe source tag/address the value came from, for your own traceability
idempotency_keystring (UUID)NoA stable UUID that identifies this reading for de-duplication (see below)

A single request may carry up to 1,000 readings. For higher volume, send multiple batches , the endpoint accepts up to 240 requests per minute.

Example request

curl -X POST "https://api.haltless.io/api/v1/ingest" \
-H "X-API-Key: hlts_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0" \
-H "Content-Type: application/json" \
-d '{
"readings": [
{
"machine_identifier": "PUMP-01",
"timestamp": "2026-08-05T10:30:00Z",
"metric_name": "temperature",
"value": 73.2,
"unit": "celsius"
},
{
"machine_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": "Unknown machine identifier: PUMP-99"
}
]
}
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, its machine_identifier, and a human-readable error

The most common rejection reason is a machine_identifier that isn't registered in your workspace. Register machines first (see the Machines API), then ingest their readings.

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, so mixed-unit inputs stay consistent.
  • Anomaly detection runs on the new data, 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.

  • A reading is de-duplicated by its natural identity (machine, metric, and timestamp). Sending the same reading twice stores it once.
  • 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 collapse to a single stored reading. This is useful when the same logical sample might be produced more than once by your pipeline.
{
"machine_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 device, Haltless can poll it for you , no software to deploy on your side. Sources are configured by an admin or operator with a user (JWT) session, from the Client Portal (Settings → Direct Ingestion) 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

curl -X POST "https://api.haltless.io/api/v1/ingest-sources" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"machine_id": "9c8b7a6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d",
"name": "Line 1 PLC",
"protocol": "opcua",
"host": "plc.example.com",
"port": 4840,
"poll_interval_seconds": 30,
"config": {}
}'
FieldTypeNotes
machine_idUUIDThe machine these readings belong to
namestringA label for the source
protocolstringopcua or modbus
hoststringPublicly reachable hostname or address
portinteger165535
poll_interval_secondsintegerHow often to read, 53600 (default 30)
is_activebooleanWhether polling is enabled (default true)
configobjectProtocol-specific settings (tags/registers, and credentials where required)

Credentials placed in config are stored securely and are always masked when a source is read back.

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": "Connected successfully" }

Manage sources

List, read, update, and delete sources with the standard REST verbs:

# 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"

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 normalization, anomaly detection, and real-time delivery described above.

Next steps