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) # secret shown ONCE
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); // secret shown ONCE
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) // secret shown ONCE
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() # raw bytes 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) # event.type, event.data return {}
import express from "express"; import { Webhook, WebhookVerificationError } from "@legalize-dev/sdk"; const app = express(); app.post( "/hooks/legalize", express.raw({ type: "application/json" }), // raw Buffer (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).