Octane

Signature verification

Verify X-Octane-Signature by computing HMAC-SHA256 over the timestamp, a dot and the raw body with your webhook secret. Samples in Node.js, Python, PHP, C# and Java.

To verify a webhook, compute HMAC-SHA256 over <t>.<raw body> with your webhook secret and compare it, in constant time, with the v1 value in the X-Octane-Signature header. Reject anything that fails, and reject timestamps older than a few minutes. Always verify before acting on the payload.

The header

X-Octane-Signature: t=1758358803,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
ElementDescription
tUnix timestamp (seconds) at which Octane signed the request.
v1Lower-case hex HMAC-SHA256 of <t>.<raw body> using your webhook secret.

Verification steps

Read the raw body

Use the exact bytes Octane sent. Do not re-serialise parsed JSON: any change in whitespace or key order changes the signature.

Parse the header

Split on ,, then each part on =, to get t and v1.

Check the timestamp

Reject the request if t is too far from your current time. A tolerance of 5 minutes is recommended. This limits replay of captured requests.

Compute the expected signature

signed_payload = t + "." + raw_body, then HMAC_SHA256(secret, signed_payload) as lower-case hex.

Compare in constant time

Compare your value with v1 using a constant-time comparison function. A plain string comparison leaks timing information.

Code samples

Each sample exposes a verify(rawBody, signatureHeader, secret) function and shows how to read the raw body in a common framework.

const crypto = require('node:crypto');

const TOLERANCE_SECONDS = 300;

function verifyOctaneSignature(rawBody, header, secret) {
  if (typeof header !== 'string') return false;

  const parts = Object.fromEntries(
    header.split(',').map((kv) => kv.split('=').map((s) => s.trim())),
  );
  const t = Number(parts.t);
  const v1 = parts.v1;
  if (!Number.isFinite(t) || !v1) return false;

  if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');

  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(v1, 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express: keep the raw body for this route
const express = require('express');
const app = express();

app.post(
  '/webhooks/octane',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const rawBody = req.body.toString('utf8');
    const ok = verifyOctaneSignature(
      rawBody,
      req.get('X-Octane-Signature'),
      process.env.OCTANE_WEBHOOK_SECRET,
    );
    if (!ok) return res.status(401).send('invalid signature');

    const event = JSON.parse(rawBody);
    // Acknowledge first, process asynchronously
    res.sendStatus(200);
    queue.enqueue(event);
  },
);

Common mistakes

  • Parsing the body before hashing. Frameworks that parse JSON automatically often discard the raw bytes. Configure the route to keep them, as shown above.
  • Hashing the body alone. The signed payload is t + "." + body, not the body by itself.
  • Comparing with ==. Use the constant-time comparison your platform provides.
  • Clock drift. If legitimate requests fail the timestamp check, sync your server clock with NTP before widening the tolerance.
  • Upper-case hex. Octane sends lower-case hex. Compare bytes, or normalise case before comparing strings.

How do I test locally?

Ask Octane to send a webhook.test event, or generate one yourself with your secret:

SECRET="your_webhook_secret"
BODY='{"id":"test","type":"webhook.test","created_at":"2026-09-20T09:00:03Z","api_version":"2026-09-01","data":{}}'
T=$(date +%s)
SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')

curl -X POST https://localhost:8443/webhooks/octane \
  -H "Content-Type: application/json" \
  -H "X-Octane-Event: webhook.test" \
  -H "X-Octane-Delivery: test" \
  -H "X-Octane-Signature: t=$T,v1=$SIG" \
  --data-binary "$BODY"

Common questions

How do I verify an Octane webhook signature?
Read the raw request body, take t and v1 from the X-Octane-Signature header, compute HMAC-SHA256 of "<t>.<raw body>" with your webhook secret as lower-case hex, and compare it with v1 using a constant-time comparison. Reject timestamps older than about 5 minutes.
Why does my signature check fail?
The most common causes are hashing a re-serialised JSON body instead of the raw bytes, forgetting the "<t>." prefix, comparing upper-case hex, or a server clock that is out of sync. Fix the raw body handling first.
Where do I get the webhook secret?
Octane generates it when your webhook URL is set and shares it with you once, together with the API key. It is never shown again and never rotated. If you lose it, Octane issues a new key with a new secret.

On this page