All chapters

Sending data

If a device can make a web request, publish an MQTT message or send an e-mail, it can report to Huntress with no driver, SDK or custom firmware. Every door below leads to the same ingestion service, so a reading is a reading whichever way it arrived.

Overview#

Protocols
ProtocolPortIdentifies the sender byCarries
HTTP (the Hunt API)Your web port, normally 443 behind a proxyA token in the URLReadings, person events, pictures, alarm-panel messages
MQTT1883, plain TCPThe token as the MQTT user nameReadings, pictures, output state and acknowledgements
SMTP25, plain TCPThe token as the recipient addressReadings and pictures sent as mail
SIA DC-092000, plain TCPThe panel's account code, optionally with an AES keyAlarm-panel events
Contact ID2001, plain TCPThe panel's account codeAlarm-panel events
ZigbeeA USB or serial coordinatorPairingSensors and outputs, see Devices
Edge linksYour web port (WebSocket)A token and an edge secretCamera video and pictures, Zigbee, HTTP relay calls
ChoosingHTTP for anything with a network stack, MQTT for firmware that already speaks it, SMTP for cameras and recorders that can only send mail, SIA or Contact ID for alarm panels.

The Hunt API#

The Hunt API is the HTTP door. Its one endpoint accepts whatever a device can easily produce.

Authentication#

There are no headers to set. The token (a GUID) in the URL identifies and authorises the sender: it is on the sensor's, person's or panel's settings page. Possession of the token is enough to report, so keep it secret; if one leaks, replace the sensor and delete the old one. Tokens are shortened to eight characters in logs, and Cookie, Authorization and proxy headers are never stored with a request.

POST /hunt/{token}#

The body is read according to its Content-Type:

