notpanel
ServicesPricingFAQGiveaway
notpanel

The fastest and most affordable SMM panel. Trusted by 1M+ users worldwide.

Product

  • Services
  • Pricing
  • Why NotPanel
  • About
  • Developers
  • Blog
  • FAQ

Legal

  • Terms of Service
  • Privacy Policy
  • Refund Policy

Connect

  • Contact Us
  • support@notpanel.com

© © 2026 NotPanel. All rights reserved.

All posts
Engineering April 25, 2026· 8 min read

Webhook signing for SMM panels: HMAC-SHA256 in production

Why webhook signing matters for SMM panel integrations, how HMAC-SHA256 with a timestamp prevents both forgery and replay, and a working Node.js verification implementation.


Webhook security is the part of an SMM integration most teams skip until something goes wrong. The default — receive POSTs at a public URL and trust anything arriving there — is fine when nothing has gone wrong. The first time someone fabricates webhook deliveries against you, the cost of skipping signing becomes clear.

What can go wrong without signing

Your webhook URL is publicly reachable by definition. Anyone who guesses or scrapes it (and most webhook URLs are predictable — /webhook, /notpanel-webhook, subdomain patterns) can send fabricated requests. Without signature verification, your code can't tell the difference. Specific failure modes:

  • Fake order completion. Attacker forges a payload claiming order #123 completed. Your code marks it complete in your downstream system. The customer sees a completed order; the underlying provider hasn't delivered anything.
  • Fake refund. Attacker forges an order.refunded event. Your code refunds the customer. You're now paying out phantom refunds against orders that didn't actually fail.
  • Fake balance alerts. Attacker spams balance.low events. Your auto-top-up logic, if any, kicks in repeatedly.
  • Replay attacks. Even with signing, if you don't check the timestamp an attacker who captures one valid request can replay it. The replay is valid by signature alone; only the timestamp would expose it.
The unsigned-webhook bug we keep seeing

Operator integrates a panel with no signing. Six months go by. They eventually notice their downstream system has been processing maybe 3% extra "completions" they have no source for. Always the same: someone scraped the URL, started forging events, the operator's auto-credit logic ran on each one. By the time they catch it, the financial cleanup is the worst part of the job.

How HMAC-SHA256 fixes this

HMAC (Hash-based Message Authentication Code) is a cryptographic primitive that combines a shared secret with a message to produce a fixed-length signature. Both sides — sender and receiver — know the secret. The sender computes the signature over the message body and attaches it to the request. The receiver computes its own signature over the body and compares.

The receiver doesn't need to share the secret with anyone but the sender. The signature can't be forged without the secret. As long as the secret stays secret, only the genuine sender can produce signatures that verify.

The full contract

A robust webhook signing scheme has four ingredients:

  1. Per-endpoint secret. Each registered webhook gets its own. If one secret leaks, the blast radius is limited to that endpoint.
  2. HMAC-SHA256 of the raw request body. Computed server-side, sent as a header (e.g. X-NotPanel-Signature).
  3. Timestamp header. Unix seconds at send time. The receiver checks the signature AND that the timestamp is recent — typically within ±5 minutes of the receiver's clock.
  4. Constant-time comparison. Comparing signatures with === leaks timing information. Use crypto.timingSafeEqual in Node, or the equivalent in your language.
Receive POSTRead raw body + headersTimestamp checkWithin ±5 min?HMAC computeSHA256(secret, body)AcceptProcess eventStale → 401Mismatch → 401
Verification flow at the receiver. Both checks must pass; either one alone is insufficient.

Reference verification (Node.js)

import { createHmac, timingSafeEqual } from "node:crypto";

interface VerifyParams {
  rawBody: string;
  signatureHeader: string | null;
  timestampHeader: string | null;
  secret: string;
  /** Tolerance window in seconds. Default 5 minutes. */
  toleranceSec?: number;
}

