Skip to content
Aletheca

Webhooks

The ledger, pushed.

The same rows the public feed renders and the notification system reads. A partner site can mirror a request's whole life without polling anything.

Events

  • proposal.created

    A proposal your site originated enters the Docket.

  • proposal.funded

    It reaches quorum and is queued for filing.

  • proposal.refused

    Review refuses it, with the reason.

  • request.filed

    The letter goes out, by whichever channel.

  • request.perfected

    The agency acknowledges it and assigns a tracking number.

  • request.disposition_changed

    Any move in the sixteen-term vocabulary.

  • request.overdue

    The statutory deadline passes with no determination.

  • request.event

    Any ledger event at all, for sites that want the firehose.

  • document.published

    Pages are released and published to the Reading Room.

  • payout.accrued

    Revenue share accrues on a request your site originated.

Payload

POST your endpoint
{
  "id": "evt_01J8Z2M4Q1",
  "type": "request.disposition_changed",
  "created": "2026-08-21T17:30:00Z",
  "partner": "uap-nexus",
  "data": {
    "request": {
      "id": "ALT-2026-0097",
      "url": "https://aletheca.com/requests/faa-slc-radar-audio-2026-03-11",
      "agency": "faa",
      "disposition": "released-full",
      "previous_disposition": "processing",
      "external_ref": "uapnexus:case:8812",
      "clock": { "elapsed": 32, "allowed": 20, "overdue": true }
    },
    "documents": [
      { "id": "doc-0097-1", "pages": 28, "redacted_pages": 0 }
    ]
  }
}

Signed

Every delivery carries Aletheca-Signature: an HMAC-SHA256 over the raw body and a timestamp, with a five-minute tolerance. Verify before parsing, and compare in constant time.

At least once

Retried with exponential backoff for 24 hours on any non-2xx. Handlers must be idempotent on the event id — a disposition change delivered twice must not send your users two emails.

Ordered by ledger, not by wire

Every event carries a monotonically increasing sequence per request. Network order is not guaranteed; sequence order is. Drop anything older than the last sequence you processed.

Verifying a delivery

node
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody: string, header: string, secret: string) {
  const [tsPart, sigPart] = header.split(",");
  const timestamp = tsPart.slice(2);
  const signature = Buffer.from(sigPart.slice(3), "hex");

  // Reject replays before spending a hash on them.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest();

  return signature.length === expected.length &&
    timingSafeEqual(signature, expected);
}