Agent skill
Castle Webhooks Skill
Receive and verify Castle webhooks. Use when setting up Castle fraud and account-security webhook handlers, debugging X-Castle-Signature verification, or handling events like $policy.matched, $list_item.created, and $list_item.archived.
Install this skill
npx skills add hookdeck/webhook-skills --skill castle-webhooks
When to Use This Skill
- How do I receive Castle webhooks?
- How do I verify the
X-Castle-Signatureheader? - Why is my Castle webhook signature verification failing?
- How do I handle
$policy.matchedor$list_item.createdevents? - Automating suspend/review actions when a Castle policy or list triggers
Verification (core)
Castle signs the raw JSON request body with HMAC-SHA256, keyed with your Castle API secret (there is no per-webhook secret), and sends the digest base64-encoded in X-Castle-Signature. There is no timestamp and no prefix. Verification is optional in Castle's docs but strongly recommended.
const crypto = require('crypto');
function verifyCastleSignature(rawBody, signatureHeader, apiSecret) {
if (!signatureHeader) return false;
const expected = crypto.createHmac('sha256', apiSecret).update(rawBody).digest('base64');
try {
return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
} catch {
return false; // different lengths
}
}
Python:
import base64, hashlib, hmac
def verify_castle_signature(raw_body: bytes, signature: str, api_secret: str) -> bool:
expected = base64.b64encode(
hmac.new(api_secret.encode(), raw_body, hashlib.sha256).digest()
).decode()
return hmac.compare_digest(signature, expected)
The event name is in the JSON body's type field (not a header). Respond with any 2xx; non-2xx responses are retried up to six more times with exponential backoff.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Common Event Types
type | Description |
|---|---|
$policy.matched | The conditions of a policy matched (or a policy returned a challenge/deny action) |
$list_item.created | A new item was added to a List (active by default) |
$list_item.archived | A list item was archived (removed from the list) |
$list_item.unarchived | A previously archived list item is active again |
Payload envelope: api_version, app_id, created_at, type, data. See references/overview.md.
Environment Variables
CASTLE_API_SECRET=your_castle_api_secret # Castle dashboard → Settings → API keys
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 castle --path /webhooks/castle
Reference Materials
- references/overview.md - Castle webhook concepts and events
- references/setup.md - Dashboard configuration
- references/verification.md - Signature verification details and gotchas