Guide to Schematic Webhooks: Features and Best Practices
Schematic webhooks tell your systems when something changes in your pricing and entitlements setup: a company is created or changes plan, a flag or plan is edited, a trial ends, or a customer hits a usage limit. Schematic manages plans, entitlements, feature flags and credits for software products, and webhooks let you keep billing, CRM and alerting tools in step with it.
This guide covers how Schematic webhooks are structured and signed, the events you can subscribe to, how Schematic retries and deactivates endpoints, and the practices that keep a Schematic receiver reliable.
What are Schematic webhooks?
You add webhook endpoints on the webhooks page of the Schematic dashboard and choose the events each one receives. Every delivery is a POST with a JSON envelope: the event name in action, the account and environment, an event_id, the object_type, and the event's data in body.
Each request carries X-Schematic-Webhook-Signature, a hex HMAC-SHA256 of the raw body, a literal +, and the X-Schematic-Webhook-Timestamp header. The key is the webhook secret the dashboard shows when you create the endpoint.
Schematic webhook features
| Feature | Details |
|---|---|
| Configuration | Webhooks page in the Schematic dashboard |
| Method | POST with a JSON body |
| Signature header | X-Schematic-Webhook-Signature, hex |
| Timestamp header | X-Schematic-Webhook-Timestamp, Unix seconds, new on each attempt |
| Version header | X-Schematic-Webhook-Signature-Version, currently v1 and not signed |
| Scheme | HMAC-SHA256 |
| Signed content | <raw body>+<timestamp> |
| Secret | Shown in the dashboard when you create the endpoint |
| Event ID | event_id in the body and X-Schematic-Webhook-Id, stable across retries |
| Response timeout | 5 seconds; any non-2xx response, 3xx included, is a failure |
| Retries | Eight attempts over about 24 hours, with ±10% jitter |
| Auto-deactivation | A 90% failure rate across at least 25 events in a rolling 24 hours |
| Delivery log | One row per event and endpoint in the dashboard |
Common events
Schematic lists 39 events:
| Area | Events |
|---|---|
| Companies | company.created, company.updated, company.deleted, company.plan_changed |
| Overrides | company.override.created, company.override.updated, company.override.deleted, company.override.expired |
| Users | user.created, user.updated, user.deleted |
| Features and flags | feature.created, feature.updated, feature.deleted, flag.created, flag.updated, flag.deleted, flag_rules.updated, rule.deleted |
| Plans | plan.created, plan.updated, plan.deleted, plan_version.deleted, plan.entitlement.created, plan.entitlement.updated, plan.entitlement.deleted |
| Billing | subscription.trial.ended |
| Entitlement triggers | entitlement.limit.reached, entitlement.limit.warning, entitlement.soft_, entitlement.soft_, entitlement.tier_, entitlement.tier_ |
| Credits | credit.limit.reached, credit.limit.warning, credit.purchase.success, auto.topup.success, auto.topup.hard.failure, auto.topup.retry.exceeded |
Each event's body matches a typed object in Schematic's SDKs, such as CompanyDetailResponseData for company events and WebFeatureUsageWebhookOutput for entitlement triggers. Every delivery shares the same envelope:
{
"action": "<webhook.name>",
"account_id": "acct_xxxxxxx",
"environment_id": "env_xxxxxxxx",
"event_id": "whe_xxxxxxxx",
"object_type": "<object>",
"body": {},
"company_id": "comp_xxxxxxxx",
"feature_id": "feat_xxxxxxxx"
}
company_id appears on company-scoped and entitlement trigger events, and feature_id on entitlement triggers only. Route on action.
Setting up Schematic webhooks
- Open the webhooks page in the Schematic dashboard and create an endpoint with your receiver's URL.
- Choose the events the endpoint should receive.
- Copy the webhook secret shown when you create the endpoint and store it, for example as
SCHEMATIC_WEBHOOK_SECRET. - Send a test from the dashboard. Test sends go out once, don't appear in the log, and carry no
event_id.
With Hookdeck. Schematic is a supported source type in Hookdeck. Create a Hookdeck source of the Schematic type, paste the webhook secret, and use the Hookdeck source URL as the endpoint URL in Schematic. The source accepts POST only and checks X-Schematic-Webhook-Signature against <raw body>+<X-Schematic-Webhook-Timestamp>. A request that fails the check still gets a 200 from Hookdeck, but Hookdeck marks it as failed verification and doesn't deliver it to your service.
Securing Schematic webhooks
Join the raw body bytes, a literal +, and the X-Schematic-Webhook-Timestamp header. The body comes first, which is the reverse of most timestamped schemes. Compute an HMAC-SHA256 with the secret as a UTF-8 string and compare it with X-Schematic-Webhook-Signature in constant time.
const crypto = require('crypto');
const express = require('express');
function verifySchematicSignature(rawBody, signature, timestamp, secret) {
if (!signature || !timestamp || !secret) return false;
if (!/^[0-9a-f]{64}$/i.test(signature)) return false; // validate before decoding
const signed = Buffer.concat([rawBody, Buffer.from(`+${timestamp}`, 'latin1')]);
const expected = crypto.createHmac('sha256', secret).update(signed).digest();
const received = Buffer.from(signature, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
// Optional replay check, off by default (0). Hookdeck retries and replays resend the original timestamp.
const TOLERANCE_SECONDS = Number(process.env.SCHEMATIC_WEBHOOK_TOLERANCE_SECONDS || 0);
const app = express();
app.post('/webhooks/schematic', express.raw({ type: '*/*' }), (req, res) => {
const secret = process.env.SCHEMATIC_WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500); // fail closed
const timestamp = req.get('x-schematic-webhook-timestamp');
if (!verifySchematicSignature(req.body, req.get('x-schematic-webhook-signature'), timestamp, secret)) {
return res.sendStatus(401);
}
if (TOLERANCE_SECONDS > 0 && Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
// Deduplicate on event.event_id, route on event.action, then process asynchronously.
res.sendStatus(200);
});
The same check in Python:
import hashlib
import hmac
import re
HEX_64 = re.compile(r"[0-9a-fA-F]{64}")
def verify_schematic_signature(raw_body: bytes, signature, timestamp, secret: str) -> bool:
if not signature or not timestamp or not secret:
return False
if not HEX_64.fullmatch(signature):
return False
signed = 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)
Schematic's SDKs ship helpers that do the same job, such as verifyWebhookSignature in @schematichq/schematic-typescript-node and verify_webhook_signature in the Python SDK. The Node helper checks the signature and nothing more: it applies no timestamp window, so it also works on events Hookdeck retries or replays. Schematic doesn't recommend a replay window either. If you add one, remember that Hookdeck applies no timestamp tolerance and its retries and replays resend the original headers.
Make Schematic webhooks production-ready. Hookdeck Event Gateway verifies the
X-Schematic-Webhook-Signatureheader, answers Schematic well inside its 5-second timeout, and keeps your endpoint clear of Schematic's auto-deactivation threshold.
Schematic webhook limitations and pain points
A 5-second deadline
The Problem: Handlers that sync data to other systems before responding time out, and Schematic treats the delivery as failed.
Why It Happens: Schematic expects a 2xx within 5 seconds and counts any other status, 3xx included, as a failure. Its docs warn that "a handler that processes the event before responding will time out under load."
Workarounds:
- Verify, store the event and return
2xx, then do the work in a background job. - Don't redirect webhook requests; serve them on the exact URL you registered.
How Hookdeck Can Help: Hookdeck acknowledges Schematic straight away and delivers to your service with its own timeout and automatic retries, so slow downstream work no longer counts against Schematic's deadline.
Endpoints are deactivated after sustained failures
The Problem: An outage or a bad deploy turns into a deactivated endpoint, and events stop arriving.
Why It Happens: Schematic deactivates an endpoint at a failure rate of 90% or more across at least 25 events in a rolling 24-hour window. A delivery counts towards that rate once it has failed three attempts, about half an hour in. Disabling an endpoint also stops retries in flight, and re-enabling it doesn't resume them.
Workarounds:
- Alert on failures in your own logs before the threshold is reached.
- After re-enabling, check the dashboard log for events that ended in
failureand reconcile them from the API.
How Hookdeck Can Help: Hookdeck keeps answering Schematic while your service is down and holds every event, so your endpoint stays healthy and you can replay the backlog once your service recovers.
Retries can arrive out of order
The Problem: An older update overwrites a newer one.
Why It Happens: Retries run over about 24 hours, and Schematic notes that "a retry can also land after newer events for the same object."
Workarounds:
- Compare the timestamps inside
bodybefore applying an update. - For state that must be correct, fetch the current object from the Schematic API.
How Hookdeck Can Help: Hookdeck records every delivery with its headers, so you can see the order events arrived in and replay one after fixing your handler.
Test sends skip the log and the event ID
The Problem: A handler passes dashboard tests but fails on real events, or a test send breaks deduplication.
Why It Happens: Dashboard test sends go out once, stay out of the webhook log, and carry no event_id or X-Schematic-Webhook-Id.
Workarounds:
- Treat a missing
event_idas a test and skip deduplication for it. - Trigger a real event in a test environment before going live.
How Hookdeck Can Help: Hookdeck stores both test and real requests, so you can inspect each one and replay real events against your handler as often as you need.
Best practices
Verify against the raw body
The signature covers the body exactly as sent. Use express.raw() in Express, await request.arrayBuffer() in a Next.js route handler, and await request.body() in FastAPI. Schematic warns that "parsed or re-serialized bodies will not match the original signature."
Put the body first
The signed string is <raw body>+<timestamp>, not <timestamp>.<body>. If you adapt code written for another provider, swap the order and the separator.
Deduplicate on event_id
event_id and X-Schematic-Webhook-Id stay the same on every attempt for one event and endpoint. Record the IDs you've handled and skip repeats. See our guide to webhook idempotency.
Route on action
Switch on action, and answer 2xx to events you don't handle so they don't count towards deactivation.
Acknowledge fast and process asynchronously
Verify, store the event, return 200, then do the work. See why to process webhooks asynchronously.
Conclusion
Schematic signs every webhook with X-Schematic-Webhook-Signature, a hex HMAC-SHA256 of the raw body, a + and the timestamp. Verify against the raw bytes with the body first, deduplicate on event_id, route on action, and answer within 5 seconds to keep your endpoint active.
Hookdeck Event Gateway sits in front of your receiver, checks every Schematic signature, and holds each event until your service has processed it.
Get started with Hookdeck for free and handle Schematic webhooks reliably in minutes.