Content types
Content-TypeWhat Huntress does with it
application/jsonAll fields below. The most flexible form. JSON field names are case-sensitive.
application/x-www-form-urlencodedThe fields value, eventCode, unit, eventType, latitude, longitude and deviceInfo (names are case-insensitive). No imageData.
text/plainA bare number (23.4) is a reading. A single word of letters, up to 32 characters (Keepalive), is a person event.
image/jpeg, image/png, image/webpThe body is a picture for a camera sensor.
application/x-sia-dc03, application/x-contact-idAn alarm-panel message; the token is the panel's token. See Alarm panels.
No content typeDetected: JSON if the body starts with {, otherwise text, otherwise a JPEG or PNG.
Anything elseStored as an unknown event with the body kept as base64 (nothing is lost, nothing is interpreted).
A trap with plain textA text body that is a single word with no digits, for example ON or alarm, is read as a person event, not a reading. Posted to a sensor token it answers 404 “Person not found”. Send readings as JSON, {"value": 1}, or as a number.

JSON fields#

What the body holds decides what it is, in this order: value makes it a sensor reading; otherwise eventType makes it a person event; otherwise imageData alone makes it a camera picture; otherwise it is stored as an unknown event.

JSON fields
FieldTypeMeaning
valuenumberThe reading. Must be a JSON number (not "1") and finite. 1 and 0 for on/off sensors.
eventCodestring, up to 64Names the event this reading means, from the sensor type's list. An invalid code is ignored and the type's own classification is used.
unitstring, up to 32A unit for this reading only, such as °F. A reading in a different unit than the sensor's is not compared with its limits.
imageDatastringA base64 JPEG or PNG, up to 10 MiB decoded. With value it attaches a snapshot to the reading; alone it is a camera picture. On a person event it is accepted but not stored.
eventTypestring, up to 64Movement, PanicAlarm or Keepalive (any case). The person must have Pulse enabled.
latitude, longitudenumberA position, −90 to 90 and −180 to 180. Used with eventType.
deviceInfostring, up to 1024A description of the device (model, app version). Used with eventType; shown on the person's Pulse tab.

A temperature reading

curl -X POST https://<host>/hunt/<sensor-token> \
  -H "Content-Type: application/json" \
  -d '{"value": 21.5, "unit": "°C"}'

Pictures#

Huntress accepts a picture as a raw body (image/*), as imageData, as an MQTT payload or as an e-mail attachment. Pictures larger than 10 MiB or 24 megapixels are refused; the rest are scaled to at most 1920 pixels wide and stored as JPEG. A picture sent to a sensor that is not a camera answers 400 “Not a visual sensor”, and a numeric value sent to a camera answers 400 “This is a visual sensor”. A picture attached to a reading that is too large is dropped, and the reading is stored without it.

A camera snapshot

curl -X POST https://<host>/hunt/<camera-token> \
  -H "Content-Type: image/jpeg" \
  --data-binary @snapshot.jpg

Person events#

A person reports with the Pulse key in place of a sensor token. The Pulse app does this for you; your own app or tracker can too.

Person event types
eventTypeMeaningSeverity
MovementThe person moved; carries a position.Info
KeepaliveA heartbeat: the person and the device are fine.Info
PanicAlarmThe person raised an alarm.Critical

A panic alarm with a position

curl -X POST https://<host>/hunt/<pulse-key> \
  -H "Content-Type: application/json" \
  -d '{"eventType": "PanicAlarm", "latitude": 59.3293, "longitude": 18.0686}'

Answers#

A stored request answers 200 with JSON that echoes what was stored. A problem answers with {"error": "…"}.

Responses
StatusBodyMeaning
200{sensorId, sensorName, value, timestamp}A reading was stored.
200{sensorId, sensorName, width, height, timestamp}A picture was stored.
200{personId, personName, eventType, latitude, longitude, timestamp}A person event was stored.
200{accepted: true, timestamp}An unclassified or raw payload was stored as an unknown event.
400{error}Invalid body or field, a value for a camera, a picture for a non-camera, a picture too large.
404{error}Unknown token, or a person event for a sensor token.
429{error}Rate limit exceeded; see below.

Retries and rate limits#

  • Idempotency. Send an Idempotency-Key header on POST /hunt/{token} and a repeated request with the same key and token within 60 seconds is accepted without storing it again, answering {"accepted": true, "timestamp": …}. A key is remembered only after a successful request, so a failed one can be retried. Not available on /raw, MQTT or e-mail.
  • Rate limit. 300 requests per second per organisation, shared by all its tokens (a one-second sliding window); beyond that, 429. Requests with an unknown token are limited to 10 per second per client address. The limit covers the HTTP API only.

GET /hunt/meta/event-codes/{token}#

Returns the event codes a sensor accepts in eventCode. It works for sensor tokens only and is not rate limited.

{
  "sensorType": "DoorContact",
  "validEventCodes": ["BurglaryAlarm", "BurglaryVerified", "TamperAlarm", "MotionDetected",
                      "VibrationDetected", "GlassBreak", "InfraredTrip", "FenceDisturbance"]
}

POST /hunt/{token}/raw#

Stores the body exactly as received as an unknown event, with no parsing or classification, for a sensor or person token. It is for debugging a new integration or for sources that manage their own payload format.

MQTT#

Huntress contains an MQTT broker on port 1883 (HuntressMqtt:Enabled, HuntressMqtt:Port). It is built for devices to report to Huntress and to receive commands from it; it is not a general-purpose broker.

  • Connecting. The MQTT user name is the sensor or person token. The password is not checked. Client id huntress is reserved. A connection with an unknown token is refused.
  • Publishing. Any topic is accepted; the topic is kept only as a note of where the message came from. The payload is read exactly like an HTTP body: JSON, a bare number, form fields or a raw JPEG. With MQTT 5 the content type is taken from the message; with 3.1.1 it is detected. eventCode and unit are honoured.
  • Subscribing. A connection may subscribe only to the command topics of its own output and of the other outputs of the same device (see Devices). Wildcards and $ topics are refused, and nothing a client publishes is forwarded to another client.
  • Limits. 2,000 connections, 10 MB per message (larger closes the connection), a 30-second communication timeout and 100 queued messages per client.

Publishing a reading

mosquitto_pub -h <host> -p 1883 -u <token> -P x -t door -m '{"value": 1}'
Plain TCPThe MQTT listener has no TLS. Use it on a trusted network or through a VPN, or turn it off with HuntressMqtt:Enabled=false if you do not use it.

SMTP#

Huntress contains an SMTP receiver on port 25 (HuntressSmtp:Enabled, HuntressSmtp:Port). It is for cameras, recorders and appliances that can report only by e-mail.

  • Addressing. The local part of the first recipient is the token: <token>@anything. The domain is ignored. A recipient that is not a token is accepted and dropped without a bounce.
  • Body. Plain text (a bare number is a reading), JSON, or a picture. For multipart mail the first image/* attachment is taken and the rest are ignored.
  • Limits. Messages up to 10 MB (HuntressSmtp:MaxMessageBytes); larger ones are rejected. There is no sender authentication and no STARTTLS. eventCode and unit are not read from mail.
  • Retries. If Huntress cannot process a message it answers a temporary failure, so the sending server retries.

On Linux, binding port 25 needs the CAP_NET_BIND_SERVICE capability, which the supplied service unit grants. A listener that cannot start is reported on /health and does not stop the server.

Securing the plain doors#

Only the HTTP door gets TLS from your reverse proxy. MQTT, SMTP, SIA DC-09 and Contact ID are plain TCP; there is no TLS code in those listeners.

  • Turn off what you do not use: HuntressMqtt:Enabled, HuntressSmtp:Enabled, HuntressTcp:Enabled.
  • Expose the others only to the networks that need them: firewall rules or a VPN. A device's token or account code is its only credential.
  • Prefer HTTPS for anything that travels over the internet. Devices that cannot do TLS can post to a gateway or a Huntress Edge on their own network.
Connection lost
Attempting to reconnect…