Webhooks
Receive asynchronous order results and monitor events securely and verify that each delivery came from Kausate.
Kausate sends completed asynchronous order results and monitor events to each active webhook registered for your organization. Every registered webhook has its own signing secret, and every delivery is signed over the exact request body.
Tell the two apart by the body: a monitor event has
"event": "monitor.change_detected", an order result has an orderId.
Create a webhook
curl -X POST https://api.kausate.com/v2/webhooks \
-H "X-API-Key: your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Order Results",
"url": "https://your-server.com/webhooks/kausate"
}'The response includes a signingSecret beginning with kwhsec_. Store it as
a secret and use it only on the server receiving the webhook.
{
"id": "4cfdf18b-6075-45ac-98bd-9ef0a41bb563",
"name": "Order Results",
"url": "https://your-server.com/webhooks/kausate",
"status": "active",
"signingSecret": "kwhsec_<base64-encoded-secret>"
}See Create Webhook for the complete request and response schema.
Choose a payload version
Set payloadVersion when creating or updating a webhook to send that endpoint
a specific supported API version, independently of other webhooks in the same
organization:
{
"name": "Legacy Consumer",
"url": "https://your-server.com/webhooks/legacy",
"payloadVersion": "2025-04-01"
}Kausate converts the payload before signing it and sets the outgoing
Kausate-Version header to the payload version. Changing a custom header does
not convert the payload. Use custom headers such as X-Consumer for proxy
routing.
Set payloadVersion to null to follow the originating request's version
again. Without an override, deliveries use that request version, the
organization's default, or the latest version when neither is set. The older
apiVersion configuration field remains informational; use payloadVersion
to control delivery.
Existing webhooks have no override. A delivery keeps its version setting across retries. Resending an individual delivery preserves its original version; resending an entire order uses each endpoint's current override. Test sends use the version selected in the webhook form, or the organization default.
If a payload version is no longer supported or conversion fails, Kausate withholds delivery and records an error instead of sending a different schema. Choose a supported version in the webhook settings to resume new deliveries.
Signed delivery headers
Each signed request contains these server-controlled headers:
| Header | Meaning |
|---|---|
Kausate-Webhook-Id | The ID of this delivery. It is stable across delivery retries. |
Kausate-Webhook-Timestamp | Unix time in seconds when this HTTP attempt was created. A retry gets a new timestamp. |
Kausate-Webhook-Signature | Space-delimited v1,<base64> HMAC-SHA256 signatures. Usually one; two during rotation. |
Kausate-Version | The API version used for the webhook payload. Monitor events always name the current one. |
The webhook ID is not the webhook configuration ID returned by
POST /v2/webhooks, and it is not just the order ID. For a normal order-result
delivery it has this structure:
{orderId}-webhook-{webhookConfigurationId}All retry attempts for a delivery keep the same ID. A later re-delivery of a
cached result gets a new one. A monitor event's delivery ID names the event and
the delivery instead of an order; deduplicate monitor events on the event_id
in the body.
The timestamp is the time of the HTTP attempt, not of the order, so a receiver can reject stale requests even when a delivery is retried much later.
Verify a signature
Verify the signature before parsing or acting on the payload:
- Read the raw request body as bytes. Do not re-serialize parsed JSON.
- Read
Kausate-Webhook-Id,Kausate-Webhook-Timestamp, andKausate-Webhook-Signature. - Reject timestamps outside your allowed clock-skew window. Five minutes is a reasonable default.
- Remove the
kwhsec_prefix fromsigningSecretand base64-decode the remainder. - Build the signed bytes as
<id>.<timestamp>.<raw-request-body>. - Calculate HMAC-SHA256, then compare it with the base64-decoded
v1signature using a constant-time comparison.
import base64
import binascii
import hashlib
import hmac
import time
from collections.abc import Mapping
def verify_kausate_webhook(
raw_body: bytes,
headers: Mapping[str, str],
signing_secret: str,
*,
tolerance_seconds: int = 300,
) -> bool:
if not signing_secret.startswith("kwhsec_"):
return False
try:
message_id = headers["Kausate-Webhook-Id"]
timestamp_text = headers["Kausate-Webhook-Timestamp"]
signature_header = headers["Kausate-Webhook-Signature"]
timestamp = int(timestamp_text)
timestamp_bytes = timestamp_text.encode("ascii")
secret = base64.b64decode(
signing_secret.removeprefix("kwhsec_"),
validate=True,
)
except (KeyError, binascii.Error, ValueError, TypeError):
return False
if abs(int(time.time()) - timestamp) > tolerance_seconds:
return False
if not secret:
return False
signed_content = (
message_id.encode("utf-8")
+ b"."
+ timestamp_bytes
+ b"."
+ raw_body
)
expected_digest = hmac.new(
secret,
signed_content,
hashlib.sha256,
).digest()
for signature in signature_header.split():
try:
version, encoded_digest = signature.split(",", 1)
provided_digest = base64.b64decode(encoded_digest, validate=True)
except (binascii.Error, ValueError):
continue
if version == "v1" and hmac.compare_digest(expected_digest, provided_digest):
return True
return FalseFramework helpers such as request.json() may consume or transform the body.
Capture the original bytes first and use those same bytes for verification.
After a successful signature check, use Kausate-Webhook-Id as an idempotency
key. Return a 2xx response only after the delivery is durably accepted.
Rotate a signing secret
Rotate a webhook independently if its signing secret is exposed or as part of your regular secret-management policy:
curl -X POST https://api.kausate.com/v2/webhooks/4cfdf18b-6075-45ac-98bd-9ef0a41bb563/secret/rotate \
-H "X-API-Key: your_api_key" \
-H "Kausate-Version: 2026-05-01"The response contains the new secret and the fixed expiry of the previous secret:
{
"signingSecret": "kwhsec_<new-base64-encoded-secret>",
"previousSecretExpiresAt": "2026-07-30T12:00:00Z"
}Update your receiver to use the new secret before that timestamp. For exactly
24 hours after rotation, Kausate includes signatures made with both the new and
previous secrets in the same Kausate-Webhook-Signature header. Verification
succeeds when any one of the space-delimited signatures matches. After the
overlap expires, only the new secret is used automatically.
Only one rotation can be active for an endpoint. Starting another rotation
before the 24-hour overlap expires returns 409 Conflict and leaves both
active secrets unchanged.
Outbound IP addresses
If your firewall requires source-IP allowlisting, allow every production address below. A webhook can originate from any availability zone.
| Environment | Outbound IPv4 address |
|---|---|
| Production | 3.65.43.249 |
| Production | 63.181.68.138 |
| Production | 3.74.212.143 |
| Staging | 3.127.78.117 |
Retry behavior
For new order-result delivery submissions, including cached-order deliveries and manual resends, Kausate uses this exponential backoff schedule:
| Setting | Value |
|---|---|
| Maximum attempts | 10 total by default: the initial request plus 9 retries |
| Initial delay | 10 minutes after the first failed request |
| Backoff multiplier | 2× after each failed request |
| Maximum delay | 12 hours between attempts |
| Overall deadline | 48 hours from submission, including queueing and attempts |
The nine retry delays are 10m, 20m, 40m, 1h20m, 2h40m, 5h20m, 10h40m, 12h, and 12h. They add up to 45 hours 10 minutes, plus time spent on delivery attempts and queueing. A hard 48-hour schedule-to-close timeout ends the delivery even if attempts remain. Each HTTP request has a 30-second timeout; the per-attempt activity timeout is 1 minute.
Any 2xx response completes delivery. Non-2xx responses (including 3xx,
4xx, and 5xx), network failures, and request timeouts are retried within the
attempt limit and overall deadline. A Retry-After response header does not
change the schedule. Permanent delivery errors are not retried.
For a development endpoint that may be unavailable, set Delivery attempts to
One attempt (for development) when creating or editing a webhook in the
dashboard, or set maxAttempts to 1 through the webhook API. This makes one
request with no retries. The limit includes the first delivery request and
applies to new deliveries.
The new scheduling policy applies to new delivery submissions; deliveries already submitted keep their original policy. Monitor events use a separate delivery schedule.
Retries keep the same Kausate-Webhook-Id, but receive a fresh
Kausate-Webhook-Timestamp and signature. Your endpoint should therefore
deduplicate only after signature and timestamp verification succeed.
Last updated on