Skip to the content.

By default case-calendar polls CourtListener on a cron. That works, but CourtListener throttles the free tier (300 requests per day) and a polling schedule means subscribers see an update minutes or hours after the entry actually hit the docket.

Webhooks flip the model. CourtListener calls your receiver the moment a new entry lands on a docket you’ve subscribed to. The receiver processes the entry, updates the SQLite store, and re-renders just the affected calendar in seconds — no polling, no quota burn.

← Back to docs

What you’ll set up

  1. A long random shared secret in .env (CASE_CALENDAR_WEBHOOK_SECRET).
  2. A small HTTPS endpoint that CourtListener can POST to, typically Caddy in front of case-calendar serve.
  3. One webhook registration in the CourtListener dashboard.
  4. One docket alert per docket in your config.yaml.

The whole thing is a 10-minute setup once your server has a public hostname.

1. Choose a secret

Generate a long random string and put it in .env:

# .env
CASE_CALENDAR_WEBHOOK_SECRET=PUT_A_LONG_RANDOM_STRING_HERE

CourtListener has no signing mechanism (no HMAC like Stripe / GitHub). The secret embedded in the receiver URL is the auth model. Treat it like a password — anyone who has it can submit forged events into your store.

The secret must be URL-safe: it goes straight into the receiver path (/webhooks/case-calendar/<secret>), so it can’t contain characters that need percent-encoding (+, /, =, &, ?, whitespace, etc.). Use Python’s secrets.token_urlsafe, which is purpose-built for this:

python -c 'import secrets; print(secrets.token_urlsafe(32))'

That returns a 43-character string drawn from the URL-safe alphabet (letters, digits, -, _) with 256 bits of entropy — plenty.

2. Run the receiver

uv run case-calendar serve --host 127.0.0.1 --port 8000

It’s a stdlib ThreadingHTTPServer listening on plain HTTP — TLS happens upstream in your reverse proxy, not in this process.

In production you’d run this as a systemd unit. There’s a case-calendar.service template in the repo root you can adapt.

3. Front it with HTTPS

You need a public hostname pointing at port 8000 of the box running serve. Most deployments use Caddy, which handles the Let’s Encrypt certificate automatically. The repo’s Caddyfile includes a working template:

webhook.example.com {
    reverse_proxy 127.0.0.1:8000
}

Edit the hostname and either symlink the file into /etc/caddy/Caddyfile or run caddy run --config Caddyfile directly. Cloudflare Tunnel, fly.io, or a tailscale funnel work just as well — anything that gives you a stable HTTPS URL pointing at port 8000.

4. Compute and verify the webhook URL

case-calendar will print the exact URL to register, with an optional probe that verifies the secret matches:

uv run case-calendar webhook-url \
    --host webhook.example.com \
    --check

What --check does:

If any of those is wrong, --check tells you which.

5. Register the webhook with CourtListener

Open courtlistener.com/profile/webhooks/ and create a new webhook:

CourtListener fires a Test event you can use to confirm the connection.

6. Subscribe to docket alerts

The webhook fires only for dockets you have a docket alert on. For each docket in config.yaml, open its CourtListener page and click “Get alerts”. (You can script this with the Docket Alerts API, but the UI is usually faster for a small case list.)

That’s it. New entries on any of those dockets now flow into your calendar in seconds.

How the receiver authenticates and dedupes

Two safety nets keep duplicate or retried deliveries from creating duplicate rows:

Even without the idempotency check, the per-entry fingerprint dedup in the store means a double-delivery of the same content does no extra work.

Polling and webhooks at the same time

You can run both safely. case-calendar sync (the polling path) and case-calendar serve (the webhook path) share the same SQLite file and use WAL journaling + a 5-second busy_timeout so concurrent writes don’t collide. There’s no harm in keeping a once-an-hour cron running as a safety net even when webhooks are healthy.

Operational tips

Next steps