Gareth Wilson Gareth Wilson

Guide to Sigfox Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationSigfox backend: Device type > Callbacks > New > Custom Callback
ChannelsURL (one request per message), BATCH_URL (one request per second), EMAIL (for testing only)
MethodsGET, POST or PUT for simple callbacks. Batch callbacks are POST only
Body formatsapplication/x-www-form-urlencoded (default), application/json (validated), text/plain
PayloadWhatever your URL pattern, headers and body template contain
AuthenticationCustom headers, including Authorization, or Basic credentials in the URL. No signature
Forbidden headersEvery standard header except Authorization. Header names can't contain variables
Data Advanced timingDelivered about 30 seconds after the message
Retries and timeoutNot documented. Failures appear in the device type's callback error log
Source IPsNot published
TLSSNI 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 typeFires whenNotable variables
Data > UplinkA message is received from a device{time}, {device}, {deviceTypeId}, {data}, {seqNumber}
Data > BidirA received message requests a downlinkThe uplink variables plus {ack} and {longPolling}
Service > Data AdvancedMessage details are ready, about 30 seconds later{lqi}, {linkQuality}, {duplicates}, {computedLocation}
Service > Status, Acknowledge, RepeaterDevice status, downlink acknowledgement or repeater reportsVaries by type
ErrorA 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

  1. Generate a long random value, for example with openssl rand -hex 32, and choose a header name such as X-Sigfox-Token. You can also use Authorization, the only standard header Sigfox allows.
  2. In the Sigfox backend, open Device type, select the device type, click Callbacks, then New and Custom Callback.
  3. Choose the callback type and set the channel to URL, or BATCH_URL for batches.
  4. Enter the URL pattern. For a GET callback, put the variables in the query string, for example ?device={device}&time={time}&data={data}.
  5. Choose the method. For POST or PUT, choose a content type and write the body template.
  6. Under Headers, add your header name and value.
  7. 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 batch field 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.