Skip to content

Monitoring Webhooks

There are a few ways to monitor webhooks, in addition to the record of webhooks on your webhook server.

The first is within Nuki Web. In the “API” menu, head to “Advanced API”, where you will see a table of the last 300 webhook requests. Here you can see the status code that we received from your webhook server, as well as the payload of the webhook.

Webhook logs in Nuki Web

Additionally, we offer a dedicated endpoint for fetching these webhook logs. Once again, only the last 300 are stored. You can fetch those logs with a GET request to a dedicated webhook logs endpoint.

The following optional parameters can be used with this endpoint:

ParameterDescription
idThe webhook ID you wish to filter for. This is a hexidecimal representation of an Object ID
limitThe number of webhook log entries that should be returned (Default: 50)

Here is an example response from this endpoint:

[
{
"id": "68c555..........6bcace0c",
"succeeded": true,
"responseStatus": 200,
"duration": 466,
"accountId": 987654321,
"response": {
"headers": {
"X-Nuki-Signature-SHA256": "d89d6833806449..............................b9d5e7d0682c32c20fcd",
"X-Nuki-Signature": "842c71c8f....................5a563b66136",
"Content-Type": "application/json; charset=UTF-8"
},
"body": {
"feature": "DEVICE_MASTERDATA",
"deleted": false,
"smartlockId": 123456789,
"accountId": 987654321,
"type": 4,
"authId": 1212121212,
"name": "Hello",
"favorite": false,
"firmwareVersion": 199175,
"hardwareVersion": 1291,
"serverState": 0,
"adminPinState": 0,
"creationDate": "2025-01-16T12:20:23.541Z",
"updateDate": "2025-09-13T11:27:37.772Z"
},
"timestamp": "2025-09-13T11:27:39.298Z",
"path": "https://andrew-nuki.free.beeceptor.com"
},
"apiKeyId": 1010101010,
"updated": "2025-09-13T11:27:39.764Z",
"created": "2025-09-13T11:27:39.000Z"
},
// ...
]

Every webhook we send is signed with a secret that only you and Nuki know. By checking this signature, you can be sure that a webhook really came from Nuki, and that its content was not changed on the way. Anyone who knows your webhook URL can send requests to it, so we recommend verifying every webhook before you act on it.

The idea is simple: we run the webhook body and your secret through a fixed calculation, and send the result along with the webhook. You run the same calculation on the body you received, using your copy of the secret. If both results are the same, the webhook is genuine. Without the secret, nobody can produce a matching result, and changing even a single character of the body produces a completely different one.

Each webhook contains two signature headers. Both are calculated from the same body with the same secret, and only differ in the hash algorithm:

HeaderAlgorithmDescription
X-Nuki-Signature-SHA256HMAC-SHA256 (RFC 2104)64 hexadecimal characters. We recommend using this header
X-Nuki-SignatureHMAC-SHA1 (RFC 2104)40 hexadecimal characters. Provided for compatibility with existing integrations

You only need to check one of them. The signature is calculated as follows:

DataThe raw request body, exactly as we sent it (JSON, UTF-8 encoded)
KeyYour secret, used as plain text (UTF-8). Even if your secret looks like hexadecimal or Base64, do not decode it
ResultThe HMAC in lowercase hexadecimal, without any prefix (such as sha256=)

Which secret is used depends on where the webhook is sent:

WebhookSecret
Central webhooksThe Client Secret of your Advanced API credentials, which you will find in Nuki Web
Decentral webhooksThe secret from the response when you registered the decentral webhook
  1. Read the raw request body before you parse it as JSON.

  2. Calculate the HMAC-SHA256 of the raw body, using your secret as the key.

  3. Convert the result to lowercase hexadecimal.

  4. Compare your result with the value of the X-Nuki-Signature-SHA256 header, using a constant-time comparison. A normal string comparison (==) can reveal through tiny timing differences how many characters matched. Every language offers a safe comparison function, and the examples below use it.

  5. If the values match, the webhook is genuine and you can process it. If the header is missing or the values differ, reject the request (for example with a 401 response) and do not process it.

Each example reads the secret from the environment variable NUKI_WEBHOOK_SECRET. To check X-Nuki-Signature instead, replace SHA-256 with SHA-1.

This example uses Express. express.raw() keeps the body exactly as it was received.

import crypto from 'node:crypto';
import express from 'express';
const NUKI_SECRET = process.env.NUKI_WEBHOOK_SECRET;
function isValidNukiSignature(rawBody, signature) {
if (typeof signature !== 'string') {
return false;
}
const expected = Buffer.from(crypto.createHmac('sha256', NUKI_SECRET).update(rawBody).digest('hex'));
const received = Buffer.from(signature.trim().toLowerCase());
return expected.length === received.length && crypto.timingSafeEqual(expected, received);
}
const app = express();
app.post('/nuki-webhook', express.raw({ type: 'application/json' }), (req, res) => {
if (!isValidNukiSignature(req.body, req.get('X-Nuki-Signature-SHA256'))) {
return res.sendStatus(401);
}
const webhook = JSON.parse(req.body.toString('utf8'));
// ... process the webhook
res.sendStatus(204);
});
app.listen(3000);

Use these values to check that your code calculates signatures correctly:

SecretmySecret123
Body{"hello":"world"}
X-Nuki-Signature-SHA256df46b477700fde714a53eca6cd37bc5480d0e0ee95037e4970da95e8bdfc4cfa
X-Nuki-Signature9ed1a5b4465ffc9c238a03096b2f49fdbcd3527a

You can reproduce these values on the command line with OpenSSL:

Terminal window
printf '%s' '{"hello":"world"}' | openssl dgst -sha256 -hmac 'mySecret123'

This also shows why the raw body matters. With a single extra space, the body {"hello": "world"} contains the same JSON data, but its signature is completely different: 090986868e32d84ec5cd78c9a4e2f20550363ccb07d0c819f6ed2737ef27eca2.

When you activate webhooks for your Advanced API, we send a test webhook with the body {} to your webhook URL. It is signed in the same way as every other webhook, so a correct implementation accepts it.