Skip to main content
Every Novatrade24 webhook request carries an HMAC-SHA256 signature in the X-Novatrade-Signature header. Always verify the signature before processing — never trust the payload otherwise.

Signature format

X-Novatrade-Signature: t=1729684200,v1=7a9c5d4e3b2f1a8c9d6e5f4b3a2c1d8e7f6a5b4c3d2e1f8a9b7c6d5e4f3a2b1c
  • t — Unix timestamp (seconds) of when the signature was generated.
  • v1 — HMAC-SHA256 hex digest of <timestamp>.<raw_request_body>, keyed by your endpoint’s secret.
Future signature schemes will use higher vN prefixes. Accept any known version; reject unknown.

Verification algorithm

  1. Parse the header into t and v1 parts.
  2. Build the signed string: <t>.<raw_body> (period-separated).
  3. Compute HMAC-SHA256(secret, signed_string) and hex-encode.
  4. Constant-time compare against the v1 value.
  5. Reject if the timestamp is more than 5 minutes in the past (replay protection).
Always use a constant-time comparison (crypto.timingSafeEqual, hmac.compare_digest, etc.). Regular == is vulnerable to timing attacks.

Implementation examples

import { createHmac, timingSafeEqual } from 'crypto';

const MAX_AGE_SECONDS = 300; // 5 minutes

export function verifyWebhookSignature(
  rawBody: string,
  signatureHeader: string,
  secret: string,
): boolean {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((p) => p.split('=', 2) as [string, string]),
  );
  const timestamp = parseInt(parts.t, 10);
  const provided = parts.v1;
  if (!timestamp || !provided) return false;

  const age = Math.floor(Date.now() / 1000) - timestamp;
  if (age < 0 || age > MAX_AGE_SECONDS) return false;

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

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

Common mistakes

The signature is computed over the raw request bytes. Any parser that normalizes whitespace or reorders keys breaks verification. Capture the raw body before JSON parsing.In Express, use express.raw({ type: 'application/json' }) for the webhook route and parse JSON yourself after verification.In Spring Boot, bind the handler parameter as byte[] or String, not a typed DTO, and parse after verification succeeds.
Enforce the 5-minute replay window. Without it, a leaked signed request can be replayed indefinitely. Reject requests with timestamps older than your window.
Each registered endpoint has its own secret. If you have multiple webhook endpoints (e.g. per partner), route by X-Novatrade-Event-Id or endpointId (where available) and use the corresponding secret.
a === b in JavaScript exits early on first mismatched character, leaking timing info. Use crypto.timingSafeEqual. Same in every language.

Next

Event catalog

Payload schemas for every event type.

Replay and test

Inspect delivery history and manually replay events.