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/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/ballotsbody proof + configured middlewareQueue a ballot
POST/ballots/updatebody proof + configured middlewareWithdraw or replace a ballot
POST/polls/{poll_id}/flushoperator authorization in separated deploymentsFlush queued operations
POST/polls/{poll_id}/confirmbody proof + configured middlewareSubmit receipt confirmation
POST/recovery/identitiesverified Firebase callerCaller-owned recovery index
POST/recovery/deleteverified Firebase callerDelete off-chain recovery data
POST/recovery/secretverified Firebase callerSave encrypted recovery secret
POST/recovery/secret/loadverified Firebase callerLoad 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.