Help center
Developers

Webhooks

Get a signed HTTP request when praise arrives or a request is answered.

Add endpoints under Developers, Webhooks. Each has its own signing secret, shown once, and its own list of events.

Events

EventFires when
praise.submittedNew praise arrives from a form, the API, an AI app, or the browser extension
praise.approvedA story becomes approved
praise.rejectedA story becomes rejected
request.openedA customer opens their request link for the first time
request.completedA customer replies through their request link

Approving something that is already approved does not fire again. webhook.test is sent only by the Send test button.

The request

POST https://your-server.example/hook
Content-Type: application/json
KikoPraise-Event: praise.approved
KikoPraise-Delivery: evt_8fK2...
KikoPraise-Signature: t=1700000000,v1=3b1c...
{
  "id": "evt_8fK2...",
  "type": "praise.approved",
  "createdAt": "2026-10-08T01:20:00.000Z",
  "data": { "testimonial": { "id": "...", "authorName": "...", "content": "...", "status": "APPROVED" } }
}

The id is the same on every redelivery, so you can use it to ignore duplicates. A customer's private email address is never included.

Verify the signature

The signature is HMAC-SHA256(secret, "<t>.<raw body>") as hex, where t is the timestamp in the header. Always verify it, and reject old timestamps (5 minutes is typical).

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyKikoPraise(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
  const timestamp = Number(parts.t);
  if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;

  const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  const given = parts.v1 ?? "";
  return given.length === expected.length && timingSafeEqual(Buffer.from(expected), Buffer.from(given));
}

Use the raw body exactly as received. Parsing and re-serialising the JSON changes the bytes and breaks the signature.

Delivery

  • Answer with a 2xx within 8 seconds.
  • A failed delivery is retried twice more, after 3 and 12 seconds, then recorded as failed. You can see and redeliver any delivery.
  • Redirects are not followed.
  • After 15 failures in a row the endpoint is switched off, with the reason shown. Resume turns it back on.
  • The last 100 deliveries per endpoint are kept.

Safety

Production endpoints must be public https:// URLs. Private network, loopback and cloud metadata addresses are refused when you add the URL and again every time we send. Up to 10 endpoints per workspace.

On this page