मुख्य सामग्री पर जाएँ

Casting votes

How a voter actually votes - authenticate once against the process census, get the credential service to blind-sign a ballot per question, then relay the signed envelope. The SDK's voting package does the signing for you; the SaaS forwards the ballot without ever decoding it.

Casting a vote is the only step with client-side cryptography. Everything else in the API is a plain REST call; here the voter signs their own ballot so the SaaS never sees how they voted. The SDK's voting package, @vocdoni/api-voting, does that cryptography for you: it generates the ephemeral key, builds and signs the protobuf vote envelope, and relays it. Start with it below; if you would rather implement the signing yourself, the raw flow is at the end of this page.

Because each question is its own on-chain election, a voter authenticates once against the process, then signs and relays a ballot per question they are eligible for.

The flow

  1. Authenticate once - the voter presents whatever the census requires: identity authFields and/or a one-time code sent to their email/phone (a 2FA census needs no auth fields). Success yields a token bound to the process.
  2. Blind-sign per question - for each question, the credential service (CSP) blind-signs the voter's ephemeral voting address for that question's election. It refuses unless the voter is in the question's eligibility subset. Signatures are salted per election, so one cannot be replayed on another question.
  3. Build and sign - the voter builds the protobuf vote envelope and signs it locally with the ephemeral key.
  4. Relay - the signed envelope is relayed (asynchronously) to the protocol, which returns a vote receipt (nullifier).

Steps 1 and 2 are plain REST calls, wrapped by the API client as client.processes; steps 3 and 4 are the cryptography @vocdoni/api-voting implements.

The voter-facing endpoints are public - they carry no API key. The voter only needs the apiUrl, the processId and the Vochain chainId. The process read that returns them is public too, so the voter app fetches them directly - no backend handover needed; each question's upstreamId is also reported by the check call below.

Cast a vote with the SDK

Install the voting package alongside the API client - the client covers the voter-facing REST calls, the voting package the cryptography:

npm install @vocdoni/api-client @vocdoni/api-voting
pnpm add @vocdoni/api-client @vocdoni/api-voting
yarn add @vocdoni/api-client @vocdoni/api-voting

client.processes covers the voter-facing calls (authenticate, check eligibility, get the CSP signature) and VotingClient does the rest - one call builds the envelope, signs it with the ephemeral key and relays it:

import { VocdoniApiClient } from '@vocdoni/api-client'
import { EphemeralSigner, VotingClient } from '@vocdoni/api-voting'

const client = new VocdoniApiClient({ apiUrl: 'https://saas-api-stg.vocdoni.net' })
const voting = new VotingClient({ client })

// Both reported by the (public) process read.
const processId = '<processId>'
const chainId = '<chainId>'

// 1. Authenticate once - send exactly the fields the census requires. An
//    auth-only census (like this one) returns a verified token right away; a
//    2FA census confirms a one-time code first (see the note below).
const { authToken } = await client.processes.authStep0(processId, { memberNumber: 'A-101' })

// 2. Check where the voter stands - census membership plus, per question,
//    eligibility and that question's on-chain election id (upstreamId).
const { belongsToProcess, questions } = await client.processes.check(processId, { authToken })
const question = questions.find((q) => q.canVote && !q.hasVoted)
if (!belongsToProcess || !question?.upstreamId) throw new Error('nothing to vote on')

// 3. Fresh ephemeral key per ballot; the CSP signs its address for this
//    question's election. Refused unless the voter is in the question's
//    eligibility subset.
const signer = new EphemeralSigner()
const { signature, weight } = await client.processes.sign(processId, {
  authToken,
  electionId: question.upstreamId,
  payload: signer.address,
})

// 4. Build + sign the vote envelope locally, relay it, and poll for the receipt.
const jobId = await voting.vote({
  processId: question.upstreamId, // the vote goes to the question's election
  chainId,
  choices: [1], // the ballot - see Voting types for its shape per ballot type
  signer,
  cspSignature: signature,
  cspWeight: weight,
})
const job = await client.jobs.waitFor(jobId)
console.log('voteID:', job.result?.voteID)

Repeat steps 3 and 4 for every question the voter is eligible for - the auth token and the check from steps 1 and 2 are reused across all of them, but each ballot needs a fresh EphemeralSigner. The returned voteID is the vote nullifier - the voter's receipt, which they can use to verify their vote was counted. Once a question ends, read its tally from Results.

