Guide to Sigfox Webhooks: Features and Best Practices
Sigfox webhooks, which Sigfox calls callbacks, are how messages from your devices reach your own systems. A sensor transmits a few bytes over the Sigfox network, the Sigfox backend receives them, and a callback forwards them to your server as an HTTP request. If you run a fleet on Sigfox, the low-power IoT network now operated by UnaBiz, callbacks are the main way device data leaves the Sigfox backend.
This guide covers how custom callbacks are built, how to authenticate them with a secret header, how the GET, POST, PUT and batch variants differ, what Sigfox doesn't document about delivery, and the best practices for production.
What are Sigfox webhooks?
A Sigfox custom callback belongs to a device type. You create it in the Sigfox backend and decide almost everything about the request it sends: the URL, the HTTP method, the headers and the body. Sigfox fills in {variables} such as {device}, {data} and {time} from each message.
That makes Sigfox different from most webhook providers. There is no fixed payload and no event name in the request. If you write a body template of {"device":"{device}","data":"{data}"}, that is exactly what arrives. Which callback fired, whether it's an uplink, a service callback or an error, is decided by which callback you created, not by anything Sigfox adds to the request. Give each callback its own URL path so your receiver can tell them apart.
Sigfox does not sign callbacks. You authenticate them with a header you add yourself.
Sigfox webhook features
| Feature | Details |
|---|---|
| Configuration | Sigfox backend: Device type > Callbacks > New > Custom Callback |
| Channels | URL (one request per message), BATCH_URL (one request per second), EMAIL (for testing only) |
| Methods | GET, POST or PUT for simple callbacks. Batch callbacks are POST only |
| Body formats | application/x-www-form-urlencoded (default), application/json (validated), text/plain |
| Payload | Whatever your URL pattern, headers and body template contain |
| Authentication | Custom headers, including Authorization, or Basic credentials in the URL. No signature |
| Forbidden headers | Every standard header except Authorization. Header names can't contain variables |
| Data Advanced timing | Delivered about 30 seconds after the message |
| Retries and timeout | Not documented. Failures appear in the device type's callback error log |
| Source IPs | Not published |
| TLS | SNI is enabled by default and Sigfox recommends leaving it on |
Common events
Because the request has no event field, the useful list is of callback types. Each type exposes its own variables.
| Callback type | Fires when | Notable variables |
|---|---|---|
| Data > Uplink | A message is received from a device | {time}, {device}, {deviceTypeId}, {data}, {seqNumber} |
| Data > Bidir | A received message requests a downlink | The uplink variables plus {ack} and {longPolling} |
| Service > Data Advanced | Message details are ready, about 30 seconds later | {lqi}, {linkQuality}, {duplicates}, {computedLocation} |
| Service > Status, Acknowledge, Repeater | Device status, downlink acknowledgement or repeater reports | Varies by type |
| Error | A device breaches the keep-alive delay set on its device type | {time}, {device}, {info}, {severity} |
{time} is an epoch timestamp in seconds, and {device} and {data} are hexadecimal strings. Some variables, such as {rssi} and {duplicates}, are only filled in for devices with the Network Metadata contract option, and {computedLocation} needs the Atlas service.
A typical JSON uplink template looks like this:
{
"device": "{device}",
"deviceTypeId": "{deviceTypeId}",
"time": {time},
"data": "{data}",
"seqNumber": {seqNumber}
}
Batch callbacks look quite different. Sigfox gathers the messages for a callback over one second and sends a single form-encoded POST with a batch field, one message per line:
batch={device};{data};{signal}
{device};{data};{signal}
{device};{data};{signal}
You choose the line format with the callback's line pattern.
Setting up Sigfox webhooks
- Generate a long random value, for example with
openssl rand -hex 32, and choose a header name such asX-Sigfox-Token. You can also useAuthorization, the only standard header Sigfox allows. - In the Sigfox backend, open Device type, select the device type, click Callbacks, then New and Custom Callback.
- Choose the callback type and set the channel to
URL, orBATCH_URLfor batches. - Enter the URL pattern. For a
GETcallback, put the variables in the query string, for example?device={device}&time={time}&data={data}. - Choose the method. For
POSTorPUT, choose a content type and write the body template. - Under Headers, add your header name and value.
- Leave SNI enabled and save.
Sigfox also accepts Basic credentials in the URL, as https://login:password@yourdomain.com/.... Sigfox warns against combining that with an Authorization header. If you use it, the request arrives with an Authorization: Basic ... header.
Two details in the docs are easy to miss. With a form-encoded POST, any variables you put in the URL's query string are also appended to the body. With application/json, Sigfox validates the template and rejects one that isn't valid JSON, so quote string variables and leave numeric ones such as {time} bare.
In Hookdeck. Create a source with the Sigfox type, enter the header name in API Key Header and the exact value in API Key, and use the source URL in the callback's URL pattern. Hookdeck matches the header name case-insensitively and the value exactly. The source type pre-selects POST, so if your callback uses GET or PUT, allow that method on the source too. If you used URL credentials instead of a header, enter Authorization as the header name and the full Basic <base64 of login:password> string as the value.
Securing Sigfox webhooks
Compare the header you configured with your stored value in constant time, and reject the request if it's missing or different. Your receiver should accept the methods you configured, and handle an empty body on GET.
const crypto = require('crypto');
const express = require('express');
function safeEqual(received, expected) {
const a = Buffer.from(received, 'utf8');
const b = Buffer.from(expected, 'utf8');
// timingSafeEqual throws on buffers of different lengths
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
function isFromSigfox(req) {
const expected = process.env.SIGFOX_WEBHOOK_SECRET;
if (!expected) return false; // fail closed when unconfigured
const received = req.get(process.env.SIGFOX_WEBHOOK_HEADER || 'X-Sigfox-Token');
return typeof received === 'string' && safeEqual(received, expected);
}
const app = express();
// One path per callback type, because the request doesn't say which type sent it.
app.all('/webhooks/sigfox/uplink', express.raw({ type: '*/*' }), (req, res) => {
if (!['GET', 'POST', 'PUT'].includes(req.method)) return res.sendStatus(405);
if (!isFromSigfox(req)) return res.sendStatus(401);
const message =
req.method === 'GET'
? req.query
: JSON.parse(req.body.toString('utf8')); // matches a JSON body template
// Deduplicate on message.device + message.seqNumber, then process async.
res.sendStatus(200);
});
The same check in Python:
import hmac
import os
def is_from_sigfox(headers) -> bool:
expected = os.environ.get("SIGFOX_WEBHOOK_SECRET")
if not expected:
return False # fail closed
header_name = os.environ.get("SIGFOX_WEBHOOK_HEADER", "X-Sigfox-Token")
received = headers.get(header_name) # Starlette and Flask headers are case-insensitive
if not received:
return False
return hmac.compare_digest(received.encode("utf-8"), expected.encode("utf-8"))
A matching header proves the sender knows the value. It doesn't prove the body is intact, and a captured request still passes if it's sent again later. Use an HTTPS callback URL so the header can't be read in transit, keep the value long and random, and change it in the callback and in your configuration if it may have leaked. Headers can also carry variables as values, but never put the secret in the URL's query string, where it ends up in access logs.
Make Sigfox callbacks production-ready. Hookdeck Event Gateway checks the secret header on every callback, stores each message durably, and lets you replay anything your service failed to process.
Sigfox webhook limitations and pain points
No signature and no fixed payload
The Problem: Nothing in a Sigfox request is cryptographically tied to Sigfox, and nothing in it identifies the callback type unless you put it there.
Why It Happens: Callbacks are templates. Sigfox substitutes variables and sends the result with the headers you set. It computes nothing over the body.
Workarounds:
- Add a long random secret header to every callback and check it on every request.
- Use a separate URL path for each callback type, or add a constant field to the body template.
- Deduplicate on
{device}and{seqNumber}, so a replayed message is ignored.
How Hookdeck Can Help: Hookdeck's Sigfox source type compares the header you name with the exact value you enter. Hookdeck answers Sigfox with a 200 either way, then records a request without the right header as failed verification, so it never reaches your destination.
Delivery behaviour is undocumented
The Problem: You don't know whether Sigfox retries a failed callback, how long it waits for your server, or when it gives up.
Why It Happens: Sigfox's callback pages don't describe retries or a timeout. When callbacks fail, the evidence is the Callback error log on the device type page, where HTTP status codes, and Sigfox's own 6xx codes, are listed.
Workarounds:
- Assume a failed callback may not be sent again. Keep your endpoint highly available and acknowledge quickly.
- Check the callback error log after any incident, and alert when expected device messages stop arriving.
How Hookdeck Can Help: Hookdeck accepts each callback as soon as it arrives and stores it, then retries delivery to your service on a schedule you control. Your own downtime doesn't depend on Sigfox's undocumented behaviour, and you can replay any message once a fix is deployed.
Bidir callbacks need your server's answer
The Problem: A device that asked for a downlink never gets one.
Why It Happens: A Data > Bidir callback expects the HTTP response to carry the downlink, as JSON keyed by device ID, for example {"1234": {"downlinkData": "1234567890123456"}}, or a 204 for no answer. Any proxy that answers on your behalf, including a queue in front of your server, replaces that response with its own.
Workarounds:
- Point Bidir callbacks directly at the server that computes the downlink, and keep Uplink and Service callbacks on your queued path.
- Answer Bidir callbacks quickly. Compute downlink data ahead of time where you can.
How Hookdeck Can Help: Hookdeck answers each request itself and delivers it to your service asynchronously, so use it for one-way callbacks such as Uplink, Data Advanced and Error, and leave Bidir callbacks on a direct URL.
Batches and duplicates
The Problem: One request can carry many messages, and the same message can show up in more than one batch.
Why It Happens: BATCH_URL callbacks gather a second's worth of messages into one form-encoded body. With the duplicate option, which needs the Network Metadata contract option, Sigfox says there's no guarantee that duplicates of a message land in the same batch.
Workarounds:
- Split the
batchfield on newlines and handle each line as a message. - Deduplicate across batches on device ID and sequence number, not just within one request.
How Hookdeck Can Help: Hookdeck stores every batch with its full body, so you can replay one against your parser after fixing a bug, and its transformations can reshape a batch before it reaches your service.
Best practices
Use one URL per callback type
The request doesn't say which callback sent it. Separate paths such as /sigfox/uplink and /sigfox/error keep the routing out of your parsing code.
Send JSON when you can
A JSON body template is validated by Sigfox when you save it, and it's easier to parse than form encoding. Quote hex strings such as {device} and {data}, and leave numbers such as {time} and {seqNumber} bare.
Deduplicate on device and sequence number
{device} plus {seqNumber} identifies a message. Store it before processing and skip repeats. See our guide to webhook idempotency.
Acknowledge fast and process asynchronously
Decode payloads, write to your time-series store and trigger alerts after you've returned 200. See why to process webhooks asynchronously.
Watch for silence
IoT devices fail quietly. Alert when a device type's message rate drops, and check the callback error log, since a broken callback looks the same as a fleet that has stopped transmitting.
Conclusion
Sigfox callbacks send exactly what you template, using the method you choose, with a header you add as the only credential. Compare that header in constant time over HTTPS, give each callback type its own path, deduplicate on device and sequence number, and keep Bidir callbacks on a direct URL so your downlink answer reaches the device.
Hookdeck Event Gateway sits in front of your receiver for one-way callbacks, checks the secret header, and stores every message until your service has processed it, so an outage on your side doesn't lose device data.
Get started with Hookdeck for free and handle Sigfox callbacks reliably in minutes.