Skip to main content
Every HTTP Tool invocation carries an X-SmartAlex-Signature header. Verifying it lets you reject forged requests — including replay attacks and any third party that gets hold of your endpoint URL.

The algorithm

  1. Take the raw request body bytes exactly as received (do not re-serialize).
  2. Take the millisecond timestamp from the t= part of X-SmartAlex-Signature.
  3. Compute HMAC-SHA256(secret, timestamp + "." + rawBody) and compare it, in constant time, against the v1= part of the same header.
  4. Reject signatures older than 5 minutes (defense against replay).
The signing secret was shown to you once on first tool save (or after rotation). It has the prefix shs_ and is 64 hex characters.

The header shape

X-SmartAlex-Signature: t=1733839200123,v1=4a7c9e2f...d6
PartMeaning
t=Epoch milliseconds at the moment we signed.
v1=Lowercase hex of HMAC-SHA256(secret, "<t>.<rawBody>").
The v1= prefix is reserved for the version of the signing scheme. If we ever need to change it, future requests will carry v2=... and existing verifiers will reject them, prompting a doc-driven upgrade — never a silent break.

Constant-time comparison

Always use a constant-time string comparison (crypto.timingSafeEqual in Node, hmac.compare_digest in Python, etc.). A naive == comparison can leak the secret one byte at a time via timing.

Code samples

import crypto from 'node:crypto';
import express from 'express';

const SIGNING_SECRET = process.env.SMARTALEX_SIGNING_SECRET; // shs_...

function verifySignature(req) {
  const header = req.header('X-SmartAlex-Signature');
  if (!header) return false;

  const [tPart, v1Part] = header.split(',');
  const ts = tPart?.split('=')[1];
  const v1 = v1Part?.split('=')[1];
  if (!ts || !v1) return false;

  // Replay defense: reject signatures older than 5 minutes or
  // more than 1 minute in the future (clock skew tolerance).
  const ageSeconds = (Date.now() - parseInt(ts, 10)) / 1000;
  if (ageSeconds > 300 || ageSeconds < -60) return false;

  // Use raw request body bytes, not the JSON-stringified parsed object.
  const expected = crypto
    .createHmac('sha256', SIGNING_SECRET)
    .update(`${ts}.${req.rawBody}`)
    .digest('hex');

  // Constant-time compare. Lengths must match or timingSafeEqual throws.
  if (v1.length !== expected.length) return false;
  return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}

const app = express();

// Capture raw body so we can HMAC the exact bytes that arrived.
app.use(express.json({
  verify: (req, _res, buf) => {
    req.rawBody = buf.toString('utf8');
  },
}));

app.post('/smartalex/lookup', (req, res) => {
  if (!verifySignature(req)) {
    return res.status(401).send('Invalid signature');
  }

  const { tool, arguments: args, call_id } = req.body;

  if (tool === 'lookup_routing') {
    const extension = lookupRouting(args.query);
    return res.json({
      extension,
      hint: `Transfer to extension ${extension}.`,
    });
  }

  res.status(404).send('Unknown tool');
});

app.listen(3000);

Rotating the signing secret

Hit Rotate in the Custom HTTP Tools manager.
Rotation invalidates the old secret immediately. There’s no grace period. Update your endpoint(s) with the new secret in the same change window. Until you do, every request from us will fail your verifySignature check.
The dashboard shows the new secret once. Save it the same way you saved the first one.

Common pitfalls

SymptomCauseFix
Signature verifies on test fire but not on live callsYou’re using the test-fire payload to derive your raw body in the wrong orderAlways read the raw body bytes off the wire — do not re-serialize from parsed JSON.
Signature fails after a code deployYour framework changed how it buffers the bodyPin to a body-buffering setup (Express verify callback, Flask get_data(), Go io.ReadAll).
First signature works, later ones failYou verify against a stale Date.now() after sitting in a queueCheck t= from the header against the current time, not the time you started processing.
All signatures fail after rotationYour endpoint still has the old secretPush the new secret to your environment + restart.
Some signatures fail intermittentlyClock skew between you and us beyond 60 secondsSync your server clock (NTP). The 60s future-tolerance handles small drift.

Next: Error codes

Every failure mode the runtime can surface, with error_message and llm_message.