Search documentation

Search documentation

Webhook Security

Every tool call from Pillar Cloud includes an X-Pillar-Signature header. The SDK verifies this signature automatically — if verification fails, the request is rejected with a 401 status.

How signatures work

Pillar Cloud signs each request using your secret and HMAC-SHA256:

  1. A Unix timestamp is generated
  2. The signing payload is constructed as {timestamp}.{raw_body}
  3. An HMAC-SHA256 digest is computed using your secret
  4. The header is set to t={timestamp},v1={hex_digest}

The SDK verifies this on every incoming request. If the signature is missing, invalid, or expired, the request is rejected.

Signature format

X-Pillar-Signature: t=1710000000,v1=5d41402abc4b2a76b9719d911017c592...
ComponentDescription
tUnix timestamp (seconds) when the request was signed
v1HMAC-SHA256 hex digest of {timestamp}.{raw_body}

Tolerance window

By default, signatures are valid for 300 seconds (5 minutes). Requests with timestamps outside this window are rejected to prevent replay attacks.

Automatic verification

When you create a Pillar instance with a secret, all incoming requests are verified automatically:

typescript
// TypeScript — signature verification is automatic
const pillar = new Pillar({
secret: process.env.PILLAR_SECRET!,
});
python
# Python — signature verification is automatic
pillar = Pillar(secret="plr_...")

The only exception is ping requests, which are not signed and are used for health checks during setup.

Manual verification

If you need to verify signatures outside of the SDK (e.g., in a middleware or a custom integration), you can use the verifyWebhookSignature function:

typescript
import { verifyWebhookSignature } from '@pillar-ai/server';
const isValid = verifyWebhookSignature(
request.headers['x-pillar-signature'],
rawBody,
process.env.PILLAR_SECRET!,
300, // tolerance in seconds (optional, defaults to 300)
);

In Python, the verification is handled internally by pillar.handle(). For manual verification, compute the HMAC yourself:

python
import hmac
import hashlib
import time
def verify_signature(signature_header: str, body: bytes, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in signature_header.split(","))
timestamp = parts["t"]
expected = parts["v1"]
if abs(time.time() - int(timestamp)) > tolerance:
return False
payload = f"{timestamp}.".encode() + body
digest = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(digest, expected)

Error handling

When signature verification fails, the SDK returns:

ScenarioStatusResponse
Missing signature header401{ "error": "Missing signature" }
Invalid or tampered signature401{ "error": "Invalid signature" }
Expired timestamp401{ "error": "Invalid signature" }

Next steps