Webhooks
Receive asynchronous order results securely and verify that each delivery came from Kausate.
Kausate sends completed asynchronous order results 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.
This page covers order-result webhooks registered through /v2/webhooks.
Monitor callbacks configured with webhookUrl have different security
semantics.
Create a webhook
curl -X POST https://api.staging.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.
Signed delivery headers
Each signed request contains these server-controlled headers:
| Header | Meaning |
|---|---|
Kausate-Webhook-Id | The Temporal workflow ID for this logical 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. |
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}Kausate uses this exact ID for the SendWebhook child workflow in Temporal.
This makes a delivery directly traceable in our workflow history. All retry
attempts for that logical delivery keep the same ID. A later re-delivery of a
cached result gets a new delivery ID.
The timestamp represents the outbound HTTP attempt, not the time the order started or completed. This is intentional: it lets your receiver 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.staging.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
Kausate retries non-2xx responses and network failures with exponential
backoff:
| Setting | Value |
|---|---|
| Maximum attempts | 50 |
| Initial interval | 1 second |
| Maximum interval | 4 hours |
| Backoff multiplier | 2× |
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