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

Quickstart

Run a full election end to end - create a managed organization, add a voter, open a voting process with an inline census, publish it, and read the tally. Examples are cURL, with TypeScript SDK, C# and Python variants; any HTTP client works the same way.

This runs the entire lifecycle once: create a managed organization for a customer, add a voter, open a yes/no process whose census is declared inline, publish it on-chain, and read the tally. There is no separate census create-and-publish step - the process carries its census.

The one step omitted here is casting a ballot - voter-facing client-side cryptography, done in the browser by the SDK. The Quickstart proves the full server-side path up to reading results; see Casting votes for the rest.

Before you start

Create an account in the API Dashboard and mint an API key under your integrator organization. To run this whole flow the key needs the managed:write, managed:read, quota:read, members:write and voting:write scopes - see API keys. Every request carries Authorization: Bearer <your-api-key>; the key is your integrator identity, so the integrator endpoints take no address in the path. The key is shown only once - store it safely. For the base URL and environments, see API conventions.

One managed organization on the free tier

The free tier allows one managed organization. Delete it (see Managed organizations) or request more quota to run the Quickstart repeatedly.

1

Set up a client

Export your key and base URL once; every curl below reuses them. Writes also send Content-Type: application/json.

export VOCDONI_BASE_URL="https://saas-api-stg.vocdoni.net"
export VOCDONI_API_TOKEN="vsk_your_key_here"
auth=(-H "Authorization: Bearer $VOCDONI_API_TOKEN" -H "Content-Type: application/json")
B="$VOCDONI_BASE_URL"
2

Create a managed organization

The integrator is resolved from the key, so this endpoint is path-less. Carry forward the returned address.

ORG=$(curl -s "${auth[@]}" -X POST "$B/integrator/organizations" \
  -d '{"type":"association","meta":{"name":"Maple Street HOA"}}' | jq -r .address)
3

Add a member

With ?async=true bulk member writes run as a job: the call returns a jobId you poll until progress: 100.

JOB=$(curl -s "${auth[@]}" -X POST "$B/organizations/$ORG/members?async=true" -d '{
  "members": [
    { "name": "Alice", "memberNumber": "A-101", "email": "alice@example.org", "weight": "1" }
  ]
}' | jq -r .jobId)
# a members-job never fails: a row that cannot be stored leaves it pending, so poll with a deadline
if [ -z "$JOB" ] || [ "$JOB" = "null" ]; then echo "no jobId - check the import response" >&2; else
  for i in $(seq 120); do
    [ "$(curl -s "${auth[@]}" "$B/jobs/$JOB" | jq -r .status)" = "completed" ] && break
    [ "$i" = 120 ] && echo "members-job $JOB did not complete - do not build the census yet" >&2
    sleep 1
  done
fi
4

Create an all-members group

The group is what the inline census points at to include your members.

GROUP=$(curl -s "${auth[@]}" -X POST "$B/organizations/$ORG/groups" \
  -d '{"title":"All voters","includeAllMembers":true}' | jq -r .id)
5

Create a voting process

One call carries the inline census (auth-only by member number, populated from the group) and the question. It returns the processId as a draft.

PROCESS=$(curl -s "${auth[@]}" -X POST "$B/processes" -d @- <<JSON | jq -r .processId
{
  "orgAddress": "$ORG",
  "census": { "authFields": ["memberNumber"], "groupId": "$GROUP" },
  "title": { "default": "Repaint the fence?" },
  "description": { "default": "Annual maintenance vote" },
  "startDate": "2027-07-01T09:00:00Z",
  "endDate": "2027-07-08T09:00:00Z",
  "questions": [
    {
      "title": { "default": "Repaint the fence?" },
      "choices": [
        { "title": { "default": "Yes" }, "value": 0 },
        { "title": { "default": "No" }, "value": 1 }
      ],
      "type": "singlechoice"
    }
  ]
}
JSON
)
6

Publish on-chain

Publishing is asynchronous and atomic (census + one election per question); poll the job until it completes. Voters then cast ballots client-side - see Casting votes.

