Agent skill

Quicknode Webhooks Skill

Receive and verify Quicknode Streams and Quicknode Webhooks deliveries. Use when setting up a Quicknode webhook handler, debugging X-QN-Signature verification, or processing block, transaction and log data streamed from a Quicknode Stream or Webhook.

Install this skill

npx skills add hookdeck/webhook-skills --skill quicknode-webhooks


Quicknode (quicknode.com) is blockchain infrastructure. Two products POST to an HTTP endpoint with the same X-QN-* signature: Webhooks (template-filtered on-chain events) and Streams (a dataset pipeline whose webhook destination delivers each block or batch).

When to Use This Skill

  • How do I receive Quicknode Streams or Quicknode Webhooks deliveries?
  • How do I verify the X-QN-Signature header?
  • Why does my Quicknode signature verification fail?
  • How do I use X-QN-Nonce and X-QN-Timestamp?
  • Why do compressed (gzip) Quicknode deliveries fail verification?

Keep payload compression (gzip) off on the stream or webhook: Quicknode signs the uncompressed body.

Verification (core)

X-QN-Signature is the bare lowercase hex HMAC-SHA256, keyed with the stream's or webhook's security token (used as UTF-8, not decoded), of X-QN-Nonce + X-QN-Timestamp + the raw body, concatenated with no separators. Header values are used exactly as received (latin1 bytes, never trimmed).

const crypto = require('crypto');

function verify(rawBody /* Buffer */, headers, secret) {
  const { 'x-qn-signature': sig, 'x-qn-nonce': nonce, 'x-qn-timestamp': ts } = headers;
  if (!sig || !nonce || !ts || !/^[0-9a-fA-F]{64}$/.test(sig)) return false; // 401
  const content = Buffer.concat([Buffer.from(nonce, 'latin1'), Buffer.from(ts, 'latin1'), rawBody]);
  const expected = crypto.createHmac('sha256', secret).update(content).digest();
  return crypto.timingSafeEqual(Buffer.from(sig.toLowerCase(), 'hex'), expected);
}
import hashlib, hmac, re

def verify(raw_body: bytes, headers, secret: str) -> bool:
    sig, nonce, ts = (headers.get(h) for h in ("x-qn-signature", "x-qn-nonce", "x-qn-timestamp"))
    if not (sig and nonce and ts and re.fullmatch(r"[0-9a-fA-F]{64}", sig)):
        return False  # 401
    content = b"".join([nonce.encode("latin-1"), ts.encode("latin-1"), raw_body])
    expected = hmac.new(secret.encode(), content, hashlib.sha256).digest()
    return hmac.compare_digest(bytes.fromhex(sig), expected)
  • Verify the raw bytes (express.raw({ type: '*/*' }), await request.arrayBuffer(), await request.body()); never JSON.parse/JSON.stringify first.
  • Validate the signature format (exactly 64 hex characters) before decoding, then compare in constant time. Hex is case-insensitive.
  • Missing or empty header, malformed value or mismatch: respond 401.
  • There is no Quicknode SDK helper for this, so all three examples verify manually.

Timestamp tolerance is opt-in

Do not reject old timestamps by default: Hookdeck retries and replays resend the original headers and timestamp, and Hookdeck applies no tolerance itself. The examples offer an opt-in QUICKNODE_WEBHOOK_TOLERANCE_SECONDS (unset or 0 = off). The X-QN-Timestamp unit is not documented, so the examples only parse it when you opt in. Storing X-QN-Nonce skips exact repeats (Hookdeck retries and replays resend it), but Quicknode doesn't document whether its own retries keep the nonce, so also key idempotency on block number or hash and transaction hash.

For complete handlers with route wiring, payload dispatch, and tests, see:

Payloads (no event types)

Quicknode has no event type field or header. Each stream or webhook delivers its filter's output: blocks, transactions, logs or matched events. The examples dispatch on payload shape using clearly labelled illustrative payloads: a Streams batch with data and metadata, or a Webhooks array of matched transactions. Check your own stream's dataset or webhook filter for the real shape.

Important Headers

HeaderDescription
X-QN-SignatureLowercase hex HMAC-SHA256 digest, no prefix
X-QN-NonceUnique string that prevents replay attacks; part of the signed content
X-QN-TimestampWhen the message was signed; part of the signed content (unit not documented)

Delivery Behavior

  • Method is POST.
  • Streams: respond 2xx within the configured timeout (default 30 s). Retries every N seconds (default 1) and pauses after N failed retries (default 3). Blocks are delivered in sequence.
  • Webhooks: "automatic retries for non-200 responses"; the schedule is not stated.
  • Treat delivery as at-least-once and deduplicate on block/transaction identifiers (the nonce is only documented as a replay guard).

Environment Variables

QUICKNODE_WEBHOOK_SECRET=your_security_token       # Stream's or webhook's security token (UTF-8)
QUICKNODE_WEBHOOK_TOLERANCE_SECONDS=0              # Optional: unset or 0 = no timestamp check

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 quicknode --path /webhooks/quicknode

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 11, 2026

View on GitHub →