Webhooks

Receive HMAC-signed event notifications at your own HTTPS endpoint.

Delivery format

Each delivery is an HTTPS POST with a JSON body and the headers listed below. The body is the canonical envelope:

{
  "event": "incident.created",
  "occurred_at": "2026-05-09T12:00:00.000Z",
  "tenant_id": "<your tenant uuid>",
  "data": { ... }
}

Signature verification

Always verify the signature before processing the body. Use a constant-time comparison to avoid timing attacks. During a secret rotation the previous secret stays valid for 60 seconds, so accept either signature within the grace window.

Node.js

// Node.js (built-in crypto, no deps)
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyKoviraSignature(opts: {
  secret: string;
  timestampHeader: string;
  signatureHeader: string;
  rawBody: string;
}): boolean {
  const expected = createHmac("sha256", opts.secret)
    .update(`${opts.timestampHeader}.${opts.rawBody}`)
    .digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(opts.signatureHeader, "hex");
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}

Python

# Python (stdlib only)
import hmac, hashlib

def verify_kovira_signature(secret: str, ts: str, sig: str, raw_body: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        f"{ts}.{raw_body}".encode(),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, sig)

Go

// Go (stdlib only)
package webhook

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
)

func VerifyKoviraSignature(secret, timestamp, signature string, rawBody []byte) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    fmt.Fprintf(mac, "%s.%s", timestamp, rawBody)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}

Retries and dead-letter queue

Deliveries are retried with exponential backoff at 1m, 5m, 15m, 1h, 4h, and 12h. After the sixth attempt the queue entry is parked in the dead-letter queue (DLQ).

A 2xx response counts as success. 4xx and 5xx responses are retried. Connection errors and timeouts (8 second wall clock) are retried.

Operators can replay any DLQ row from the workspace settings UI. Replay resets the attempt counter and re-queues the row for immediate delivery.

Test events

The "Send test event" action enqueues a sample payload tagged with _test=true at the envelope root. Receivers can filter on this flag so test events never trip production pipelines.

Secret rotation

Rotating a subscription secret returns a fresh value once. The previous secret remains valid for a 60 second grace window so in-flight deliveries verify with either key while you roll your receiver configuration.