export function verifyWebhook({
  rawBody,
  signatureHeader,
  timestampHeader,
  secret,
  toleranceSec = 300,
}: VerifyParams): boolean {
  if (!signatureHeader || !timestampHeader) return false;

  const ts = Number.parseInt(timestampHeader, 10);
  if (!Number.isFinite(ts)) return false;

  const nowSec = Math.floor(Date.now() / 1000);
  if (Math.abs(nowSec - ts) > toleranceSec) return false;

  const expected = createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  let received: Buffer;
  try {
    received = Buffer.from(signatureHeader, "hex");
  } catch {
    return false;
  }
  const expectedBuf = Buffer.from(expected, "hex");
  if (received.length !== expectedBuf.length) return false;

  return timingSafeEqual(received, expectedBuf);
}

Wiring it into an Express handler

import express from "express";
import { verifyWebhook } from "./verify-webhook";

const app = express();

// Capture the raw body BEFORE any JSON parsing. The signature was computed
// over the raw bytes; re-serialising the parsed object will not match.
app.post(
  "/notpanel-webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const ok = verifyWebhook({
      rawBody: req.body.toString("utf-8"),
      signatureHeader: req.header("X-NotPanel-Signature"),
      timestampHeader: req.header("X-NotPanel-Timestamp"),
      secret: process.env.NOTPANEL_WEBHOOK_SECRET!,
    });

    if (!ok) {
      return res.status(401).json({ error: "Invalid signature" });
    }

    const event = JSON.parse(req.body.toString("utf-8"));
    // ... handle event
    res.status(200).json({ received: true });
  },
);

The "raw body" gotcha

Most JSON-parsing middleware reads the request body and discards the original bytes. By the time your handler runs, you have a parsed object — but the signature was computed over the original bytes, including specific whitespace and key ordering. Re-serialising the parsed object will produce different bytes, a different signature, and spurious verification failures.

Capture the raw body before any JSON parsing. In Express, that's express.raw() instead of express.json(). In Next.js route handlers, read request.text() and verify before parsing. In Fastify, use rawBody. Same principle across frameworks: don't let the JSON parser touch the body until the signature is verified.

Half of all "signature mismatch" bugs are the JSON parser eating the body before the verifier sees it.

Replay attacks and why timestamps matter

Without a timestamp check, an attacker who captures one valid webhook delivery — say, by intercepting a single request through a misconfigured proxy or a compromised log — can replay it indefinitely. The signature is valid; only timing would betray the replay.

A 5-minute tolerance window is the standard. Shorter windows risk rejecting legitimate deliveries when clocks drift; longer windows give attackers a bigger replay window. Five minutes balances the two for most use cases.

For extra paranoia, deduplicate event IDs at the receiver. Webhook events typically have a stable ID — remember the ones you've already processed (in Redis, with a TTL longer than your tolerance window) and reject duplicates. This catches both replays and accidental re-deliveries from the sender.

Reader poll

Of integrations using NotPanel webhooks, what's the breakdown of verification implementations?

HMAC + timestamp + constant-time + dedup41%
HMAC + timestamp + constant-time38%
HMAC + constant-time, no timestamp check14%
No verification at all7%
Audited 217 production integrations against NotPanel webhooks, March 2026.

Rotating secrets

Periodic rotation is good hygiene. The mechanics:

  1. Register a second webhook endpoint with a new secret.
  2. Switch your downstream consumer to verify against either the old or new secret (check both, accept if either matches).
  3. Wait long enough for in-flight deliveries to settle (24 hours is plenty).
  4. Remove the old endpoint.
  5. Switch the consumer to verify only against the new secret.

Most panels don't expose direct "rotate secret" semantics. The register-then-deregister pattern above approximates the same outcome without needing API support for in-place rotation.

What "good enough" looks like

For a typical SMM integration: a determined attacker who has read your public site, scraped your webhook URL, and intercepted some traffic should not be able to forge a valid delivery. HMAC + timestamp + constant-time compare gets you there.

The signing implementation in NotPanel's developer documentation (see /developers/webhooks) covers the exact header names and signature format. The verification code above works against it as-is.

Continue reading

Reseller business

Bulk SMM orders: how agencies process 100,000+ orders per day

Tactics

Choosing the right SMM panel: 12 signals of a real source vs a reseller

Engineering

Why we rebuilt NotPanel on Next.js + PostgreSQL (and what we learned)