Agent skill
Sublime Security Webhooks Skill
Receive and verify Sublime Security webhooks. Use when setting up Sublime Security webhook Action handlers, debugging X-Sublime-Signature verification (t=...,v0=...), or handling message.flagged, group, and manual email security events.
Install this skill
npx skills add hookdeck/webhook-skills --skill sublime-security-webhooks
When to Use This Skill
- How do I receive Sublime Security webhooks?
- How do I verify the
X-Sublime-Signatureheader (t=...,v0=...)? - How do I handle
message.flagged,group, ormanualevents? - Why is my Sublime Security webhook signature verification failing?
- How do I send flagged email alerts from Sublime to my own service?
Essential Code (USE THIS)
Sublime signs every webhook Action delivery with X-Sublime-Signature:
X-Sublime-Signature: t=1626139320,v0=3d35d777510a7ccf73ded723f141b0ada2986aebe63c4e45a9c7e4bb1abaca06
tis the Unix timestamp (seconds). Each signature is prefixed by a scheme (vplus an integer);v0is the only scheme today. There is no space after the comma.- Each
v0value is the lowercase hex HMAC-SHA256 of"<t>.<raw body>", keyed with the webhook Action's signing secret (Actions → select the webhook Action → click to reveal), used as UTF-8 bytes. - The header can carry several
v0values. Accept the request if any one matches. - Check that each candidate is exactly 64 hex characters before decoding:
Buffer.from(x, 'hex')silently truncates invalid hex. Hex is case-insensitive. - Missing or invalid signature: respond
401. Respond2xxwithin 10 seconds on success.
Verification (Node.js)
const crypto = require('crypto');
// rawBody: Buffer of the exact request bytes (express.raw({ type: '*/*' }), not express.json)
function verifySublime(rawBody, signatureHeader, secret) {
if (!secret || !signatureHeader) return false;
let timestamp;
const signatures = [];
for (const item of signatureHeader.split(',')) {
const i = item.indexOf('=');
if (i <= 0) continue;
const key = item.slice(0, i).trim(), value = item.slice(i + 1).trim();
if (key === 't') timestamp = value;
else if (key === 'v0') signatures.push(value);
}
if (!/^\d+$/.test(timestamp ?? '') || Number(timestamp) <= 0 || signatures.length === 0) return false;
// Optional replay window, off by default: Hookdeck retries and replays keep the original timestamp
const expected = crypto.createHmac('sha256', secret)
.update(`${timestamp}.`).update(rawBody).digest();
return signatures.some((sig) =>
/^[0-9a-fA-F]{64}$/.test(sig) && crypto.timingSafeEqual(Buffer.from(sig, 'hex'), expected));
}
Verification (Python)
import hashlib, hmac, re
def verify_sublime(raw_body: bytes, signature_header, secret: str) -> bool:
if not secret or not signature_header:
return False
timestamp, signatures = None, []
for item in signature_header.split(","):
key, sep, value = item.partition("=")
key, value = key.strip(), value.strip()
if not sep or not key:
continue
if key == "t":
timestamp = value
elif key == "v0":
signatures.append(value)
if not timestamp or not re.fullmatch(r"[0-9]+", timestamp) or int(timestamp) <= 0 or not signatures:
return False
expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return any(re.fullmatch(r"[0-9a-fA-F]{64}", s) and hmac.compare_digest(s.lower().encode(), expected.encode())
for s in signatures)
Replay window (opt-in)
The examples do not enforce a timestamp window by default. Sublime's own retries arrive with a new timestamp and signature, but Hookdeck retries and replays resend the original request with the original t, so a window would reject late Hookdeck redeliveries. To opt in, set SUBLIME_ to a positive whole number; any other value leaves it off, with a warning at startup. Use the body's id for idempotency instead.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Common Event Types
The body is JSON: { id, api_. Dispatch on type, which says why the delivery was sent. data holds message, flagged_rules, and triggered_actions.
type | Description |
|---|---|
message.flagged | A message matched a detection rule and the webhook Action fired |
group | The Action fired for a message group |
manual | A user sent the message to the webhook Action by hand |
type_extended may list several reasons at once and adds group_late_arriving. See references/overview.md.
For the full payload reference, see Sublime's webhook documentation.
Important Headers
| Header | Description |
|---|---|
X-Sublime-Signature | t=<unix seconds>,v0=<hex>[,v0=<hex>...] |
Environment Variables
SUBLIME_SECURITY_WEBHOOK_SECRET=your_webhook_action_signing_secret # Actions → webhook Action → click to reveal
SUBLIME_SECURITY_TIMESTAMP_TOLERANCE_SECONDS=0 # optional; a positive value opts in to the replay window
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 sublime-security --path /webhooks/sublime-security
Reference Materials
- references/overview.md - Sublime webhook Actions, event types, payload structure
- references/setup.md - Creating a webhook Action and finding the signing secret
- references/verification.md - Signature verification details and gotchas