voting
Base URL
Configured via shyconfig.api.base_url. Default local: http://localhost:8080.
Privacy contract
GET /polls/{id}/voters returns a count only — never individual identity_hash values. GET /polls/{id}/votes returns List 1 records (ballot IDs + choices) with no voter identity. List 1 / List 2 separation is enforced at the handler level.
Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /polls | — | List polls |
GET | /polls/{poll_id} | — | Get a poll |
GET | /polls/{poll_id}/votes | — | List 1 records (closed polls only) |
GET | /polls/{poll_id}/voters | — | Voter count only |
GET | /polls/{poll_id}/tally | — | Finalized tally + KMS attestation |
GET | /polls/{poll_id}/confirms | — | Confirmed receipt count |
GET | /vote_exists/{submission_id} | — | Presence check — { exists: boolean } |
GET | /reattestation_audit/{scoping_id} | — | Re-attestation audit log |
GET | /idv_audit/{scoping_id} | — | IDV audit log |
GET | /authority_actions/{scoping_id}/{identity_hash} | — | Eligibility actions for a participant |
GET | /submission/receipt/{scoping_id} | body proof | Retrieve private receipt (recoverable posture only) |
POST | /ballots | body proof | Submit a ballot to the pre-commit queue |
POST | /ballots/update | body proof | Rescind or replace a ballot |
POST | /submissions/{scoping_id}/flush | operator JWT | Flush queued ballots to canonical state |
POST | /submission/confirm | body proof | Submit a receipt confirmation (live biometric re-auth) |
POST | /submission/receipt | body proof | Save a private receipt (recoverable posture only) |
POST /ballots
{
"type": 2,
"data": {
"scoping_id": "referendum-2026-1",
"choices": ["yes"],
"submission_nonce": "<32-byte hex>",
"voter_pub_key": "<ed25519-hex>",
"voter_sig": "<base64>",
"idv_attestation_sig": "<base64>",
"timestamp": 1700040000
}
}
ZK tier replaces voter_pub_key / voter_sig / idv_attestation_sig with zk_nullifier, zk_nullifier_proof, zk_commitment, and didit_commitment_sig.
Response: { "queued": true, "submission_id": "<direction-free id>" }
POST /ballots/update
Rescind (new_choices: []) or replace (new_choices: [newChoice]) an existing ballot.
{
"scoping_id": "referendum-2026-1",
"new_submission_nonce": "<32-byte hex>",
"new_choices": ["no"],
"identity_hash": "<H(commitment || scoping_id)>",
"idv_proof_hash": "<H(proof || scoping_id || ...)>"
}
POST /submission/confirm
Submits a receipt confirmation. Requires a fresh live IDV attestation — confirmations from fabricated identities are structurally rejected because they cannot produce a valid idv_attestation_sig without a real biometric session.
{
"scopingId": "referendum-2026-1"
}
GET /polls/{poll_id}/tally
{
"scoping_id": "referendum-2026-1",
"counts": { "yes": 1203, "no": 644 },
"total_votes": 1847,
"confirmed_count": 1731,
"vote_merkle_root": "<base64>",
"voter_merkle_root": "<base64>",
"signature": "<base64-der-ecdsa>",
"public_key": "<base64-der-ecdsa>",
"height": 5891
}
Verify the KMS signature:
go run ./verify/cmd/verify --api https://your-deployment.example --poll referendum-2026-1
total_votes - confirmed_count is the Sybil-audit gap — fabricated identities cannot confirm because they never held a person_secret.
GET /polls/{poll_id}/votes
[{ "submission_id": "7c3a...", "scoping_id": "referendum-2026-1", "choices": ["yes"], "height": 142 }]
submission_id = H(submission_nonce) — cannot be linked to identity_hash without the nonce.