K-Block Cloud sends webhook events to your HTTPS endpoints as signed JSON POST requests. This page is for whoever writes the receiver: what a delivery looks like, how to check that it really comes from K-Block, and what happens when your receiver is slow, down or misconfigured.
To create endpoints, choose events and read the delivery log in the dashboard, see Webhooks. For the data of every event type, see Webhook events.
How events become deliveries
When something happens that an event reports (an approval is requested, a receipt is created…), K-Block records the event together with the change itself. An event therefore exists exactly when the change took effect, and it is never sent for a change that did not take effect.
At the same moment K-Block creates one delivery for every enabled endpoint that subscribes to the event type. K-Block then sends the deliveries in the background:
- at most one delivery per endpoint is in flight at a time, and at most four per organization;
- each request must finish within 10 seconds;
- only a
2xxanswer counts as success; anything else, a timeout or a connection error is a failed attempt.
Delivery is at least once: the same event can arrive more than once, for example when your receiver processed it but its answer did not reach K-Block. Deliveries are also not ordered: a delivery waiting for a retry does not hold back later events. Use the event's id to deduplicate and its created_at to order.
What a delivery looks like
POST /your/webhook/path HTTP/1.1
Content-Type: application/json
User-Agent: KBlock-Webhooks/1.0
KBlock-Event-Id: <event id>
KBlock-Delivery-Id: <delivery id>
KBlock-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256>| Header | Meaning |
|---|---|
KBlock-Event-Id | The event's ID, the same for every endpoint and every attempt. Deduplicate on it. |
KBlock-Delivery-Id | This endpoint's delivery of the event, the same for every attempt of it. |
KBlock-Signature | The timestamp and one or more signatures (see below). |
The body is a JSON object with the same five members for every event type. For example, a receipt.created event:
{
"id": "<event id>",
"type": "receipt.created",
"created_at": "2026-01-01T12:00:00.000Z",
"organization_id": "<organization id>",
"data": {
"receipt_id": "<receipt id>",
"protected_action_id": "<protected action id>",
"receipt_object_id": "<receipt object id>",
"thread_id": "<thread id>",
"outcome": "succeeded",
"bundle_sha256": "<sha-256 of the .kbundle>"
}
}created_at is when the event happened, not when this attempt was sent. Payloads never contain secrets.
Verify the signature
Every delivery is signed with the endpoint's signing secret (whsec_…, shown once when you create the endpoint or rotate the secret):
KBlock-Signature: t=<unix seconds>,v1=<hex>[,v1=<hex>]
v1 = hex( HMAC-SHA256( key = the whole secret string, including "whsec_",
message = "<t>." followed by the exact raw request body ) )To accept a delivery, your receiver must:
- Read the raw body bytes before any JSON parsing. Re-serializing parsed JSON changes the bytes and breaks the signature.
- Parse the header: exactly one
t, one or morev1. - Refuse the delivery if
tis more than 5 minutes away from your clock. Every attempt is signed at the time it is sent, so retries pass this check. - Compute the HMAC and compare it, in constant time, with each
v1. Accept if one matches. - Deduplicate on
KBlock-Event-Id, then answer2xxquickly and process the event afterwards.
A receiver in TypeScript for Node.js, using only node:crypto:
import { createHmac, timingSafeEqual } from "node:crypto";
/**
* Checks a K-Block webhook delivery. `rawBody` must be the exact bytes received,
* `secret` the endpoint's signing secret (whsec_…).
*/
export function verifyKBlockSignature(header: string | undefined, rawBody: Buffer, secret: string, toleranceSeconds = 300): boolean {
if (header === undefined) return false;
let timestamp: number | undefined;
const signatures: Buffer[] = [];
for (const part of header.split(",")) {
const eq = part.indexOf("=");
if (eq <= 0) return false;
const key = part.slice(0, eq).trim();
const value = part.slice(eq + 1).trim();
if (key === "t") {
if (timestamp !== undefined || !/^\d{1,15}$/.test(value)) return false;
timestamp = Number(value);
} else if (key === "v1") {
if (!/^[0-9a-f]{64}$/.test(value)) return false;
signatures.push(Buffer.from(value, "hex"));
}
}
if (timestamp === undefined || signatures.length === 0) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest();
let matched = false;
for (const signature of signatures) matched = timingSafeEqual(signature, expected) || matched;
return matched;
}To try your receiver, select Send test event on the endpoint's page. K-Block sends a webhook.test event to that endpoint only, signed like every other delivery.
Retries and permanent failure
A failed attempt is retried with exponential backoff: the delay after attempt n is 30 seconds × 2n−1, at most 6 hours.
| After failed attempt | Next attempt in |
|---|---|
| 1 | 30 seconds |
| 2 | 1 minute |
| 3 | 2 minutes |
| 4 | 4 minutes |
| 5 | 8 minutes |
| 6 | 16 minutes |
| 7 | 32 minutes |
| 8 | none: the delivery is Failed |
So a receiver that stays down gets its last attempt a little more than an hour after the first, and the delivery is then failed for good. Every attempt is kept in the endpoint's delivery log (Deliveries on the endpoint's page) with its time, HTTP status or error, and duration.
K-Block never disables an endpoint because its deliveries fail: every new event gets its own 8 attempts. Watch the endpoint's delivery log to notice a receiver that keeps failing.
Redelivery
A Failed delivery can be queued again: select Redeliver in the endpoint's delivery log, or call:
POST /api/v1/orgs/<org-id>/webhooks/<endpoint-id>/deliveries/<delivery-id>/redeliverThe delivery goes back to Pending with 8 new attempts; its earlier attempts stay in the log. It is sent with the same event ID, delivery ID and body, signed with the endpoint's current secret (and, within 24 hours of a rotation, also the previous one). Only failed deliveries can be redelivered (others answer 409 INVALID_STATE), only while the endpoint is enabled (409 ENDPOINT_DISABLED). Test events and redeliveries together are limited to 5 per minute per endpoint (429 RATE_LIMITED).
Like every webhook route, this one needs a signed-in owner, admin or developer: management API keys cannot manage webhooks.
Disabled endpoints
While an endpoint is disabled:
- events that happen are not queued for it, and are never sent to it later;
- deliveries that were already pending wait, and are sent when you enable it again;
- redeliveries and test events are refused with
409 ENDPOINT_DISABLED; the dashboard disables Send test event and does not offer Redeliver.
K-Block disables endpoints by itself when the member who created an endpoint is removed, leaves, or gets a role that cannot manage webhooks (approver or viewer), and when K-Block deletes that person's account (accounts cannot be deleted from the dashboard or through the Cloud API). The audit log records this as webhook.updated with a reason. An owner, admin or developer can enable the endpoint again. Deleting an endpoint deletes all of its deliveries, pending or not, and its delivery log.
Secret rotation
Rotating a secret (Rotate secret, or PATCH with "rotate_secret": true) returns the new secret once. For the next 24 hours every delivery carries two signatures, v1=<new> and v1=<previous>, so a receiver that still has the old secret keeps verifying while you deploy the new one. After 24 hours only the new secret signs.
Only one previous secret is kept: if you rotate again within the 24 hours, the secret from before the first rotation stops signing at once.
Addresses an endpoint may use
The Cloud API checks an endpoint's URL when you create or change it, and checks the address again on every connection:
https://only, at most 2,048 characters, with no user name or password and no fragment (#…).- The host name is resolved and every address it resolves to must be public. Loopback, private (RFC 1918), link-local (including the cloud metadata address
169.254.169.254), carrier-grade NAT, multicast, documentation and other special-purpose ranges are refused, for IPv4 and IPv6 alike; IPv6 forms that embed an IPv4 address are judged by that address. A refused URL, or a host name that does not resolve, answers422 URL_NOT_ALLOWED. - Redirects are never followed. A
3xxanswer is a failed attempt ("redirects are not followed"), so point the endpoint at its final URL. - Keep your answer's body small; its content is ignored. An answer whose
Content-Lengthis over 64 KiB is a failed attempt (RESPONSE_TOO_LARGE), even with a2xxstatus.
K-Block delivers only to public https:// addresses. A receiver that can be reached only on a private network or only over http:// cannot receive deliveries: make it reachable at a public https:// address and point the endpoint there.
What is not sent as a webhook
Some changes are recorded in the audit log but have no webhook event in this version: cancelling an action (only its receipt.created is sent, with outcome final), an approved action expiring before it ran, an approved action being superseded, the start of an execution, and changes to keys, delegations and members.
K-Block does not delete webhook events. Deliveries and their attempts are deleted only together with their endpoint.