Documentation Index
Fetch the complete documentation index at: https://docs.closient.com/llms.txt
Use this file to discover all available pages before exploring further.
Webhooks deliver real-time notifications to your server when events occur in Closient. Each webhook endpoint receives signed HTTP POST requests with JSON payloads.
Creating a Webhook Endpoint
Configure endpoints in Settings > Integrations in your Dashboard, or via the Integrations API.
Each endpoint has:
- URL — your HTTPS endpoint (HTTPS required in production)
- Event types — which events to subscribe to (e.g.,
["offer.updated", "product.recalled"]). Prefix matching is supported: subscribing to "offer" receives all offer.* events.
- Filters — optional per-event-type filtering rules (AND logic between filter keys, OR within a key)
- Signing secret — auto-generated 64-byte hex secret for payload verification
Every webhook delivery is a POST request with a JSON body:
{
"id": "evt_a1b2c3d4e5f6",
"type": "offer.updated",
"created_at": "2026-04-01T12:00:00+00:00",
"data": {
"gtin": "00012345678905",
"offer_id": "ack3p9tw6x7r"
}
}
| Field | Type | Description |
|---|
id | string | Unique event identifier (evt_ prefix). Use for idempotency. |
type | string | Dot-separated event type (e.g., offer.updated). |
created_at | string | ISO 8601 timestamp of when the event was created. |
data | object | Event-specific payload. Structure varies by event type. |
Every delivery includes these headers:
| Header | Example | Description |
|---|
Content-Type | application/json | Always JSON. |
X-Closient-Signature | t=1711972800,v1=5a3c... | HMAC-SHA256 signature for verification. |
X-Closient-Event-Type | offer.updated | The event type (same as type in body). |
X-Closient-Delivery-ID | d290f1ee-6c54-... | Unique delivery attempt ID. |
User-Agent | Closient-Webhooks/1.0 | Identifies Closient as the sender. |
Verifying Signatures
The X-Closient-Signature header uses a Stripe-style format that includes a timestamp for replay protection:
t=1711972800,v1=5a3c9f...
To verify:
- Extract the timestamp (
t) and signature (v1) from the header.
- Construct the signed payload:
{timestamp}.{raw_json_body}.
- Compute HMAC-SHA256 using your signing secret.
- Compare your computed signature with
v1 using a constant-time comparison.
- Check the timestamp is within your tolerance window (recommended: 5 minutes).
import hashlib
import hmac
import time
def verify_closient_webhook(
payload: bytes,
signature_header: str,
secret: str,
tolerance_seconds: int = 300,
) -> bool:
"""Verify a Closient webhook signature.
Args:
payload: Raw request body bytes.
signature_header: Value of X-Closient-Signature header.
secret: Your endpoint's signing secret.
tolerance_seconds: Max age in seconds (default 5 min).
"""
# Parse header: t=123,v1=abc...
parts = {}
for element in signature_header.split(","):
key, _, value = element.partition("=")
parts[key.strip()] = value.strip()
timestamp = parts.get("t", "")
if not timestamp:
return False
# Replay protection
age = abs(int(time.time()) - int(timestamp))
if age > tolerance_seconds:
return False
# Compute expected signature
signed_payload = f"{timestamp}.{payload.decode('utf-8')}"
expected = hmac.new(
secret.encode("utf-8"),
signed_payload.encode("utf-8"),
hashlib.sha256,
).hexdigest()
# Check current signature (v1), then rotated signature (v1old)
return (
hmac.compare_digest(expected, parts.get("v1", ""))
or hmac.compare_digest(expected, parts.get("v1old", ""))
)
const crypto = require("crypto");
function verifyClosientWebhook(payload, signatureHeader, secret, toleranceSeconds = 300) {
// Parse header: t=123,v1=abc...
const parts = {};
for (const element of signatureHeader.split(",")) {
const [key, ...rest] = element.split("=");
parts[key.trim()] = rest.join("=").trim();
}
const timestamp = parts.t;
if (!timestamp) return false;
// Replay protection
const age = Math.abs(Math.floor(Date.now() / 1000) - parseInt(timestamp, 10));
if (age > toleranceSeconds) return false;
// Compute expected signature
const signedPayload = `${timestamp}.${payload}`;
const expected = crypto
.createHmac("sha256", secret)
.update(signedPayload, "utf-8")
.digest("hex");
// Check current signature (v1), then rotated signature (v1old)
const v1Match = crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(parts.v1 || "", "hex").subarray(0, 32)
);
if (v1Match) return true;
if (parts.v1old) {
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(parts.v1old, "hex").subarray(0, 32)
);
}
return false;
}
Always verify the signature before processing a webhook payload. Reject requests with missing, expired, or invalid signatures.
Secret Rotation
When you rotate a signing secret via the API (POST /endpoints/{id}/rotate-secret), the previous secret remains valid for a 24-hour grace period. During this window, deliveries include both signatures:
t=1711972800,v1=<current_secret_sig>,v1old=<previous_secret_sig>
Your verification code should check v1 first, then fall back to v1old. After the grace period, only v1 is included.
Delivery Lifecycle
Each delivery progresses through these statuses:
| Status | Description |
|---|
pending | Queued for delivery. |
delivered | Your endpoint returned a 2xx response. |
failed | Non-2xx response or connection error. Will be retried. |
dead_letter | All retry attempts exhausted. Can be replayed manually. |
Retry Policy
Failed deliveries are retried with exponential backoff and jitter:
| Attempt | Approximate Delay |
|---|
| 1 | Immediate |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 15 minutes |
| 5 | 1 hour |
| 6 | 4 hours |
| 7 | 12 hours |
| 8 | 24 hours |
After 8 attempts, the delivery moves to dead_letter. You can replay dead-lettered deliveries via the API:
POST /integrations/api/v1/webhooks/deliveries/{delivery_id}/replay/
Auto-Disable
If an endpoint has a 100% failure rate over the last 24 hours (with at least one delivery attempt), Closient automatically disables it and sends an email notification. Re-enable it from the Dashboard or API after resolving the issue.
Testing
Send a test event to verify your endpoint is reachable and signature verification works:
POST /integrations/api/v1/webhooks/endpoints/{endpoint_id}/test/
This delivers a test.ping event synchronously and returns the HTTP status code from your endpoint.
Best Practices
- Return 2xx quickly — acknowledge receipt, then process asynchronously. Closient times out after 30 seconds.
- Handle duplicates — use the
id field for idempotency. The same event may be delivered more than once during retries.
- Verify signatures — always validate
X-Closient-Signature before processing. Reject unsigned or expired requests.
- Use HTTPS — required in production. HTTP is only allowed in development.
- Monitor health — check delivery status in the Dashboard or via the Integrations API. Address failures before they accumulate into auto-disable.
See the Integrations API reference for full endpoint management, delivery inspection, and filtering options.