Census
A census is the who-can-vote list and how they prove who they are. It is declared inline when you create a process - no separate create-and-publish step - and its type is inferred from the fields you pick.
A census is the eligible-voter list plus the rules for how a voter authenticates. You no longer
build and publish it separately: you declare it inline in the census object of
POST /processes, and the server materializes and publishes it
for you at publish time. Its internal id is
never sent or returned.
The census object
{
"weighted": false, // when true, each member's weight counts as vote weight
"anonymous": false, // when true, the CSP blind-signs ballots it cannot link to voters
"authFields": ["memberNumber"],
"twoFaFields": [], // e.g. ["email"] for an email OTP second factor
"groupId": "", // populate from a group's members...
"memberIds": [] // ...or list member ids explicitly
}
| Field | Type | Description |
|---|---|---|
authFields |
string[] | Optional. Identity fields a voter must present (e.g. memberNumber). |
twoFaFields |
string[] | Optional. Channels for a one-time-code second factor: email or phone. |
weighted |
boolean | Count each member's weight as their vote weight. |
anonymous |
boolean | Blind the CSP signatures so authorizations cannot be linked to ballots - see Anonymous voting. |
groupId |
string | Populate the census from a group's members. |
memberIds |
string[] | Populate the census from an explicit list of member ids. |
Populate the census from a groupId or an explicit memberIds list - both reference the
organization's members and groups. Use the auto "All members"
group to include everyone.
The fields above are what you send. A process read adds response-only size and totalWeight to
the census object - see Reading a process.
Authentication
authFields and twoFaFields are two independent settings, each optional. Set either, or both:
authFieldsonly - an auth-only census: the voter authenticates purely by presenting these identity fields, no second factor.twoFaFieldsonly - a 2FA census with noauthFieldsrequired: the voter identifies by the contact channel (email or phone), receives a one-time code, and enters it. Turning on 2FA does not require any auth fields.- both - identity fields and a one-time code.
The census type is inferred from twoFaFields (you never set it directly); authFields can be
combined with any of them:
| Type | twoFaFields |
Second factor |
|---|---|---|
auth |
none | none (auth-only) |
mail |
["email"] |
email OTP |
sms |
["phone"] |
SMS OTP |
sms_or_mail |
["email","phone"] |
voter's choice |
authFieldsoptions:name,surname,memberNumber,nationalId,birthDate,password.twoFaFieldsoptions:email,phone.
Every voter needs complete, unique login data
Whatever identifies a voter - the authFields values, plus the email/phone used for the code -
must be unique across the members you include. Members the census cannot tell apart (equal data,
including values that differ only by case) make POST /processes and PUT /processes/{processId}
fail with 400 (error code 40037) and their ids in data.duplicates. Members missing the data
instead - an empty or whitespace-only auth field, or no 2FA channel - are left out of the census
and listed in the response's missingData; they cannot log in and do not count toward the census
size. A password is the exception to uniqueness: it is a secret, not an
identifier, so members may share one. See
Creating a process, and
validate the census first to catch both.
Weighted voting
Set "weighted": true to make each member's weight count as their vote weight - use it for
shareholder meetings or any vote where members do not count equally.
Password credential
Add password to authFields to make voters also present the
member password the organization set for them.
It checks a voter's identity but never finds them on its own, so the census needs at least one
other auth field or a 2FA field next to it:
"census": { "authFields": ["memberNumber", "password"], "groupId": "<group>" }
Prefer another auth field over a 2FA field alone: with password as the only auth field, failed logins
are capped once for the whole process rather than per voter (see
Password censuses).
A census whose only credential is password is refused with 400 (error code 40037, message
"password needs another credential or a 2FA field") by POST /processes, PUT /processes/{processId}
and POST /processes/census/validation.
Members without a password when the census is built are left out, like any member with missing data,
and listed in missingData. Setting their password later does not add them by itself, as for any other
auth field, so set passwords before creating the process.
Anonymous voting
Set "anonymous": true on the census to make the process unlinkable: the credential service
(CSP) signs each ballot blind - it signs a message it cannot read - so it cannot correlate the
authorization it granted with the envelope that lands on chain. Nobody, including the CSP, can tell
who cast which vote; double-vote protection keys on the voter's auth token instead of an address.
The flag is orthogonal to everything else in the census: combine it with any authFields, any 2FA
channel, and with weighted (the authorized weight is cryptographically bound into the signature,
so a voter cannot claim a different one).
A blind signature, not zero-knowledge
This is not zk voting: the on-chain envelope type stays non-anonymous, and anonymity comes from the blind signature rather than a zero-knowledge membership proof. Anyone can still verify that every ballot was authorized by the CSP - just not for whom.
Under the hood, an anonymous process publishes with the CSP's blind public key as its census root, and ballots carry blind-signature proofs. Voter authentication is unchanged; only the signing step differs - a two-round blind exchange instead of a plain sign. See Anonymous voting with blind signatures for the voter flow.
One consequence to plan for: the CSP never learns the voter's address or nullifier, so
sign-info cannot return them - a vote receipt exists
only in the session that cast it. Anonymous voting through the blind CSP is available on every plan.
Per-question eligibility
The process census is the full electorate. A single question can narrow it to a subset with its
own optional census object (a groupId or memberIds, always within the process census); omit it and
every census member may vote on that question. Reads expose the resolved subset as eligibleMemberIds,
but only to a manager/admin (or a voting:write key) - it is stripped from the public process
reads. An empty subset means no restriction: the question is open to the whole census. A voter
checks their own per-question eligibility with
POST /processes/{processId}/check.
The subset is not fixed at publish time: replace it later - even while voting is ongoing - with
PUT /processes/{processId}/questions/{questionId}/census.
"questions": [{
"title": { "default": "Board seat (full members only)" },
"choices": [ /* ... */ ],
"census": { "groupId": "<full-members-group>" } // subset of the process census
}]
Identity and weight always come from the process census; the subset only decides which members may vote on which question. This is how one process runs questions with different electorates without building multiple censuses.
Validating a census
Dry-run a census spec before you create the process. It flags the common problems - duplicate or
missing auth-field data across the members it resolves - without creating anything. A field that is
empty or only whitespace counts as missing, and a member with missing data is reported in
missingData, never in duplicates. It also replaces the per-group validation endpoint of earlier API
versions: to check a group, pass its groupId.
curl "${auth[@]}" -X POST "$B/processes/census/validation" -d @- <<JSON
{
"orgAddress": "$ORG",
"census": { "authFields": ["memberNumber"], "groupId": "$GROUP" }
}
JSON
// Resolves when the census is usable; throws a VocdoniApiError (400) listing the offending members otherwise.
await client.elections.validateCensus({
orgAddress: org,
census: { authFields: ['memberNumber'], groupId: group },
})
A usable census answers a bare 200; an unusable one is a 400 (error code 40037) whose data
lists the offending member ids in duplicates, missingData and, for an explicit memberIds list,
notFound. The same code also answers a malformed request - a census with neither authFields nor
twoFaFields, one whose only credential is password, an unknown group or a malformed member id - and
then data carries no lists, so check for them before reading them. The dry-run is stricter than
creating the process: members with missing data fail it, while POST /processes leaves them out of the
census and goes ahead.
Kept in sync with the memberbase
The memberbase is the source of truth: a process census is a snapshot of it taken through a group or a member list, kept in sync after publishing. Changes to members and groups cascade to the censuses of ongoing processes:
- Additions propagate. A member added to the group a live census was built from (including new
members landing in an auto "All members" group) joins that census and can vote; the affected
elections are resized on chain automatically, through a
census-update job whose id the member or group response
returns in
censusJobIds. Only elections stillREADYorPAUSEDare resized; one that already ended on chain is skipped rather than failing the job. Additions are gated by your plan's census quota. - Removals cascade, but voters are protected. Deleting a member or removing them from a group
strips them from the affected censuses and question eligibility lists, so the credential service
stops signing for them. If the CSP has already signed for the member on a question that is
still
READYorPAUSED, the removal is refused with a409(error code40173) and the offending ids indata.signedMemberIds- once voting closes it succeeds. - The census can also be edited directly on the process - adding members, removing them, or replacing a question's eligibility list - see Managing a published census.