2FA censuses

When the census verifies voters by email/phone, step 0 sends a one-time code and returns a pending token. Confirm it before signing: client.processes.authStep1(processId, { authToken, authData: ['123456'] }). Need a new code? client.processes.resend(processId, { authToken }).

Encrypted questions

A question created with secretUntilTheEnd keeps its ballots sealed until it ends. Fetch its encryptionKeys from the public question read and pass them to vote() - the ballot is sealed automatically:

const { encryptionKeys } = await client.processes.getQuestion(processId, question.questionId)

const jobId = await voting.vote({
  // ...same options as above, plus:
  encryptionKeys,
})

The keykeepers publish the keys asynchronously, so encryptionKeys can be absent for a few seconds right after publish - poll the question read until it is present before building the ballot.

Building with React

@vocdoni/react-providers wraps this whole flow in context providers and hooks that authenticate, sign and relay for you. See the SDK repository and the SDK quickstart.

Voter status

Public helpers let a UI show a voter where they stand without casting anything. Each identifies the voter by their verified authToken:

POST/processes/{processId}/check
POST/processes/{processId}/weight
POST/processes/{processId}/sign-info
curl -X POST "$B/processes/$PROCESS/check" -d '{ "authToken": "<authToken>" }'
# -> { "belongsToProcess": true, "weight": "1",
#      "questions": [ { "questionId": "...", "upstreamId": "...", "canVote": true, "hasVoted": false } ] }

Looking up whether specific members voted is an admin task, not a voter one:

GET/processes/{processId}/participants
GET/processes/{processId}/participants/{participantId}

Doing the signing yourself

Not using the SDK - a non-JS client, or you want to audit what goes on the wire? Everything above reduces to the REST calls below plus building and signing the envelope: generate an ephemeral secp256k1 keypair, build the protobuf VoteEnvelope carrying the CSP (CA) proof, and sign the transaction with an EIP-191 personal_sign signature. The voting package source is the reference implementation.

# a) Authenticate (step 0) - send exactly the fields the census requires (authFields and/or the
#    email/phone used for the code). Here an auth-only census by memberNumber; a mail census would
#    send { "email": "[email protected]" }. Auth-only censuses are verified here and there is no code.
curl -X POST "$B/processes/$PROCESS/auth/0" \
  -H "Content-Type: application/json" \
  -d '{ "memberNumber": "A-101" }'
# -> { "authToken": "<authToken>" }

# 2FA censuses only: submit the emailed/SMS one-time code (authData[0] is the code).
# Need a new code? POST /processes/$PROCESS/auth/resend with the authToken.
curl -X POST "$B/processes/$PROCESS/auth/1" \
  -H "Content-Type: application/json" \
  -d '{ "authToken": "<authToken>", "authData": ["123456"] }'

# b) CSP blind-signs the ephemeral address for one question's election (electionId = its upstreamId).
#    Refused unless the voter is in that question's eligibility subset.
curl -X POST "$B/processes/$PROCESS/sign" \
  -H "Content-Type: application/json" \
  -d '{ "authToken": "<authToken>", "electionId": "<upstreamId>", "payload": "<hex ephemeral address>" }'
# -> { "signature": "<csp-signature>", "weight": "1" }

# c) Build + sign the protobuf Vote envelope locally, hex-encode the SignedTx, then relay it (async).
curl -X POST "$B/vote" \
  -H "Content-Type: application/json" \
  -d '{ "txPayload": "<hex of the signed Vote envelope>" }'
# -> 202 Accepted   { "jobId": "<jobId>" }

curl -s "$B/jobs/<jobId>"
# -> { "status": "completed", "result": { "voteID": "<nullifier>" } }

Repeat steps b and c for every question the voter is eligible for; the auth token from step a is reused across all of them.

What is in the envelope

The vote package inside the envelope is {"votes":[<choice>]} - for example {"votes":[1]}. Building and signing the envelope is exactly what the SDK does for you above. See Voting types for how the choices array is shaped per ballot type.

Encrypted (secret-until-the-end) questions

For a question created with secretUntilTheEnd, seal the vote package with the question's encryptionKeys before building the envelope. Read them from the question read: the field is absent until the keykeepers publish the keys, so poll the question until encryptionKeys is present, then encrypt with them.