Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.kansato.com/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Whistle pushes events to URLs you configure in Settings → Developer → Webhooks (outbound from Whistle, inbound to your stack). Each POST is signed so you can reject forged traffic. Endpoint URLs and secrets are managed in the dashboard, not through the SDK.

Event types

EventWhen it fires
report.createdA new report was ingested.
enforcement.action.createdAn enforcement action was recorded against a subject.
enforcement.action.updatedAn existing enforcement action was updated (expiry, metadata).
enforcement.action.revokedAn enforcement action was revoked or lifted.
appeal.createdAn appeal was submitted against an enforcement action.
appeal.reviewedAn appeal was reviewed and a decision was made.

Request envelope

Each delivery body is a JSON object:
{
  "id": "evt_abc123",
  "type": "report.created",
  "createdAt": "2026-04-04T12:00:00.000Z",
  "data": { }
}
  • id — unique event ID. Use for idempotency and deduplication.
  • type — the event name from your subscription.
  • createdAt — ISO 8601 timestamp of when the event occurred.
  • data — event-specific payload.

HTTP headers

Whistle sends POST with Content-Type: application/json; charset=utf-8 and:
HeaderMeaning
x-whistle-eventEvent type string.
x-whistle-event-idSame as the envelope id.
x-whistle-delivery-idInternal delivery row ID (retries reuse the same ID).
x-whistle-signatureHMAC-SHA256 signature for verification.

Verifying signatures with the SDK

The SDK provides verifyWebhookSignature to verify incoming webhooks. It parses the signature header, checks the timestamp, computes the HMAC, and returns the typed event.

Next.js App Router

// app/api/webhooks/whistle/route.ts
import { verifyWebhookSignature } from "@kansato/whistle-sdk";

export async function POST(request: Request) {
  const signature = request.headers.get("x-whistle-signature") ?? undefined;
  const payload = await request.text();

  try {
    const event = verifyWebhookSignature({
      payload,
      signature,
      secret: process.env.WHISTLE_WEBHOOK_SECRET!,
      tolerance: 300, // seconds (default: 300)
    });

    switch (event.type) {
      case "report.created":
        // event.data contains the report payload
        console.log("New report:", event.data);
        break;
      case "enforcement.action.created":
        console.log("Enforcement action:", event.data);
        break;
      case "appeal.created":
        console.log("New appeal:", event.data);
        break;
    }

    return Response.json({ received: true });
  } catch (err) {
    console.error("Webhook verification failed:", err);
    return Response.json({ error: "Invalid signature" }, { status: 400 });
  }
}

Express

import express from "express";
import { verifyWebhookSignature } from "@kansato/whistle-sdk";

const app = express();

app.post("/webhooks/whistle", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-whistle-signature"] as string | undefined;

  try {
    const event = verifyWebhookSignature({
      payload: req.body,
      signature,
      secret: process.env.WHISTLE_WEBHOOK_SECRET!,
    });

    // Handle event...
    res.json({ received: true });
  } catch {
    res.status(400).json({ error: "Invalid signature" });
  }
});

Manual verification

The x-whistle-signature header has the format:
t=<unixSeconds>,v1=<hex>
  1. Parse the timestamp t and signature v1 from the header.
  2. Reject if the timestamp is older than your tolerance (recommended: 300 seconds).
  3. Compute HMAC-SHA256 of the string <t>.<rawBody> using your signing secret.
  4. Compare the resulting hex digest to v1 using constant-time comparison.
The raw body must be the exact bytes Whistle sent. Verify before JSON parsing.

Retries and logs

Failed deliveries retry with backoff. Treat x-whistle-delivery-id (or the envelope id) as your idempotency key so a retried POST does not double-apply side effects. The View deliveries panel in the dashboard shows status, HTTP status codes, errors, and payload snapshots for recent attempts.