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": { ... }
}X-Kovira-Event: the event kindX-Kovira-Timestamp: ISO-8601 UTC timestamp at sign timeX-Kovira-Signature: hex HMAC-SHA256 over{timestamp}.{body}X-Kovira-Subscription-Id: subscription UUID
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.