PJOB=$(curl -s "${auth[@]}" -X POST "$B/processes/$PROCESS/publish" | jq -r .jobId)
# no jobId: the process was already published, or the publish was rejected
if [ -z "$PJOB" ] || [ "$PJOB" = "null" ]; then echo "no jobId - check the publish response" >&2; else
  for i in $(seq 150); do
    S=$(curl -s "$B/jobs/$PJOB" | jq -r .status)
    [ "$S" = "completed" ] && break
    if [ "$S" = "failed" ] || [ "$i" = 150 ]; then echo "publish job $PJOB: $S" >&2; break; fi
    sleep 2
  done
fi
7

Read the results

Public, no auth - addressed by the processId, one tally per question.

curl -s "$B/processes/$PROCESS/results" | jq

Next steps

Read Members and groups for bulk imports and the members-job, Census for auth types and per-question eligibility, Voting processes for the full authoring API, Casting votes for the client-side ballot flow, and Voting types for the four named ballot types (single choice, multichoice, ranked, cumulative) and the raw override.

The same flow with TypeScript, C# and Python

The bash steps above translate directly. The TypeScript variant uses the integrator SDK (@vocdoni/api-client); the C# and Python variants define the Post/Get helpers they reuse.

client setup - the SDK client, or the Post / Get helpers the flow reuses
import { VocdoniApiClient } from '@vocdoni/api-client'

const client = new VocdoniApiClient({
  apiUrl: 'https://saas-api-stg.vocdoni.net',
  authToken: process.env.VOCDONI_API_TOKEN,
})
using System.Net.Http.Json;
using System.Text.Json;

var http = new HttpClient { BaseAddress = new Uri("https://saas-api-stg.vocdoni.net") };
http.DefaultRequestHeaders.Authorization =
    new("Bearer", Environment.GetEnvironmentVariable("VOCDONI_API_TOKEN"));

async Task<JsonElement> Post(string path, object? body)
{
    var res = await http.PostAsJsonAsync(path, body);
    res.EnsureSuccessStatusCode(); // fail on an API error, like GetFromJsonAsync and raise_for_status
    return await res.Content.ReadFromJsonAsync<JsonElement>();
}
async Task<JsonElement> Get(string path) => await http.GetFromJsonAsync<JsonElement>(path);
import os, time, requests

B = "https://saas-api-stg.vocdoni.net"
s = requests.Session()
s.headers.update({"Authorization": f"Bearer {os.environ['VOCDONI_API_TOKEN']}",
                  "Content-Type": "application/json"})

def post(path, body=None): r = s.post(B + path, json=body); r.raise_for_status(); return r
def get(path):             r = s.get(B + path);             r.raise_for_status(); return r
full election flow - end to end
// 1. managed org
const { address: org } = await client.organizations.createManaged({
  name: 'Maple Street HOA',
  type: 'association',
})

// 2. member (async) -> wait for the members-job
const { jobId } = await client.organizations.addMembers(
  org,
  [{ name: 'Alice', memberNumber: 'A-101', email: 'alice@example.org' }], // weight defaults to 1
  { async: true },
)
if (jobId) await client.jobs.waitFor(jobId)

// 3. all-members group
const { id: group } = await client.organizations.createGroup(org, {
  title: 'All voters',
  includeAllMembers: true,
})

// 4. create the process draft (inline census + question) -> processId
const processId = await client.elections.create({
  orgAddress: org,
  census: { authFields: ['memberNumber'], groupId: group },
  title: 'Repaint the fence?',
  description: 'Annual maintenance vote',
  startDate: '2027-07-01T09:00:00Z',
  endDate: '2027-07-08T09:00:00Z',
  questions: [
    {
      title: 'Repaint the fence?',
      choices: [
        { title: 'Yes', value: 0 },
        { title: 'No', value: 1 },
      ],
      type: 'singlechoice',
    },
  ],
})

// 5. publish (async) -> wait for the job
// The wait gives up after 60s by default while the publish carries on; allow longer.
await client.elections.publishAndWait(processId, { timeoutMs: 5 * 60_000 })

// 6. results - one tally per question
console.log(await client.elections.getResults(processId))
// 1. managed org
var org = (await Post("/integrator/organizations",
    new { type = "association", meta = new { name = "Maple Street HOA" } })).GetProperty("address").GetString();

