Send device data over an HTTPS webhook
Webhook ingest lets a device (or any script or server) push telemetry to Synacl over plain HTTPS, instead of connecting to our MQTT broker. It's handy when a device already speaks HTTP, sits behind a firewall that only allows outbound HTTPS, or is driven by a cron job or cloud function you already run.
Data that arrives this way flows through the exact same pipeline as MQTT — live dashboards, stored history, threshold alert rules, actions, macros and egress all work the same.
Availability. Webhook ingest is a paid-plan feature enabled per account. If it isn't available on your plan, the endpoint returns HTTP 403 — see plans and limits.
The endpoint
POST https://api.synacl.com/ingest/webhook/<deviceId>
Content-Type: application/json
Use your device's own id (copy it from the device's detail page). Every request must be authenticated (below) and carry a JSON body.
Create a webhook device
The quickest path is to make webhook the device's protocol from the start:
- Go to Devices → Add Device and pick Webhook (HTTP push) as the protocol. No gateway or connection fields are needed.
- Define your tags — the keys you'll send in the JSON payload must match these names letter-for-letter.
- Save. You land on the device's page with the Webhook Ingest panel already live: secret minted, ingest enabled, endpoint URL and ready-to-paste
curlexamples on screen.
A webhook device's protocol is fixed at creation (as is
mqtt-direct's) — to move an existing device to webhook push, create a new webhook device instead.
Get your webhook secret
Each device has its own secret — a string that looks like whsk_1a2b3c…. Webhook-protocol devices get one automatically at creation. You can also enable webhook push on any existing device (say, an mqtt-direct device that should fall back to HTTPS):
- Open the device from Devices and find the Webhook Ingest panel on its detail page.
- Click Generate to create a secret (or Rotate to replace an existing one).
- Copy it somewhere safe — treat it like a password.
Rotating invalidates the old secret immediately. Any device still using the previous secret will start getting rejected, so update it everywhere before (or right after) you rotate.
Authenticate the request
Include one of these two headers — either one is accepted:
| Header | What to send |
|---|---|
Authorization: Bearer <secret> |
The per-device secret verbatim (whsk_…). Simplest option. |
X-Synacl-Signature: sha256=<hmac> |
An HMAC-SHA256 of the exact raw JSON body, keyed by the secret. Nothing secret travels on the wire. |
Use the signature style when you'd rather not send the raw secret with every request. If both headers are present, either one satisfies auth.
The body: two shapes
Flat — just the tag/value pairs. The server timestamps it on arrival:
{ "temperature": 22.5, "humidity": 60 }
In flat mode the keys ts, q and seq are reserved control keys — don't use them as tag names.
Native — wrap your readings in values, and set your own timestamp and quality:
{
"ts": 1720000000000,
"values": { "temperature": 22.5, "humidity": 60 },
"q": "good"
}
ts— epoch time in milliseconds.q— quality, one ofgood,bad,uncertain,stale.
Rules for the payload
- Tag names must match exactly. The keys you send (
temperature,humidity, …) must match the device's configured tag/parameter names letter-for-letter, or the readings won't line up with your dashboards, history or rules. See device parameters (tags). - Values must be a number, boolean, or string — nothing else.
Example: Bearer token
curl -X POST https://api.synacl.com/ingest/webhook/<deviceId> \
-H "Authorization: Bearer whsk_1a2b3c4d5e6f7890" \
-H "Content-Type: application/json" \
-d '{"temperature": 22.5, "humidity": 60}'
Native shape works the same way — just change the body:
curl -X POST https://api.synacl.com/ingest/webhook/<deviceId> \
-H "Authorization: Bearer whsk_1a2b3c4d5e6f7890" \
-H "Content-Type: application/json" \
-d '{"ts": 1720000000000, "values": {"temperature": 22.5}, "q": "good"}'
Example: HMAC signature
Sign the exact raw bytes you're about to send, then send those same bytes. If you re-format or re-serialize the JSON after signing, the hash won't match.
SECRET="whsk_1a2b3c4d5e6f7890"
BODY='{"temperature": 22.5, "humidity": 60}'
# HMAC-SHA256 of the raw body, keyed by the secret, as hex
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -X POST https://api.synacl.com/ingest/webhook/<deviceId> \
-H "X-Synacl-Signature: sha256=$SIG" \
-H "Content-Type: application/json" \
-d "$BODY"
This is the same signature scheme Synacl uses for outbound webhooks — see verifying webhook signatures for the details.
What to expect back
202 Acceptedmeans we received your data and queued it. Like MQTT, ingest is fire-and-forget — a202is not a guarantee every reading was stored, just that it was accepted for processing.400means the body wasn't valid JSON, or didn't match either accepted shape.401means the secret was wrong or missing.403means webhook ingest isn't enabled on your plan.404means no such device, or ingest is switched off for it.- Over-rate messages are silently dropped, exactly as they are over MQTT. Your plan has a shared per-second message budget across MQTT and HTTP combined, so heavy webhook traffic counts against the same limit. See understanding rate limits.
Once a reading lands, open the device or a dashboard bound to it — you should see the value appear live within a second or two, just as if it had come in over MQTT.
When something's wrong, check Events
Your device gets the HTTP error, but you might not be watching it. Rejected pushes also raise a warning on the Events page, so a broken integration is visible from inside Synacl:
| Event | What it means |
|---|---|
| Ingest Auth Failed | The secret was wrong or missing. Usually a rotation that never made it to the device. |
| Ingest Bad Payload | The body didn't match either accepted shape — often a nested object where a number belongs. |
| Ingest Unknown Tag | The push succeeded, but a key matched none of the device's tags. |
That last one is the quiet killer. A typo like voltag_battery instead of voltage_battery is still accepted with a 202 — the reading is simply filed under a name nothing is bound to, so your dashboard stays empty while the API keeps reporting success. The response body also names the offending keys:
{ "accepted": true, "tags": 1, "unknownTags": ["voltag_battery"] }
To keep the event log readable, each kind of failure is recorded at most once a minute per device — a device retrying in a loop produces one warning, not thousands.