Webhooks and usage
Subscribe to inference events with HMAC-signed deliveries, and read your usage.
Webhooks push events to a URL you control, so you do not have to poll. Each registration belongs to the consumer that owns the calling key, and deliveries are HMAC-signed with a secret you choose.
Register an endpoint
/api/v1/webhooksurlevent_types["inference.completed", "key.expiring"].secretcurl -X POST "https://api-diagnostics.karmat-hq.com/api/v1/webhooks" \
-H "X-API-Key: $DIAGNOSTICS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://clinic.example.com/hooks/karmat",
"event_types": ["inference.completed", "key.expiring"],
"secret": "'"$(openssl rand -hex 32)"'"
}'{
"id": "wh_01HQ8ZK3M4",
"url": "https://clinic.example.com/hooks/karmat",
"event_types": ["inference.completed", "key.expiring"],
"is_active": true,
"created_at": "2026-08-27T09:21:44Z"
}Store the secret on your side
Verify every delivery
An unverified webhook endpoint is an open API that anyone who learns the URL can post to. Compute the HMAC over the raw body, because parsing first and re-serialising will change the bytes and break the signature.
import hmac, hashlib
def verify(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
# Constant-time: a plain == leaks timing that can forge a signature.
return hmac.compare_digest(expected, signature)import { createHmac, timingSafeEqual } from "node:crypto";
export async function POST(request: Request) {
const raw = await request.text(); // raw text, not request.json()
const signature = request.headers.get("X-Karmat-Signature") ?? "";
const expected = createHmac("sha256", process.env.KARMAT_WEBHOOK_SECRET!)
.update(raw)
.digest("hex");
const ok =
signature.length === expected.length &&
timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!ok) return new Response("invalid signature", { status: 400 });
await enqueue(JSON.parse(raw)); // hand off, respond fast
return new Response(null, { status: 204 });
}Acknowledge fast, work later
List and delete
/api/v1/webhooksReturns your registrations. Secrets are omitted.
curl "https://api-diagnostics.karmat-hq.com/api/v1/webhooks" \
-H "X-API-Key: $DIAGNOSTICS_API_KEY"/api/v1/webhooks/{webhook_id}curl -X DELETE "https://api-diagnostics.karmat-hq.com/api/v1/webhooks/wh_01HQ8ZK3M4" \
-H "X-API-Key: $DIAGNOSTICS_API_KEY"Usage analytics
/api/v1/analytics/usageAggregate request counts and latency for the calling key. This is where you check what you are actually consuming, and the limits attached to your key.
curl "https://api-diagnostics.karmat-hq.com/api/v1/analytics/usage" \
-H "X-API-Key: $DIAGNOSTICS_API_KEY"Pair this with your own logging. Record the response model_version alongside your request id on every inference call. When a clinician questions a result months later, that pairing is the difference between answering and guessing.
