Reading RoomBorn Between 2 Generals Get the widget →

How publishing works

Five stages, and the third one cannot be skipped.

A finished request arrives over the API, becomes a draft, and gets scanned. The scan’s findings sit between the draft and the world, and the only thing that clears them is a person.

The ingest endpoint has no publish path. It accepts a publish field and stores it as false, and there is no value of it that produces a public post. That is the shape of the whole system: the API can put research into the reading room, and only a human session can put it out.

The five stages

Ingest

A POST /api/ingest carrying a bearer token with the publish scope. The body is the request as it actually happened: the request identifier, the agency, the jurisdiction, the topic and subject from the shared vocabulary, the date filed, the date answered, the outcome, a summary, and the documents.

Typically this call comes from the records console that filed the request, which already holds every one of those fields. It can also be made by hand.

Draft

The response is { id, status: "draft", pii: [...], editUrl }. Status is draft on every successful ingest without exception. Nothing is publicly readable, nothing is indexed, and the draft is visible only to the author.

Scan — the gate

Before the response returns, every character submitted is scanned: the summary, the document text, the file names. Findings come back in the ingest response, so the tool that sent the documents is told immediately what it just sent.

Findings are attached to the draft and they block. A draft with unresolved findings has no publish action — not a disabled one, none. Findings marked for review go to a reviewer rather than to the author. What the scan looks for →

Review and publish

A person opens the draft, reads the findings in place, redacts or accepts each one, writes whatever the post is going to say, and publishes. The provenance block is assembled from the ingest fields and cannot be edited into something the request did not say — an author can add to it, not contradict it.

Correction

A published post can be corrected, and the correction is visible: what changed, when, and why, kept with the post rather than replacing it silently. A post that was wrong and got fixed reads better than a post that was never wrong. Removal is a separate path →

The ingest contract

One endpoint, one shape. The token needs the publish scope; a read-only token cannot create a draft.

POST /api/ingest
Authorization: Bearer <token with scope: publish>
Content-Type: application/json

{
  "requestId":    "REQ-2026-0184",
  "agency":       "…",
  "jurisdiction": "…",
  "topic":        "policing",
  "subject":      "use-of-force",
  "filedOn":      "2026-02-03",
  "respondedOn":  "2026-03-17",
  "outcome":      "released" | "denied" | "no-records" | "partial",
  "summary":      "…",
  "documents":    [ { "name": "…", "text": "…", "url": "…" } ],
  "canonicalUrl": "…",
  "publish":      false
}

→ 200 { "id": "…", "status": "draft", "pii": [ … ], "editUrl": "…" }

publish is in the body and it is not a choice

The field exists so that a caller reading the contract cannot mistake what happens. It is stored as false. There is no true that works, no query parameter, no header, and no scope that unlocks one. If that field ever appears to do something, it is a bug and should be reported as one.

Documents may carry extracted text, a url, or both. Text is what the scan reads — a document supplied only as a URL is a document the scan cannot see, and a draft whose documents were all URL-only is marked as unscanned rather than as clean. That distinction matters →

What this path refuses to do

It does

Return the findings to the callerThe tool that sent the documents learns what was in them at the moment it sends, not later in an email. That is the only moment where the person who has the file in front of them is still looking at it.

It does

Keep the provenance immutableAgency, jurisdiction, request identifier and dates come from the ingest call and are not editable in the composer. An author who filed under one law cannot present it as another.

Refuses

Publish on ingestUnder any flag, scope, or parameter. The API has no route that makes something public.

Refuses

Let an author clear a minor or medical findingThose two route to review. It is not a statement about any particular author’s judgement; it is that these are the findings whose consequences fall entirely on someone who is not in the conversation.

Careful

Silence on an empty scanA scan that found nothing produces a draft that says the scan found nothing — never a draft that says the documents are clean. Those are different sentences and the second one is not ours to write.

The order matters

The gate is before the door, not beside it.

Every system that puts the safety check next to the publish button eventually ships the version where somebody clicks the button first. So there is no button until the check is answered.

How this works

0:56 · narrated · captioned · served from this site, and nothing is sent anywhere when you press play. Prefer to read it? The transcript is below.

Transcript
AtSectionWhat it says
0:02IngestA finished request is posted to the ingest endpoint: the agency, the jurisdiction, the topic, the dates, the outcome, the summary, and the documents.
0:13Always a draftThe response is always a draft. The publish field is stored as false, and there is no flag, scope or parameter anywhere that makes this call publish something.
0:23The gateBefore that response returns, everything submitted is scanned. The findings come back in the response, so the tool that sent the documents is told immediately.
0:33BlockingA draft with unresolved findings has no publish action. Not a disabled one. None. Minors and medical details go to a reviewer rather than back to the author.
0:44CorrectionsA published post can be corrected, and the correction is visible: what changed, when, and why, kept with the post instead of quietly replacing it.