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-Signature header?
  • Why is my Castle webhook signature verification failing?
  • How do I handle $policy.matched or $list_item.created events?
  • 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

typeDescription
$policy.matchedThe conditions of a policy matched (or a policy returned a challenge/deny action)
$list_item.createdA new item was added to a List (active by default)
$list_item.archivedA list item was archived (removed from the list)
$list_item.unarchivedA 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 2, 2026

View on GitHub →