Kenal Stamps Docs

Webhooks

Receive real-time contract status notifications via signed webhooks.

Kenal Stamps delivers webhook events to your configured endpoint when contract statuses change. Webhooks are signed with your webhookSecret so you can verify their authenticity.

Setup

Configure your webhookUrl when creating or editing your integration instance in the dashboard at /dashboard/admin/integrations.

Webhooks are only sent for contracts submitted via the Partner Integration API (i.e., contracts with a partnerId).

Event Types

EventTrigger
ContractConfirmedContract confirmed and wallet charged
ContractProcessingContract picked up for LHDN submission
ContractCompletedStamping complete, certificate available
ContractFailedProcessing failed

Payload

{
  "eventType": "ContractCompleted",
  "contractId": "a1b2c3d4-...",
  "externalReferenceId": "LOAN-001",
  "organizationId": "...",
  "contractType": "General",
  "status": "Completed",
  "occurredAt": "2026-03-15T14:30:00.000Z"
}

Webhook Signing

Each webhook delivery includes these headers:

HeaderDescription
x-service-idYour integration UUID
x-event-typeThe event type (e.g. ContractCompleted)
x-timestampISO-8601 timestamp
x-signatureHMAC-SHA256 hex signature

Verification Algorithm

verify-webhook.ts
import crypto from "crypto";

function verifyWebhook(
  webhookSecret: string,
  timestamp: string,
  eventType: string,
  rawBody: string,
  receivedSignature: string
): boolean {
  const bodyHash = crypto.createHash("sha256").update(rawBody).digest("hex");
  const canonical = `${timestamp}\n${eventType}\n${bodyHash}`;
  const expected = crypto
    .createHmac("sha256", webhookSecret)
    .update(canonical)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(receivedSignature, "hex")
  );
}

Always use crypto.timingSafeEqual() for signature comparison to prevent timing attacks.

Delivery Semantics

  • At-least-once delivery: your endpoint may receive the same event more than once. Use contractId + eventType for deduplication.
  • Retry policy: failed deliveries (non-2xx response) are retried with exponential backoff — base 30 seconds, doubling per attempt, capped at 1 hour, up to 10 attempts.
  • Timeout: your endpoint must respond within 10 seconds.

Handling Webhooks

Your endpoint should:

  1. Verify the signature before processing
  2. Return 200 immediately (process asynchronously if needed)
  3. Be idempotent — handle duplicate deliveries gracefully
webhook-handler.ts
import express from "express";

app.post("/webhooks/kenal-stamps", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-signature"] as string;
  const timestamp = req.headers["x-timestamp"] as string;
  const eventType = req.headers["x-event-type"] as string;

  if (!verifyWebhook(WEBHOOK_SECRET, timestamp, eventType, req.body.toString(), signature)) {
    return res.status(401).send("Invalid signature");
  }

  const payload = JSON.parse(req.body.toString());

  // Process the event asynchronously
  processWebhookEvent(payload).catch(console.error);

  // Return 200 immediately
  res.status(200).send("OK");
});

Monitoring

View webhook delivery status in the admin dashboard at /admin/webhooks. You can:

  • See all pending, delivered, and failed webhook events
  • Retry failed deliveries manually
  • Filter by status and search by contract ID