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/webhookRequires 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)→ throwsKrovaWebhookErroron failure.verifyKrovaRequest({ payload, headers, secret })→ framework-agnostic; returns the verifiedevent/deliverytoo.krovaWebhook({ secret })→ Express middleware.parseSignatureHeader/computeSignature→ low-level helpers for tests and custom tooling.
Next steps
- CLI —
krova webhooks listento test deliveries locally. - TypeScript SDK — register and manage webhook endpoints over the API.
- REST API reference — the webhook endpoints and event payloads.