Overview

The voting system provides RESTful API endpoints for both admin operations and public voting. All admin endpoints require authentication (see authentication).

Base URLs

  • Production: https://your-worker.workers.dev
  • Local Development: http://localhost:8787

Authentication

Admin Endpoints

All /admin/* endpoints require authentication:

Cloudflare Access (preferred):

  • Automatically handled by Cloudflare Zero Trust
  • No headers needed

API Key (fallback):

Authorization: Bearer YOUR_API_KEY

or

Authorization: YOUR_API_KEY

See authentication for details.

Admin API Endpoints

Elections

Create Election

POST /admin/elections

Request Body:

{
  "title": "Board President 2024",
  "description": "Election for board president",
  "open_at": "2024-01-01T00:00:00Z",
  "close_at": "2024-01-15T23:59:59Z",
  "ballot_type": "RANKED_CONDORCET",
  "settings": {
    "publish_results_to_home": true
  },
  "invite_mode": "batch"
}

Response:

{
  "success": true,
  "election": {
    "id": "elec_abc123",
    "title": "Board President 2024",
    ...
  }
}

List Elections

GET /admin/elections

Response:

{
  "elections": [
    {
      "id": "elec_abc123",
      "title": "Board President 2024",
      ...
    }
  ]
}

Get Election

GET /admin/elections/:id

Response:

{
  "election": {
    "id": "elec_abc123",
    "title": "Board President 2024",
    ...
  }
}

Update Election Dates

PATCH /admin/elections/:id/dates

Request Body:

{
  "open_at": "2024-01-01T00:00:00Z",
  "close_at": "2024-01-15T23:59:59Z"
}

Update Election Settings

PATCH /admin/elections/:id/settings

Request Body:

{
  "publish_results_to_home": true,
  "poll_response_type": "multiple"
}

Delete Election

DELETE /admin/elections/:id

Response:

{
  "success": true
}

Close Election

POST /admin/elections/:id/close

Response:

{
  "success": true
}

Tokens

Generate Tokens

POST /admin/elections/:id/tokens

Request Body:

{
  "count": 100,
  "batch_name": "Initial Batch"
}

Response:

{
  "success": true,
  "tokens_generated": 100,
  "batch_id": "batch_abc123"
}

List Token Batches

GET /admin/elections/:id/tokens

Response:

{
  "batches": [
    {
      "batch_name": "Initial Batch",
      "count": 100,
      "created_at": 1704067200000,
      "used_count": 45,
      "emails": ["voter1@example.com"]
    }
  ]
}

Candidates/Options

Add Candidate

POST /admin/elections/:id/candidates

Request Body:

{
  "name": "Alice Smith",
  "display_order": 1
}

Add Poll Option

POST /admin/elections/:id/options

Request Body:

{
  "name": "Option A",
  "display_order": 1
}

List Candidates/Options

GET /admin/elections/:id/candidates
GET /admin/elections/:id/options

Delete Option

DELETE /admin/elections/:id/options/:optionId

Invites

Send Invites

POST /admin/elections/:id/invite

Request Body:

{
  "emails": ["voter1@example.com", "voter2@example.com"],
  "distribution_list_ids": ["list_abc123"],
  "invite_mode": "batch",
  "queue": false
}

Response:

{
  "success": true,
  "results": [
    { "email": "voter1@example.com", "success": true },
    { "email": "voter2@example.com", "success": true }
  ],
  "mode": "batch"
}

Bulk Invites (Multiple Elections)

POST /admin/bulk-invites

Send invites for multiple elections to multiple recipients in one operation. This is more efficient than sending invites for each election individually.

Request Body:

{
  "election_ids": ["elec_123", "elec_456", "elec_789"],
  "emails": ["voter1@example.com", "voter2@example.com"],
  "distribution_list_ids": ["list_abc123"],
  "invite_mode": "batch",
  "queue": false
}

Parameters:

  • election_ids (required): Array of election IDs to send invites for
  • emails (optional): Array of email addresses
  • distribution_list_ids (optional): Array of distribution list IDs
  • invite_mode (optional): “batch” (default) or “individual”
  • queue (optional): If true, queue invites for upcoming elections

Response:

{
  "success": true,
  "mode": "batch",
  "queued": false,
  "summary": {
    "total": 10,
    "sent": 9,
    "failed": 1,
    "queued": 0
  },
  "results": [
    { "email": "voter1@example.com", "success": true },
    { "email": "voter2@example.com", "success": false, "error": "Invalid email format" }
  ]
}

Behavior:

  • Batch Mode: Creates invites for all selected elections, then sends one email per recipient listing all their elections
  • Individual Mode: Creates invites and sends separate emails for each election
  • Queue Mode: Creates invites with QUEUED status, to be sent when elections open
  • Validates all elections exist and are not closed
  • Automatically prevents duplicate invites
  • Updates all invites to SENT or FAILED based on email result

Use Cases:

  • Sending invites for multiple elections to the same group of people
  • Managing invites across multiple elections efficiently
  • Reducing the number of API calls needed

List Invites

GET /admin/elections/:id/invites

Response:

{
  "invites": [
    {
      "id": "inv_abc123",
      "email": "voter@example.com",
      "status": "SENT",
      "created_at": 1704067200000,
      "sent_at": 1704067300000
    }
  ]
}

Resend Invite

POST /admin/elections/:id/invites/:inviteId/resend

Send Results Email

POST /admin/elections/:id/results-email

Distribution Lists

List Distribution Lists

GET /admin/distribution-lists

Get Distribution List

GET /admin/distribution-lists/:id

Create Distribution List

POST /admin/distribution-lists

Request Body:

{
  "name": "Board Members",
  "description": "All current board members"
}

Update Distribution List

PUT /admin/distribution-lists/:id

Delete Distribution List

DELETE /admin/distribution-lists/:id

Add Emails to List

POST /admin/distribution-lists/:id/emails

Request Body:

{
  "emails": ["member1@example.com", "member2@example.com"]
}

Remove Email from List

DELETE /admin/distribution-lists/:id/emails/:emailId

Notion Integration

List Databases

GET /admin/notion/databases

Response:

{
  "databases": [
    {
      "id": "abc123",
      "name": "Elections Database"
    }
  ]
}

Search Pages

GET /admin/notion/databases/:databaseId/pages?query=search+term

Validate Page

GET /admin/notion/pages/:pageId/validate?property_name=Voting+Status
POST /admin/elections/:id/notion/link

Request Body:

{
  "database_id": "abc123",
  "page_id": "def456",
  "status_property_name": "Voting Status"
}
DELETE /admin/elections/:id/notion/link

Sync with Notion

POST /admin/elections/:id/notion/sync

Audit Logs

Get Audit Logs

GET /admin/audit-logs?limit=100&offset=0&action=vote.submitted&election_id=elec_123

Response:

{
  "logs": [
    {
      "id": "log_abc123",
      "action": "vote.submitted",
      "ip_address": "192.168.1.1",
      "election_id": "elec_123",
      "timestamp": 1704067200000,
      "metadata": "{\"ballot_type\":\"SIMPLE_TRIPLE\"}"
    }
  ],
  "total": 150
}

Public API Endpoints

Voting

Get Election Info

GET /e/:id

Query Parameters:

  • t: Token (optional, can be entered on page)

Response: HTML page

Get Voting Page

GET /e/:id/vote

Query Parameters:

  • t: Token (optional)

Response: HTML page

Submit Vote

POST /e/:id/vote

Request Body (Simple):

{
  "token": "voter_token_here",
  "choice": "YES"
}

Request Body (Ranked):

{
  "token": "voter_token_here",
  "ranking": ["cand1", "cand2", "cand3"]
}

Request Body (Poll):

{
  "token": "voter_token_here",
  "selected_option": "opt1"
}

or

{
  "token": "voter_token_here",
  "selected_options": ["opt1", "opt2"]
}

Response:

{
  "success": true,
  "message": "Vote recorded successfully"
}

Get Poll Voting Page

GET /e/:id/vote/poll

Submit Poll Vote

POST /e/:id/vote/poll

Results

Get Results

GET /e/:id/results

Query Parameters:

  • refresh: Force refresh (bypass cache)

Response: HTML page or JSON (if Accept: application/json)

Get My Elections Page

GET /vote/my-elections?email=voter@example.com&token=magic_token_here

Response: HTML page with all elections for the email

Rate Limiting

All endpoints are rate-limited. See rate-limiting for details.

Rate Limit Headers:

  • X-RateLimit-Limit: Maximum requests
  • X-RateLimit-Remaining: Remaining requests
  • X-RateLimit-Reset: Reset timestamp

Rate Limit Exceeded:

  • Status: 429 Too Many Requests
  • Body: { "error": "Rate limit exceeded", "retryAfter": 45 }

Error Responses

Standard Error Format

{
  "error": "Error message",
  "message": "Detailed error description"
}

Common Status Codes

  • 200 OK: Success
  • 400 Bad Request: Invalid request
  • 401 Unauthorized: Authentication required
  • 404 Not Found: Resource not found
  • 429 Too Many Requests: Rate limit exceeded
  • 500 Internal Server Error: Server error

See Also