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
{
"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
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);
}