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 dictionary, including open polls |
GET | /polls/{poll_id}/voters | — | Voter count for this poll only |
GET | /polls/{poll_id}/tally | — | Finalized tally; optional signer attestation |
GET | /polls/{poll_id}/confirms | — | Confirmed receipt count |
GET | /api/v1/posture | — | Operator policy; local safety fallbacks still apply |
POST | /ballots | body proof + configured middleware | Queue a ballot |
POST | /ballots/update | body proof + configured middleware | Withdraw or replace a ballot |
POST | /polls/{poll_id}/flush | operator authorization in separated deployments | Flush queued operations |
POST | /polls/{poll_id}/confirm | body proof + configured middleware | Submit receipt confirmation |
POST | /recovery/identities | verified Firebase caller | Caller-owned recovery index |
POST | /recovery/delete | verified Firebase caller | Delete off-chain recovery data |
POST | /recovery/secret | verified Firebase caller | Save encrypted recovery secret |
POST | /recovery/secret/load | verified Firebase caller | Load encrypted recovery secret |
GET /api/v1/posture returns write_only for a permanent write-only deployment,
or null to use the client manifest otherwise. A recoverable default is not a
server override of a user's restrictive preference.
These are the current Go relay routes; adapter-specific /submission/* routes
are not interchangeable. A deployment must enable Firebase write authentication
and configure its App Attest verifier explicitly. Manifest team/bundle fields
alone do not enable the verifier. SDK clients send X-Shyware-Posture: write_only
to suppress recovery writes; unknown networks must retain that restrictive mode.
POST /ballots
The relay accepts { "tx": "<JSON-serialized signed envelope>" }. The
enclosed transaction contains the following data plus its envelope signature:
{
"type": 2,
"data": {
"scoping_id": "referendum-2026-1",
"choices": ["yes"],
"submission_nonce": "<32-byte hex>",
"beacon_block_hash": "<original block hash hex>",
"beacon_block_height": 42,
"didit_session_id": "<approved session>",
"voter_pub_key": "<ed25519-hex>",
"voter_sig": "<base64>",
"idv_attestation_sig": "<base64>",
"identity_hash": "<hex, enclave-attested>",
"timestamp": 1700040000
}
}
identity_hash is produced by the IDV attestation enclave's /attest response, not computed by the client — see IDV attestation enclave. For the default Didit tier it's sha256(document_key || scoping_id), derived from Didit's id_verification document data (Didit's real API has no stable per-person identifier field, so the enclave uses the one real document behind each verification instead) — stable across devices using the same document; renewed verification with a different document is not guaranteed to produce the same hash. The chain verifies idv_attestation_sig covers this exact value; it has no way to recompute it itself.
ZK tier replaces voter_pub_key / voter_sig / idv_attestation_sig / identity_hash 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.
The body is { "tx": "<JSON-serialized signed type-6 envelope>" }.
Its data includes scoping_id, old_submission_id, new_submission_nonce,
beacon_block_hash, beacon_block_height, new_choices, timestamp,
voter_pub_key, voter_sig, identity_hash, and idv_attestation_sig.
Use the SDK builder so signatures cover the canonical update message.
Response: { "queued": true, "scoping_id": "<poll>", "submission_id": "<new id>" }.
Replacements and withdrawals use the same periodic queue as casts. This response
is not ledger confirmation; query inclusion before reporting success. Recoverable
receipt mappings are changed only after the ledger confirms publication. Queued
updates require the separated publication gate; there is no immediate paired
transaction fallback.
A successful withdrawal removes both active records; a replacement changes List 1 while keeping the poll's participant count constant. Updates require the original voter key commitment and reject identifiers already belonging to another ballot. Older records without an ownership commitment cannot be edited without an authorized migration.
POST /polls/{poll_id}/confirm
Submits a signed type-4 envelope in { "tx": "<JSON>" }. Requires a fresh
confirmation-domain IDV attestation and device signature. Enrollment attestations
cannot be replayed as confirmations. Confirmation counts measure participation
in this audit; non-confirmation alone does not prove a fabricated voter.
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
}
Independently verify the tally (count-match check runs regardless; a signature check runs when a signer public key is present; software keys, KMS, and HSM are optional choices):
go run ./embodiments/shyvoting/cmd/shyvoting-verify --api https://your-deployment.example --poll referendum-2026-1
total_votes - confirmed_count is the unconfirmed count. Genuine voters may also decline or fail to confirm; a gap alone is not proof of fabrication.
GET /polls/{poll_id}/votes
{ "7c3a...": { "ballot_id": "7c3a...", "choices": ["yes"] } }
For default voting, submission_id = SHA-256(decoded beacon bytes ‖ decoded nonce bytes). Retain the original beacon with the private nonce in recoverable posture. Count-match is |L1(poll)| = |L2(poll)| at the same height: one person may vote in many different polls, while each poll contains at most one active vote for that identity. A mismatch must fail verification, independently of signing configuration.