Skip to main content

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

MethodPathAuthDescription
GET/pollsList polls
GET/polls/{poll_id}Get a poll
GET/polls/{poll_id}/votesList 1 records (closed polls only)
GET/polls/{poll_id}/votersVoter count only
GET/polls/{poll_id}/tallyFinalized tally + KMS attestation
GET/polls/{poll_id}/confirmsConfirmed 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 proofRetrieve private receipt (recoverable posture only)
POST/ballotsbody proofSubmit a ballot to the pre-commit queue
POST/ballots/updatebody proofRescind or replace a ballot
POST/submissions/{scoping_id}/flushoperator JWTFlush queued ballots to canonical state
POST/submission/confirmbody proofSubmit a receipt confirmation (live biometric re-auth)
POST/submission/receiptbody proofSave 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.