# 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).

```javascript
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);
}

```

```python
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:
> 
> * [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/quicknode-webhooks/examples/express/)
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/quicknode-webhooks/examples/nextjs/)
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/quicknode-webhooks/examples/fastapi/)

## 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

| Header | Description |
| --- | --- |
| `X-QN-Signature` | Lowercase hex HMAC-SHA256 digest, no prefix |
| `X-QN-Nonce` | Unique string that prevents replay attacks; part of the signed content |
| `X-QN-Timestamp` | When 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

```bash
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

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

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/quicknode-webhooks/references/overview.md) - Streams vs Webhooks, payload shapes, delivery
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/quicknode-webhooks/references/setup.md) - Dashboard configuration and security token
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/quicknode-webhooks/references/verification.md) - Signature details, gzip and tolerance gotchas