Agent skill
Schematic Webhooks Skill
Receive and verify Schematic webhooks. Use when setting up Schematic webhook handlers, debugging X-Schematic-Webhook-Signature verification, or handling company.created, plan.created, flag.updated, subscription.trial.ended and entitlement.limit.reached events.
Install this skill
npx skills add hookdeck/webhook-skills --skill schematic-webhooks
When to Use This Skill
- How do I receive Schematic webhooks?
- How do I verify the
X-Schematic-Webhook-Signatureheader? - Why is my Schematic webhook signature verification failing?
- How do I handle
company.created,entitlement.limit.reachedorsubscription.trial.endedevents? - How do I deduplicate Schematic webhook retries?
Verification (core)
Schematic sends the bare lowercase hex digest (no prefix) in X-Schematic-Webhook-Signature:
HMAC-SHA256(secret, rawBody + "+" + X-Schematic-Webhook-Timestamp) → hex
Build the signed message as bytes: raw body, then a literal +, then the timestamp header string exactly as received (latin1, never trimmed or reformatted). The key is the dashboard secret's UTF-8 bytes, used as-is. Validate the signature is exactly 64 hex characters before decoding; hex is case-insensitive. X-Schematic-Webhook-Signature-Version (v1) is not signed.
Node:
const crypto = require('crypto');
function verify(rawBody, signature, timestamp, secret) {
if (!signature || !timestamp || !/^[0-9a-fA-F]{64}$/.test(signature)) return false;
const signed = Buffer.concat([rawBody, Buffer.from('+'), Buffer.from(timestamp, 'latin1')]);
const expected = crypto.createHmac('sha256', Buffer.from(secret, 'utf8')).update(signed).digest();
const provided = Buffer.from(signature, 'hex');
return provided.length === expected.length && crypto.timingSafeEqual(provided, expected);
}
Python:
import hashlib, hmac, re
def verify(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool:
if not signature or not timestamp or not re.fullmatch(r"[0-9a-fA-F]{64}", signature):
return False
signed = b"".join([raw_body, b"+", timestamp.encode("latin-1")])
expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).digest()
return hmac.compare_digest(bytes.fromhex(signature), expected)
Read the raw body first (express.raw({ type: '*/*' }), await request.arrayBuffer(), await request.body()) and parse JSON only after the check. A missing/malformed signature or timestamp, or a mismatch, returns 401.
Timestamp window is off by default. Hookdeck retries and replays resend the original headers and timestamp, and Hookdeck applies no tolerance, so a fixed window would reject legitimate retries. The examples offer an opt-in SCHEMATIC_ (unset or 0 = off); X-Schematic-Webhook-Timestamp is Unix seconds.
Schematic's SDKs also ship verify helpers (for example verifyWebhookSignature in @schematichq/schematic-typescript-node); the examples verify by hand so the algorithm stays visible and works in every framework.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Envelope
Every delivery is a POST with a JSON body. Dispatch on action:
{ "action": "company.created", "account_id": "acct_...", "environment_id": "env_...",
"event_id": "whe_...", "object_type": "company", "body": {}, "company_id": "comp_...", "feature_id": "feat_..." }
event_id (also X-Schematic-Webhook-Id) is stable across retries, so deduplicate on it. Dashboard test sends carry neither.
Common Event Types
| Event | Description |
|---|---|
company.created / company.updated / company.deleted | Company lifecycle |
company.plan_changed | A company changed plan |
user.created | User created (also user.updated, user.deleted) |
plan.created | Plan created (also plan.updated, plan.deleted) |
flag.updated | Flag changed (also flag.created, flag.deleted, flag_rules.updated) |
subscription.trial.ended | A subscription's trial ended |
entitlement.limit.reached | A company hit a feature usage limit (also .warning, soft and tier variants) |
credit.purchase.success | Credits purchased (also auto.topup.success, auto.topup.hard.failure) |
All 39 events are listed in references/overview.md.
Important Headers
| Header | Description |
|---|---|
X-Schematic-Webhook-Signature | Hex HMAC-SHA256 digest |
X-Schematic-Webhook-Timestamp | Unix seconds when this attempt was sent (signed) |
X-Schematic-Webhook-Signature-Version | v1 (not signed) |
X-Schematic-Webhook-Id | Same as event_id; absent on test sends |
X-Schematic-Webhook-Attempt | Attempt number, from 1 |
Environment Variables
SCHEMATIC_WEBHOOK_SECRET=your_schematic_webhook_secret # Shown in the Schematic dashboard when you create the endpoint
SCHEMATIC_WEBHOOK_TOLERANCE_SECONDS=0 # Optional replay window; unset or 0 = off
Delivery
Respond with any 2xx within 5 seconds (anything else, 3xx included, counts as failed). Schematic makes eight attempts over about 24 hours, then drops the event. Endpoints with a failure rate of 90% or more across at least 25 events in 24 hours are deactivated. The timestamp and signature are fresh on every attempt, so verify each request on its own.
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 schematic --path /webhooks/schematic
Reference Materials
- references/overview.md - Schematic webhook concepts and all events
- references/setup.md - Configuration guide
- references/verification.md - Signature verification details and gotchas