Ballot TrailBorn Between 2 Generals

Section 10 · Section 11

Roles & API

Separation of duties is the control that makes every other control worth having. A perfect audit log written by an account that can do everything proves very little.

Who may write what

Rendered from the engine's own ROLES table. The permission check runs inside the write path — try a violation on the lifecycle page.

RoleMay writeMust never

Where two people are required

No single role or individual may silently complete any of these:

  • Physical custody transfer, where procedure requires it
  • Privacy separation — the one that matters most
  • Manual override of a rejected event
  • Mass token void or reissue
  • Emergency key revocation or recovery
  • Import of externally corrected bulk records
  • Release of restricted audit data

Note what is on that list besides separation: the bulk operations. A single mistaken mass-void is a disenfranchisement event, and it does not need malice to happen — it needs one tired person and an unfiltered query.

The API surface

Small on purpose. Every write is attributable to an authenticated service, device or person; mutual TLS between services; OIDC or a jurisdiction-approved equivalent for people; MFA for anyone privileged; device certificates for managed scanners where feasible.

POST /v1/events                                  // the only write
GET  /v1/tokens/{token-id}/status              // shape varies by role
GET  /v1/tokens/{token-id}/events              // staff + auditors, redacted by role
GET  /v1/audit/batches/{batch-id}/proof?event_id=…

The status endpoint is worth dwelling on: it does not return a record that the caller then filters. It returns a different object depending on who is asking, and the voter-facing object is constructed by a function that has no branch capable of returning more.

// the entire voter-facing response
{
  "status": "RECEIVED",
  "status_label": "Your ballot package was received by the election office.",
  "next_action": null,
  "last_updated": "2026-11-04T13:10:00-05:00"
}

The error model

Fail closed, and say which wall you hit — except where saying so would leak.

HTTPErrorMeaning

Two of those deserve a note. 404 TOKEN_NOT_FOUND covers both "unknown" and "intentionally undisclosed" — deliberately, because an endpoint that distinguishes them is an oracle for testing whether a code is real. And 503 AUDIT_STORE_UNAVAILABLE fails closed for state-changing operations: if the tamper-evidence store is down, the correct answer is to stop writing, not to write unwitnessed.

Reason codes

Jurisdiction-configurable, but a baseline set exists so that reconciliation categories mean the same thing across offices. No reason code may describe or imply a ballot selection — a rejection reason is about an envelope, a signature or a deadline, never about a choice.

Paper remains authoritativeThe token never touches a voteAppend, never overwrite