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:
- A Unix timestamp is generated
- The signing payload is constructed as
{timestamp}.{raw_body} - An HMAC-SHA256 digest is computed using your secret
- 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...
| Component | Description |
|---|---|
t | Unix timestamp (seconds) when the request was signed |
v1 | HMAC-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 — signature verification is automaticconst pillar = new Pillar({secret: process.env.PILLAR_SECRET!,});
# Python — signature verification is automaticpillar = 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:
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:
import hmacimport hashlibimport timedef 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 Falsepayload = f"{timestamp}.".encode() + bodydigest = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()return hmac.compare_digest(digest, expected)
Error handling
When signature verification fails, the SDK returns:
| Scenario | Status | Response |
|---|---|---|
| Missing signature header | 401 | { "error": "Missing signature" } |
| Invalid or tampered signature | 401 | { "error": "Invalid signature" } |
| Expired timestamp | 401 | { "error": "Invalid signature" } |
Next steps
- Framework Integration — Mount the handler in your framework
- Testing — Generate test signatures for your test suite