Skip to content

Repository files navigation

kick-webhook-verify

CI

RSA signature verification for Kick.com webhooks (Kick Events API), following Kick's official webhook security spec.

Zero runtime dependencies. TypeScript-first, ships its own types.

There's no dedicated Node.js/TypeScript package for this today — general-purpose Kick API clients exist (@nekiro/kick-api, @retconned/kick-js), but not a focused signature-verification module like the ones that already exist for Go and Python.

Install

npm install kick-webhook-verify

Why raw body matters

Kick signs the exact bytes of the request body. If your framework parses JSON before you verify (e.g. express.json()), the body gets re-serialized and the signature will no longer match — even though the payload is semantically identical. Always capture the raw body.

Usage (Express)

import express from "express";
import { verifyKickWebhook } from "kick-webhook-verify";

const app = express();

app.post(
  "/webhooks/kick",
  express.raw({ type: "application/json" }), // raw body, not parsed JSON
  async (req, res) => {
    const result = await verifyKickWebhook({
      publicKey: process.env.KICK_PUBLIC_KEY!,
      headers: {
        "kick-event-message-id": req.header("Kick-Event-Message-Id"),
        "kick-event-message-timestamp": req.header("Kick-Event-Message-Timestamp"),
        "kick-event-signature": req.header("Kick-Event-Signature"),
        "kick-event-type": req.header("Kick-Event-Type"),
      },
      rawBody: req.body, // Buffer, from express.raw()
    });

    if (!result.isValid) {
      return res.status(401).json({ error: "Invalid signature", details: result.errors });
    }

    const event = JSON.parse(req.body.toString("utf8"));
    // ... handle event

    res.sendStatus(200);
  }
);

Fetching Kick's public key

Kick publishes its current RSA public key at a public endpoint. fetchKickPublicKey() wraps that call with retry + timeout:

import { fetchKickPublicKey } from "kick-webhook-verify";

const publicKey = await fetchKickPublicKey();
// cache it yourself (Redis, in-memory, env var, whatever fits your app) —
// this function does not cache anything.

Class-based API

If you're verifying many webhooks and want to reuse a configured instance instead of passing publicKey every time:

import { KickSecurityVerifier, createDefaultSecurityConfig } from "kick-webhook-verify";

const verifier = new KickSecurityVerifier(
  createDefaultSecurityConfig(publicKey, { maxTimestampDelta: 300 })
);

const result = await verifier.verifyWebhookAuthenticity(headers, rawBody);

Pass a logger (anything with debug/info/warn/error(message, meta?), e.g. console or winston) if you want verification internals logged — it's silent by default.

API

  • verifyKickWebhook({ publicKey, headers, rawBody, maxTimestampDelta?, logger? })Promise<VerificationResult>
  • fetchKickPublicKey({ maxAttempts?, timeoutMs?, logger? })Promise<string>maxAttempts is the total number of tries (default 3), not additional retries. A malformed 200 response is not retried (retrying won't fix a response Kick already sent successfully).
  • new KickSecurityVerifier(config)verifyWebhookAuthenticity, updateConfig, getConfigStatus, generateSHA256Hash
  • createDefaultSecurityConfig(publicKey?, overrides?)
  • parseKickPublicKey(pem) — validates PEM formatting
  • KICK_SECURITY_CONSTANTSMAX_TIMESTAMP_DELTA, SIGNATURE_ALGORITHM, RSA_PADDING

VerificationResult:

interface VerificationResult {
  isValid: boolean;
  errors: string[];
  messageId: string;
  timestamp: string;
}

Security notes

  • Signed message format: messageId + "." + timestamp + "." + rawBody, verified with RSA-SHA256 / PKCS1v15 padding — exactly as Kick documents it.
  • Timestamps outside maxTimestampDelta (default 300s) are rejected to mitigate replay attacks.
  • getConfigStatus() never exposes the raw public key, only a truncated fingerprint — safe to log or expose in health checks.

License

MIT


Extracted from and maintained alongside the webhook handling in TriBathon, a multi-platform subathon timer for Twitch, Kick, YouTube, StreamElements and Streamlabs.

About

RSA signature verification for Kick.com webhooks — zero dependencies, TypeScript

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages