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_KEYor
Authorization: YOUR_API_KEYSee authentication for details.
Admin API Endpoints
Elections
Create Election
POST /admin/electionsRequest 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/electionsResponse:
{
"elections": [
{
"id": "elec_abc123",
"title": "Board President 2024",
...
}
]
}Get Election
GET /admin/elections/:idResponse:
{
"election": {
"id": "elec_abc123",
"title": "Board President 2024",
...
}
}Update Election Dates
PATCH /admin/elections/:id/datesRequest Body:
{
"open_at": "2024-01-01T00:00:00Z",
"close_at": "2024-01-15T23:59:59Z"
}Update Election Settings
PATCH /admin/elections/:id/settingsRequest Body:
{
"publish_results_to_home": true,
"poll_response_type": "multiple"
}Delete Election
DELETE /admin/elections/:idResponse:
{
"success": true
}Close Election
POST /admin/elections/:id/closeResponse:
{
"success": true
}Tokens
Generate Tokens
POST /admin/elections/:id/tokensRequest Body:
{
"count": 100,
"batch_name": "Initial Batch"
}Response:
{
"success": true,
"tokens_generated": 100,
"batch_id": "batch_abc123"
}List Token Batches
GET /admin/elections/:id/tokensResponse:
{
"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/candidatesRequest Body:
{
"name": "Alice Smith",
"display_order": 1
}Add Poll Option
POST /admin/elections/:id/optionsRequest Body:
{
"name": "Option A",
"display_order": 1
}List Candidates/Options
GET /admin/elections/:id/candidates
GET /admin/elections/:id/optionsDelete Option
DELETE /admin/elections/:id/options/:optionIdInvites
Send Invites
POST /admin/elections/:id/inviteRequest 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-invitesSend 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 foremails(optional): Array of email addressesdistribution_list_ids(optional): Array of distribution list IDsinvite_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/invitesResponse:
{
"invites": [
{
"id": "inv_abc123",
"email": "voter@example.com",
"status": "SENT",
"created_at": 1704067200000,
"sent_at": 1704067300000
}
]
}Resend Invite
POST /admin/elections/:id/invites/:inviteId/resendSend Results Email
POST /admin/elections/:id/results-emailDistribution Lists
List Distribution Lists
GET /admin/distribution-listsGet Distribution List
GET /admin/distribution-lists/:idCreate Distribution List
POST /admin/distribution-listsRequest Body:
{
"name": "Board Members",
"description": "All current board members"
}Update Distribution List
PUT /admin/distribution-lists/:idDelete Distribution List
DELETE /admin/distribution-lists/:idAdd Emails to List
POST /admin/distribution-lists/:id/emailsRequest Body:
{
"emails": ["member1@example.com", "member2@example.com"]
}Remove Email from List
DELETE /admin/distribution-lists/:id/emails/:emailIdNotion Integration
List Databases
GET /admin/notion/databasesResponse:
{
"databases": [
{
"id": "abc123",
"name": "Elections Database"
}
]
}Search Pages
GET /admin/notion/databases/:databaseId/pages?query=search+termValidate Page
GET /admin/notion/pages/:pageId/validate?property_name=Voting+StatusLink Election to Notion
POST /admin/elections/:id/notion/linkRequest Body:
{
"database_id": "abc123",
"page_id": "def456",
"status_property_name": "Voting Status"
}Unlink Election from Notion
DELETE /admin/elections/:id/notion/linkSync with Notion
POST /admin/elections/:id/notion/syncAudit Logs
Get Audit Logs
GET /admin/audit-logs?limit=100&offset=0&action=vote.submitted&election_id=elec_123Response:
{
"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/:idQuery Parameters:
t: Token (optional, can be entered on page)
Response: HTML page
Get Voting Page
GET /e/:id/voteQuery Parameters:
t: Token (optional)
Response: HTML page
Submit Vote
POST /e/:id/voteRequest 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/pollSubmit Poll Vote
POST /e/:id/vote/pollResults
Get Results
GET /e/:id/resultsQuery Parameters:
refresh: Force refresh (bypass cache)
Response: HTML page or JSON (if Accept: application/json)
My Elections (Magic Link)
Get My Elections Page
GET /vote/my-elections?email=voter@example.com&token=magic_token_hereResponse: 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 requestsX-RateLimit-Remaining: Remaining requestsX-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: Success400 Bad Request: Invalid request401 Unauthorized: Authentication required404 Not Found: Resource not found429 Too Many Requests: Rate limit exceeded500 Internal Server Error: Server error
See Also
- authentication - Authentication details
- voting-system - Voting flow
- rate-limiting - Rate limit details