Your first $5 top-up doubles to $10 — one-time, for every new account. Get Started
Krova CloudKrova Cloud

Webhooks

Receive Krova Cloud webhook events and verify their signatures with @krovacloud/webhook — HMAC-SHA256, constant-time comparison, and replay protection, in any framework.

Krova Cloud can deliver outbound webhooks to a URL you control when things happen to your Cubes (for example cube.running or cube.created). Every delivery is signed, so you can prove it genuinely came from Krova Cloud and was not tampered with or replayed. The @krovacloud/webhook package performs that verification for you — HMAC-SHA256, constant-time comparison, and a replay window, with zero runtime dependencies (built on node:crypto).

Install

npm install @krovacloud/webhook
# or: pnpm add @krovacloud/webhook  /  yarn add @krovacloud/webhook

Requires Node.js 18 or newer. Ships both ESM and CommonJS, types bundled.

Create a webhook endpoint

Register the endpoint over the API (or with the SDK's client.raw), choosing which events to receive. Krova Cloud returns a signing secret— store it as an environment variable; you'll need it to verify deliveries.

const { data } = await krova.raw.POST("/spaces/{spaceId}/webhooks", {
  params: { path: { spaceId: "space_123" } },
  body: { url: "https://example.com/webhooks/krova", events: ["cube.running"] },
});

Prefer to develop locally without deploying? The CLI's krova webhooks listen runs a local receiver that verifies each delivery and pretty-prints the event.

How a delivery is signed

On every delivery, Krova Cloud sends these headers alongside the raw JSON body:

  • X-Krova-Signature — e.g. t=1700000000,v1=3f8a… (signed timestamp + HMAC-SHA256).
  • X-Krova-Event — the event name, e.g. cube.created.
  • X-Krova-Delivery — a unique id for this delivery.

The signature is computed as:

timestamp = floor(Date.now() / 1000)
signed    = `${timestamp}.${rawBody}`      // "t.body", literal dot separator
v1        = HMAC_SHA256(secret, signed)     // lowercase hex

X-Krova-Signature: t=<timestamp>,v1=<hex>

Verification recomputes the HMAC, compares it to v1 in constant time, and rejects the request if the timestamp is outside a 300-second (5-minute) window in either direction — which stops replays. Unknown signature fields are ignored, so future v2/v3 schemes stay backward compatible.

The raw body matters. The signature is over the exact bytes Krova Cloud sent. If your framework parses the JSON and you re-serialize it, the bytes — and therefore the signature — will differ. Always verify against the raw request body.

Express

Mount express.raw() on the webhook route so the handler receives the raw Buffer, then use the krovaWebhook middleware. On a bad signature, stale timestamp, or missing header it responds 401 and does not call next().

import express from "express";
import { krovaWebhook } from "@krovacloud/webhook";

const app = express();

app.post(
  "/webhooks/krova",
  express.raw({ type: "application/json" }), // REQUIRED: gives us the raw body
  krovaWebhook({ secret: process.env.KROVA_WEBHOOK_SECRET! }),
  (req, res) => {
    // Signature already verified. Now it's safe to parse.
    const payload = JSON.parse(req.body.toString("utf8"));
    const { event, delivery, timestamp } = req.krovaWebhook!;
    console.log(`Received ${event} (${delivery}) signed at ${timestamp}`);
    res.status(200).json({ received: true });
  },
);

Next.js Route Handler

Read the raw body with await req.text() and pass the Headers straight to verifyKrovaRequest.

// app/api/webhooks/krova/route.ts
import { verifyKrovaRequest } from "@krovacloud/webhook";

export async function POST(req: Request) {
  const rawBody = await req.text(); // raw, un-parsed body
  const result = verifyKrovaRequest({
    payload: rawBody,
    headers: req.headers, // matched case-insensitively
    secret: process.env.KROVA_WEBHOOK_SECRET!,
  });

  if (!result.valid) {
    return Response.json({ error: result.reason }, { status: 401 });
  }

  const payload = JSON.parse(rawBody);
  // result.event / result.delivery / result.timestamp are verified
  return Response.json({ received: true });
}

Framework-agnostic verify

Use verifyKrovaWebhook when you have the raw body and the signature header in hand (any framework, serverless, or a queue consumer). It never throws — it returns a result you branch on.

import { verifyKrovaWebhook } from "@krovacloud/webhook";

const result = verifyKrovaWebhook({
  payload: rawBody,                        // string or Buffer — the raw body
  signature: req.headers["x-krova-signature"] ?? "",
  secret: process.env.KROVA_WEBHOOK_SECRET!,
  // toleranceSeconds: 300,                // optional, defaults to 300 (5 min)
});

if (!result.valid) {
  // result.reason: "invalid_signature" | "timestamp_out_of_tolerance" | "malformed_header"
  throw new Error(`Krova webhook rejected: ${result.reason}`);
}
// Safe to trust and process the payload now.

Prefer exceptions? verifyKrovaWebhookOrThrow throws a KrovaWebhookError (with a .reason) instead of returning a result. There's also a verifyKrovaRequest that accepts a whole request's headers (a Headers instance, a plain object, or anything with .get()) and pulls the event and delivery id out for you — ideal for Next.js, Fastify, and node:http.

API summary

  • verifyKrovaWebhook(options) → non-throwing { valid, reason?, timestamp? }.
  • verifyKrovaWebhookOrThrow(options) → throws KrovaWebhookError on failure.
  • verifyKrovaRequest({ payload, headers, secret }) → framework-agnostic; returns the verified event / delivery too.
  • krovaWebhook({ secret }) → Express middleware.
  • parseSignatureHeader / computeSignature → low-level helpers for tests and custom tooling.

Next steps

  • CLIkrova webhooks listen to test deliveries locally.
  • TypeScript SDK — register and manage webhook endpoints over the API.
  • REST API reference — the webhook endpoints and event payloads.