Webhooks
Legalize sends a signed HTTP POST to your endpoint the moment a
law is created, updated, or repealed. Signatures are constant-time
HMAC-SHA256, delivery is retried on failure, and the SDKs ship a
one-line verifier.
Create an endpoint
endpoint = client.webhooks.create(
url="https://yourapp.example/hooks/legalize",
event_types=["law.updated", "reform.created"],
description="Prod receiver",
)
print(endpoint.id, endpoint.secret)
const endpoint = await client.webhooks.create({
url: "https://yourapp.example/hooks/legalize",
eventTypes: ["law.updated", "reform.created"],
description: "Prod receiver",
});
console.log(endpoint.id, endpoint.secret);
endpoint, _ := client.Webhooks().Create(ctx, legalize.WebhookCreateOptions{
URL: "https://yourapp.example/hooks/legalize",
EventTypes: []string{"law.updated", "reform.created"},
Description: "Prod receiver",
})
fmt.Println(endpoint.ID, endpoint.Secret)
curl -X POST "https://legalize.dev/api/v1/webhooks" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://yourapp.example/hooks/legalize","event_types":["law.updated"]}'
Store the secret. It's returned exactly once in
the create response. It's never shown again by list
or retrieve. Lose it and you have to rotate by
deleting and re-creating the endpoint.
Delivery format
Each delivery is a POST with these headers:
X-Legalize-Signature: v1=<hex_hmac_sha256> — signature over timestamp + "." + raw_body, keyed by the endpoint secret. Multiple v1=… entries can be comma-joined.
X-Legalize-Timestamp — Unix seconds at the moment we signed the payload.
X-Legalize-Event — the event type (redundant with the body but handy for fast routing).
Content-Type: application/json.
Verify in your handler
Use the raw request bytes. Re-serializing the JSON
changes whitespace and breaks the signature. Every framework has
an escape hatch for this (Express: express.raw(),
Flask: request.get_data(), FastAPI: await request.body()).
from fastapi import FastAPI, Request, HTTPException
from legalize import Webhook, WebhookVerificationError
app = FastAPI()
@app.post("/hooks/legalize")
async def receive(req: Request):
body = await req.body()
try:
event = Webhook.verify(
payload=body,
sig_header=req.headers["X-Legalize-Signature"],
timestamp=req.headers["X-Legalize-Timestamp"],
secret=os.environ["LEGALIZE_WHSEC"],
)
except WebhookVerificationError as e:
logger.warning("webhook rejected: %s", e.reason)
raise HTTPException(400)
handle(event)
return {}
import express from "express";
import { Webhook, WebhookVerificationError } from "@legalize-dev/sdk";
const app = express();
app.post(
"/hooks/legalize",
express.raw({ type: "application/json" }),
(req, res) => {
try {
const event = Webhook.verify({
payload: req.body,
sigHeader: req.header("X-Legalize-Signature"),
timestamp: req.header("X-Legalize-Timestamp"),
secret: process.env.LEGALIZE_WHSEC,
});
handle(event);
res.status(204).send();
} catch (err) {
if (err instanceof WebhookVerificationError) return res.status(400).send();
throw err;
}
}
);
http.HandleFunc("/hooks/legalize", func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
defer r.Body.Close()
event, err := legalize.Verify(
body,
r.Header.Get("X-Legalize-Signature"),
r.Header.Get("X-Legalize-Timestamp"),
os.Getenv("LEGALIZE_WHSEC"),
)
if err != nil {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
handle(event)
w.WriteHeader(http.StatusNoContent)
})
Event types
Currently emitted:
law.created — new law appears in the repository.
law.updated — existing law body changed (reform, consolidation).
law.repealed — law marked as no longer in force.
reform.created — a new reform record linked to one or more laws.
test.ping — synthetic event from the dashboard's "Send test event" button.
Your SDK accepts any string — we may add event types in future releases; forward compatibility is intentional.
Retries, delivery receipts, replay
Failed deliveries (non-2xx from your server, timeouts, TLS errors)
are retried with exponential backoff for up to 24 hours. List past
deliveries via webhooks.deliveries(endpoint_id) and
retry one manually with webhooks.retry(endpoint_id, delivery_id).
Replay protection. The verifier rejects any
payload whose timestamp is more than 5 minutes off the server
clock. Clock-skew tolerance is configurable
(tolerance= in Python, tolerance option
in Node, WithTolerance(...) in Go).