# 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_limit.reached`, `entitlement.soft_limit.warning`, `entitlement.tier_limit.reached`, `entitlement.tier_limit.warning` |
| 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:

```json
{
  "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

1. Open the webhooks page in the Schematic dashboard and create an endpoint with your receiver's URL.
2. Choose the events the endpoint should receive.
3. Copy the webhook secret shown when you create the endpoint and store it, for example as `SCHEMATIC_WEBHOOK_SECRET`.
4. 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.

```javascript
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:

```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](/event-gateway) verifies the `X-Schematic-Webhook-Signature` header, 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](/docs/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 `failure` and 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](/docs/use-cases/receive-webhooks#replay-for-debugging-and-recovery) 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 `body` before 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_id` as 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](/webhooks/guides/implement-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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## 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](https://hookdeck.com) sits in front of your receiver, checks every Schematic signature, and holds each event until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Schematic webhooks reliably in minutes.