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:
- Push readings over HTTP , your system calls
POST /api/v1/ingestwith an API key. Best when your data lives behind your own software. - 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
| Method | POST |
| URL | https://api.haltless.io/api/v1/ingest |
| Auth | X-API-Key header (key needs the ingest scope) |
| Success | 207 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:
| Field | Type | Required | Description |
|---|---|---|---|
machine_identifier | string | Yes | Your identifier for the machine, as registered in Haltless |
timestamp | string (ISO 8601) | Yes | When the sample was taken. May be at most 5 minutes in the future |
metric_name | string | Yes | The metric being reported, for example temperature |
value | number | Yes | A finite numeric value (not NaN or infinity) |
unit | string | Yes | The unit the value is in, for example celsius |
raw_tag | string | No | The source tag/address the value came from, for your own traceability |
idempotency_key | string (UUID) | No | A 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"
}
]
}
| Field | Meaning |
|---|---|
accepted_count | Number of readings newly stored by this request |
rejected_count | Number of readings that could not be stored |
errors | One 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_countreflects only readings newly stored by that call. If you replay a batch that was already accepted, you'll getaccepted_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.
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": {}
}'
| Field | Type | Notes |
|---|---|---|
machine_id | UUID | The machine these readings belong to |
name | string | A label for the source |
protocol | string | opcua or modbus |
host | string | Publicly reachable hostname or address |
port | integer | 1–65535 |
poll_interval_seconds | integer | How often to read, 5–3600 (default 30) |
is_active | boolean | Whether polling is enabled (default true) |
config | object | Protocol-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
- Managing API Keys , create a key with the
ingestscope - Edge Agent overview , collect data from a private network
- Sensor Data API , query the readings you've ingested
- WebSocket Integration , receive readings and alerts live