HomeGuidesWebhook Signature Verification: Securing Your SMLLR Endpoint

Webhook Signature Verification: Securing Your SMLLR Endpoint

How to verify that a webhook request actually came from SMLLR — the exact header format, the HMAC-SHA256 formula, and a working Node.js code sample for a generic endpoint.

The Short Answer

Every generic SMLLR webhook delivery includes an X-Smllr-Signature header formatted as sha256=, alongside X-Smllr-Timestamp and X-Smllr-Event headers. The HMAC is computed as HMAC-SHA256(secret, "${timestamp}.${body}"), using the per-endpoint secret shown once when you created the endpoint in Settings → Webhooks & Slack. Recompute that same HMAC on your server using the raw request body and the timestamp header, then compare it to the signature you received — if they match, the request came from SMLLR and hasn't been tampered with in transit. Slack endpoints skip this entirely: a Slack Incoming Webhook URL is itself the secret, so there's no signature header on Slack deliveries.

Why Verify at All

A webhook endpoint is a public URL — anyone who guesses or discovers it can POST a fabricated payload to it, and without verification your server has no way to tell a real SMLLR event from a forged one. For most use cases the stakes are moderate (a fake scan.created event just pollutes analytics), but for anything that triggers a real-world action — restocking inventory on qr.scan_limit_reached, notifying a team channel, writing to a database your other systems trust — an unverified endpoint means anyone can trigger that action on demand. Signature verification is the difference between 'a request arrived at this URL' and 'a request that SMLLR actually sent arrived at this URL.'

The Exact Header Format

Three headers arrive with every generic endpoint delivery:

  • **`X-Smllr-Signature`** — `sha256=<hex-encoded HMAC>`, the value you'll recompute and compare.
  • **`X-Smllr-Timestamp`** — the Unix timestamp (seconds) at the moment SMLLR sent the request, and the same value used inside the signed string.
  • **`X-Smllr-Event`** — the event type for this delivery (e.g. `scan.created`), useful for routing before you've even parsed the body.

The Signing Formula

The signature is an HMAC-SHA256 of a single concatenated string — the timestamp, a literal period, and the raw request body — keyed with your endpoint's secret:

signature = HMAC-SHA256(secret, `${timestamp}.${body}`)

A few details that matter in practice: body has to be the raw, unparsed request body — if your framework auto-parses JSON before you can capture the raw bytes, re-stringifying the parsed object won't reliably reproduce byte-for-byte the same string SMLLR signed (key order, whitespace, and number formatting can all differ). And the timestamp is part of the signed string, not just a sidecar value — it's there specifically so a captured, valid payload can't be silently replayed hours later without you having a way to notice.

Verifying It in Node.js

A minimal Express example, using the raw body (captured via express.raw() rather than express.json(), so req.body is a Buffer and not already-parsed):

const crypto = require('crypto');
const express = require('express');
const app = express();

app.post('/webhooks/smllr', express.raw({ type: 'application/json' }), (req, res) => {
  const signatureHeader = req.get('X-Smllr-Signature') || '';
  const timestamp = req.get('X-Smllr-Timestamp') || '';
  const rawBody = req.body.toString('utf8');

  const expected = crypto
    .createHmac('sha256', process.env.SMLLR_WEBHOOK_SECRET)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  const received = signatureHeader.replace('sha256=', '');

  const isValid =
    received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(rawBody);
  // handle event.type / event.data here
  res.status(200).send('ok');
});

Two things worth calling out: crypto.timingSafeEqual is used instead of === specifically to avoid a timing side-channel that could theoretically leak information about the correct signature one byte at a time, and both buffers passed to it need to be the same length first (a plain string comparison of unequal lengths would throw). Store SMLLR_WEBHOOK_SECRET as an environment variable or in a secrets manager — never commit it alongside application code.

The Slack Exception

Slack endpoints don't carry any of the above — no X-Smllr-Signature, no HMAC, nothing to verify. That's because the delivery mechanism is different: SMLLR is POSTing to a Slack Incoming Webhook URL, and the URL itself (https://hooks.slack.com/services/...) functions as the credential — Slack accepts any request to that exact URL as authorized. There's no server of yours in the loop to verify anything on, so treat the Incoming Webhook URL the way you'd treat an API key: don't post it publicly, don't commit it to a public repo, and regenerate it in Slack if you suspect it's leaked.

What It Costs

Signature verification is a property of every generic webhook endpoint on SMLLR's Pro plan (₹4,999/month) and above — there's no separate charge or higher tier required to get signed payloads; every generic endpoint gets one automatically. For the endpoint setup itself, see How to Set Up SMLLR Webhooks.

Frequently Asked Questions

What's the exact formula SMLLR uses to sign a webhook payload?

HMAC-SHA256(secret, ${timestamp}.${body}), where secret is the per-endpoint signing secret, timestamp is the Unix timestamp in the X-Smllr-Timestamp header, and body is the raw, unparsed request body.

Where do I get my endpoint's signing secret?

It's generated automatically when you create a generic endpoint in Settings → Webhooks & Slack, and shown once at creation time — store it securely, since it isn't displayed again after that.

Why doesn't my signature verification match even though I'm using the right secret?

The most common cause is signing a re-serialized/parsed version of the body instead of the exact raw bytes SMLLR sent — capture the raw request body before any JSON parsing middleware touches it.

Do Slack webhook endpoints have a signature to verify?

No. A Slack Incoming Webhook URL is itself the secret — there's no separate signature header on Slack deliveries.

Why use crypto.timingSafeEqual instead of a normal string comparison?

A normal comparison can leak timing information about how many leading characters matched, which is a theoretical side-channel; timingSafeEqual compares in constant time regardless of where the first mismatch occurs.

Is the timestamp header just informational, or does it matter for security?

It matters — it's part of the signed string itself, which lets you optionally reject requests with an old timestamp to reduce the window for a captured payload to be replayed.

What plan do I need for signed webhooks?

The same Pro plan (₹4,999/month) or above requirement as webhooks generally — every generic endpoint on that plan gets signed deliveries automatically.

Related Resources