// 2. member (async) -> poll the members-job until progress == 100
var job = (await Post($"/organizations/{org}/members?async=true",
    new { members = new[] { new { name = "Alice", memberNumber = "A-101",
                                  email = "alice@example.org", weight = "1" } } })).GetProperty("jobId").GetString();
// completes at progress 100; a failed row keeps it pending, hence the deadline
for (var i = 1; (await Get($"/jobs/{job}")).GetProperty("status").GetString() != "completed"; i++)
{
    if (i == 120) throw new Exception($"members-job {job} did not complete");
    await Task.Delay(1000);
}

// 3. all-members group
var group = (await Post($"/organizations/{org}/groups",
    new { title = "All voters", includeAllMembers = true })).GetProperty("id").GetString();

// 4. create the process draft (inline census + question) -> { processId }
var process = (await Post("/processes", new {
    orgAddress = org,
    census = new { authFields = new[] { "memberNumber" }, groupId = group },
    title = new { @default = "Repaint the fence?" },
    description = new { @default = "Annual maintenance vote" },
    startDate = "2027-07-01T09:00:00Z", endDate = "2027-07-08T09:00:00Z",
    questions = new[] { new {
        title = new { @default = "Repaint the fence?" },
        choices = new[] { new { title = new { @default = "Yes" }, value = 0 },
                          new { title = new { @default = "No" },  value = 1 } },
        type = "singlechoice",
    }}})).GetProperty("processId").GetString();

// 5. publish (async) -> wait for the job
// no jobId on a 200: the process was already published (an API error throws in Post)
if ((await Post($"/processes/{process}/publish", null)).TryGetProperty("jobId", out var pj))
{
    var pjob = pj.GetString();
    for (var i = 1; ; i++)
    {
        await Task.Delay(2000);
        var status = (await Get($"/jobs/{pjob}")).GetProperty("status").GetString();
        if (status == "completed") break;
        if (status == "failed" || i == 150) throw new Exception($"publish job {pjob}: {status}");
    }
}

// 6. results - one tally per question
Console.WriteLine(await Get($"/processes/{process}/results"));
# 1. managed org
org = post("/integrator/organizations",
           {"type": "association", "meta": {"name": "Maple Street HOA"}}).json()["address"]

# 2. member (async) -> poll the members-job
job = post(f"/organizations/{org}/members?async=true",
           {"members": [{"name": "Alice", "memberNumber": "A-101",
                         "email": "alice@example.org", "weight": "1"}]}).json()["jobId"]
for _ in range(120):  # completes at progress 100; a failed row keeps it pending, hence the deadline
    if get(f"/jobs/{job}").json()["status"] == "completed":
        break
    time.sleep(1)
else:
    raise RuntimeError(f"members-job {job} did not complete")

# 3. all-members group
group = post(f"/organizations/{org}/groups",
             {"title": "All voters", "includeAllMembers": True}).json()["id"]

# 4. create the process draft (inline census + question) -> { processId }
process = post("/processes", {
    "orgAddress": org,
    "census": {"authFields": ["memberNumber"], "groupId": group},
    "title": {"default": "Repaint the fence?"},
    "description": {"default": "Annual maintenance vote"},
    "startDate": "2027-07-01T09:00:00Z", "endDate": "2027-07-08T09:00:00Z",
    "questions": [{"title": {"default": "Repaint the fence?"},
                   "choices": [{"title": {"default": "Yes"}, "value": 0},
                               {"title": {"default": "No"}, "value": 1}],
                   "type": "singlechoice"}]}).json()["processId"]

# 5. publish (async) -> wait for the job
pjob = post(f"/processes/{process}/publish").json().get("jobId")  # None: already published
for _ in range(150 if pjob else 0):
    status = get(f"/jobs/{pjob}").json()["status"]
    if status == "completed":
        break
    if status == "failed":
        raise RuntimeError(f"publish job {pjob} failed")
    time.sleep(2)
else:
    if pjob:
        raise RuntimeError(f"publish job {pjob} did not complete")

# 6. results - one tally per question
print(get(f"/processes/{process}/results").json())