Introduction
Welcome to the Voting Integrations (VI) documentation for ElectionBuddy (electionbuddy.com). We continue to maintain both our version 1 (or V1) integrations, as well our version 2 (or V2) voting integrations to allow your voters to authenticate with ElectionBuddy within your own member or customer portal or customer relationship management (CRM) software.
V1 focuses on providing you with a method to generate a signed URL to send your voters to a specific election.
V2 expands on this by allowing you to give your voters a list of all running elections in which they are eligible to vote, and allowing them to vote in any or all of them in turn while being authenticated behind your customer portal.
Webhooks, in contrast, sends requests to a URL provided by you. This allows integration with services that support receiving webhooks, or possibly your own web server, if configured appropriately.
Version 2
Running Elections List Widget
The function/method below will return templated HTML in the form of a
<ul>
list of elections that the current voter is eligible to vote in. It will search allelections
belonging to anorganization
, including sisterorganization
s (i.e. sharing the samebilling account
).
require 'addressable/uri'
require 'base64'
require 'openssl'
require 'faraday'
#####
# Parameters:
# `oid` - your Organization's ID (available from https://secure.electionbuddy.com/organizations)
# `exp` - Unix epoch timestamp (we validate this to within +/- 30 seconds of current time)
# `identifier` - the Voter's identifier for which to search for eligible elections
# `secret_token` - your Organizations Secret Token (https://secure.electionbuddy.com/organizations)
# `signature` - a Base64, HMAC-SHA256 signature of the above parameters in order (exp, identifier, oid)
#
# Returns HTML:
#
#####
def get_election_list(oid, identifier, secret_token)
url = 'https://secure.electionbuddy.com/integrations/v2/elections'
query_values = {
exp: Time.now.to_i,
identifier: identifier,
oid: oid
}
uri = Addressable::URI.parse(url)
uri.query_values = query_values
message = uri.query
signature = Base64.urlsafe_encode64(
OpenSSL::HMAC.digest('sha256', secret_token, message)
)
uri.query_values = query_values.merge(signature: signature)
Faraday.get(uri).body
end
import base64
import hashlib
import hmac
import time
import requests
from urllib.parse import urlparse, urlencode
#####
# Parameters:
# `oid` - your Organization's ID (available from https://secure.electionbuddy.com/organizations)
# `exp` - Unix epoch timestamp (we validate this to within +/- 30 seconds of current time)
# `identifier` - the Voter's identifier for which to search for eligible elections
# `secret_token` - your Organizations Secret Token (https://secure.electionbuddy.com/organizations)
# `signature` - a Base64, HMAC-SHA256 signature of the above parameters in order (exp, identifier, oid)
#
# Returns HTML:
#
#####
def get_election_list(oid, identifier, secret_token):
url = 'https://secure.electionbuddy.com/integrations/v2/elections'
query_values = {
'exp' : int(time.time()),
'identifier' : identifier,
'oid' : oid
}
message = urlencode(query_values)
signature = base64.urlsafe_b64encode(
hmac.new(secret_token.encode(), message.encode(), hashlib.sha256).digest()
).decode()
query_values['signature'] = signature
message = urlencode(query_values)
uri = url + '?' + message
return requests.get(uri).content
<?
#####
# Parameters:
# `oid` - your Organization's ID (available from https://secure.electionbuddy.com/organizations)
# `exp` - Unix epoch timestamp (we validate this to within +/- 30 seconds of current time)
# `identifier` - the Voter's identifier for which to search for eligible elections
# `secret_token` - your Organizations Secret Token (https://secure.electionbuddy.com/organizations)
# `signature` - a Base64, HMAC-SHA256 signature of the above parameters in order (exp, identifier, oid)
#
# Returns HTML:
#
#####
function get_election_list($oid, $identifier, $secret_token) {
$url = 'https://secure.electionbuddy.com/integrations/v2/elections';
$query_values = array(
'exp' => time(),
'identifier' => $identifier,
'oid' => $oid
);
$message = http_build_query($query_values);
$signature = strtr(base64_encode(hash_hmac('sha256', $message, $secret_token, true)), '+/', '-_');
$query_values['signature'] = $signature;
$message = http_build_query($query_values);
$uri = $url . '?' . $message;
return file_get_contents($uri);
}
?>
This endpoint authenticates a voting request for a particular organization, and returns an HTML list (<ul>
) with links to vote.
HTTP Request
GET https://secure.electionbuddy.com/integrations/v2/elections?{parameters}
Query Parameters
Parameter | Description |
---|---|
exp | Request expiration date, in Unix Epoch Time. This time must be +/- 30 seconds from the time the request is received. |
identifier | Member ID: A unique identifier for the member who is voting, such as an email address, or the identifier when you uploaded your voter details for an election. This MUST NOT be the database primary key for this voter, as this is not unique across elections. |
oid | Your Organization's ID (available from https://secure.electionbuddy.com/organizations) |
signature | Generated signature using secret_token . See below. |
- All requests must be signed by
signature
appended (as the last parameter) to the query string, and generated from the formatted query string consisting ofoid
,exp
, andidentifier
. - Signatures must be generated using HMAC-SHA256 using the
secret_token
for your Organization in your ElectionBuddy account. - All other parameters should appear alphabetically in the query string (i.e.
exp
,identifier
,oid
).
Voter Status
Below is an example request to obtain voter status:
require 'net/http'
require 'addressable/uri'
require 'base64'
require 'openssl'
require 'json'
#####
# Parameters:
# `oid` - your Organization's ID (available from https://secure.electionbuddy.com/organizations)
# `exp` - Unix epoch timestamp (we validate this to within +/- 30 seconds of current time)
# `identifier` - the Voter's identifier for which to search for eligible elections
# `secret_token` - your Organizations Secret Token (https://secure.electionbuddy.com/organizations)
# `signature` - a Base64, HMAC-SHA256 signature of the above parameters in order (exp, identifier, oid)
#
# Returns HTML:
#
#####
def get_voter_status(oid, identifier, secret_token)
url = 'https://secure.electionbuddy.com/integrations/v2/elections'
headers = { 'Accept' => 'application/json' }
query_values = {
exp: Time.now.to_i,
identifier: identifier,
oid: oid
}
uri = Addressable::URI.parse(url)
uri.query_values = query_values
message = uri.query
signature = Base64.urlsafe_encode64(
OpenSSL::HMAC.digest('sha256', secret_token, message)
)
uri.query_values = query_values.merge(signature: signature)
net = Net::HTTP.new(uri.host, 443)
net.use_ssl = true
net.verify_mode = OpenSSL::SSL::VERIFY_PEER
response = net.post(uri.path, uri.query, headers)
JSON.parse(response.body)
end
The same endpoint can be used to obtain a voter's status, with an Accept
header of application/json
. This can be used, for example, to send out your own custom vote reminders to those voters who have not yet voted.
HTTP Request
GET https://secure.electionbuddy.com/integrations/v2/elections?{parameters}
(Header: Accept: application/json
)
HTTP Code | Response Body | Meaning |
---|---|---|
200 | [{ "id" : 123, "name" : "Running Election 1", "start_date" : "2021-02-10T22:35:00.000-06:00", "end_date" : "2021-02-12T12:00:00.000-06:00", "aasm_state" : "running" }, { "id" : 124, "name" : "Running Election 2", "start_date" : "2021-02-10T22:35:00.000-06:00", "end_date" : "2021-02-12T12:00:00.000-06:00", "aasm_state" : "running" }] |
The only valid election state for voting is running : all other states mean that either voting has not begun yet, or has ended. |
Version 1
Election Widget
The function/method below will generate a signed anchor
<a>
element that you can use on your organization's internal dashboard (i.e. after a member/voter logs in). You can style it as you wish to match your organization's design or theme. You'll need to pass ineid
,exp
,mid
, andsignature
as appropriate.
require 'addressable/uri'
require 'base64'
require 'openssl'
##### Example values:
# eid = '12'
# mid = 'jane@example.com'
# secret_key = 'N+vlebJgl/Lkxtu2b4hOe+JUTpVm5arWGJbQ6U7BOFs='
#####
def generate_vote_anchor(secret_key, eid, mid)
url = 'https://secure.electionbuddy.com/integrations/v1/sso'
query_values = {
eid: eid,
exp: Time.now.to_i,
mid: mid
}
uri = Addressable::URI.parse(url)
uri.query_values = query_values
message = uri.query
signature = Base64.urlsafe_encode64(
OpenSSL::HMAC.digest('sha256', secret_key, message)
)
uri.query_values = query_values.merge(signature: signature)
"<a href='#{uri}' class='electionbuddy-vote-button'>Vote now</a>"
end
import base64
import hashlib
import hmac
import time
from urllib.parse import urlparse, urlencode
##### Example values:
# eid = '12'
# mid = 'jane@example.com'
# secret_key = 'N+vlebJgl/Lkxtu2b4hOe+JUTpVm5arWGJbQ6U7BOFs='
#####
def generate_vote_anchor(secret_key, eid, mid):
url = 'https://secure.electionbuddy.com/integrations/v1/sso'
query_values = {
'eid' : eid,
'exp' : int(time.time()),
'mid' : mid
}
message = urlencode(query_values)
signature = base64.urlsafe_b64encode(
hmac.new(secret_key.encode(), message.encode(), hashlib.sha256).digest()
).decode()
query_values['signature'] = signature
message = urlencode(query_values)
uri = url + '?' + message
return "<a href='" + uri + "' class='electionbuddy-vote-button'>Vote now</a>"
<?
##### Example values:
# $eid = '12'
# $mid = 'jane@example.com'
# $secret_key = 'N+vlebJgl/Lkxtu2b4hOe+JUTpVm5arWGJbQ6U7BOFs='
#####
function generate_vote_anchor($secret_key, $eid, $mid) {
$url = 'https://secure.electionbuddy.com/integrations/v1/sso';
$query_values = array(
'eid' => $eid,
'exp' => time(),
'mid' => $mid
);
$message = http_build_query($query_values);
$signature = strtr(base64_encode(hash_hmac('sha256', $message, $secret_key, true)), '+/', '-_');
$query_values['signature'] = $signature;
$message = http_build_query($query_values);
$uri = $url . '?' . $message;
return ("<a href='" . $uri . "' class='electionbuddy-vote-button'>Vote now</a>");
}
?>
This endpoint authenticates a voting request for a particular user (for example, from your organization's internal dashboard). If the signature is valid and the voter (identified by mid
) has yet to vote, they will be forwarded to a fresh ballot for the election eid
.
HTTP Request
GET https://secure.electionbuddy.com/integrations/v1/sso?{parameters}
Query Parameters
Parameter | Description |
---|---|
eid | Election ID: when editing your election, your election ID is the numeral that appears in the URL. (1234 in https://secure.electionbuddy.com/elections/1234/edit ) |
exp | Request expiration date, in Unix Epoch Time. This time must be +/- 5 minutes from the time the request is received. If exp is older than 5 minutes, the voter will be directed to a "Link Expired" page. |
mid | Member ID: A unique identifier for the member who is voting, such as an email address, or a membership ID, or a database primary key. This string can be anything that you can guarantee is unique for each of your voters. If a ballot attached to this member ID is already on your ElectionBuddy election voter list, this ballot will be shown to the authenticated voter. If there is no voter on your ElectionBuddy election voter list with this member ID, a fresh ballot will be created with an anonymized ballot ID. |
signature | Generated signature using secret_key . See below. |
- All requests must be signed by
signature
appended (as the last parameter) to the query string, and generated from the formatted query string consisting ofeid
,exp
, andmid
. - Signatures must be generated using HMAC-SHA256 using the
secret_key
for your election in your ElectionBuddy account. Secret keys are unique to yourElection
. - All other parameters should appear alphabetically in the query string (i.e.
eid
,exp
,mid
).
Voter Status
Below is an example request to obtain voter status:
require 'net/http'
require 'addressable/uri'
require 'base64'
require 'openssl'
require 'json'
##### Example values:
# eid = '12'
# mid = 'jane@example.com'
# secret_key = 'N+vlebJgl/Lkxtu2b4hOe+JUTpVm5arWGJbQ6U7BOFs='
#####
def get_voter_status(secret_key, eid, mid)
url = 'https://secure.electionbuddy.com/sso'
headers = { 'Accept' => 'application/json' }
query_values = {
eid: eid,
exp: Time.now.to_i,
mid: mid
}
uri = Addressable::URI.parse(url)
uri.query_values = query_values
message = uri.query
signature = Base64.urlsafe_encode64(
OpenSSL::HMAC.digest('sha256', secret_key, message)
)
uri.query_values = query_values.merge(signature: signature)
net = Net::HTTP.new(uri.host, 443)
net.use_ssl = true
net.verify_mode = OpenSSL::SSL::VERIFY_PEER
response = net.post(uri.path, uri.query, headers)
JSON.parse(response.body)
end
The same endpoint can be used to obtain a voter's status. This can be used, for example, to send out your own custom vote reminders to those voters who have not yet voted.
Voter status requests use the same parameters as above, but will request a JSON response (header of: Accept: application/json
).
HTTP Request
GET https://secure.electionbuddy.com/sso?{parameters}
(Header: Accept: application/json
)
HTTP Code | Response Body | Meaning |
---|---|---|
200 | { 'voted' : true, 'election_state' : 'running' } |
The only valid election state for voting is running : all other states mean that either voting has not begun yet, or has ended. |
Errors
The ElectionBuddy APIs return the following error codes:
Error code | Meaning |
---|---|
422 | Unprocessable Entity -- Your request is valid but we were unable to create a ballot based on your request. This usually happens when your election has reached its maximum number of voters. |
404 | Not Found -- The election does not exist, the voter is not found, or the voter is not authorized. |
Webhooks
Our webhooks feature allows a URL, supplied by you, to receive HTTP POST requests corresponding to events related to your vote(s). For example, when individual votes are recorded.
The URL is configured through the Electionbuddy website, if enabled on your account. To enable webhooks contact an Electionbuddy customer support representative.
Once enabled, individual webhooks can be enabled for all elections, or enabled or disabled on an election-by-election basis, in the organization settings.
If enabled on an election-by-election basis, the webhooks can be configured on the voter list section fo the vote setup.
Payload Overview
{
"organization_id": 1234,
"organization_name": "My organization",
"organization_owner_name": "Jane Doe",
"organization_owner_email": "support@electionbuddy.com",
"election_id": 12345,
"election_name": "My election",
"identifier": "1",
"email": "voter@example.com",
"sms": "555-5555",
"postal": "1, 3984 Massachusetts Avenue, Washington DC",
"status": [
"Voted",
"Key Surfaced"
],
"opened_at": 1600000000,
"completed_at": 1600001234,
"added_at": null,
"spoiled_at": null,
"spoil_reason": null,
"key_surfaced_at": null,
"ballot_printed_at": null,
"ip": "0.0.0.0",
"unsubscribed_at": null
"voter_data": {
// keys and values for the voter added when using the
// CSV upload feature on the "Voters" step of vote setup.
}
// Non-exhaustive.
}
The HTTP POST requests will contain a JSON payload. The payload contains information about your vote, the organization associated with that vote, and the current status of the vote. The parts of the payload will be different depending on your election configuration.
Voter information, such as their postal address, SMS number and email address will be included if that information is included in the voter list.
The identifier
field also contains a string that was entered into the voter
list. Specifically, the identifier
field contains what was entered into the
Key
column for medium integrity elections, (including meeting votes), or the
ID
column for all other election types.
The opened_at
, completed_at
, etc. fields represent instants in time when the
given event happened. If the given event has happened then the value with be a
UNIX timestamp. That is, a number of seconds after January 1st, 1970.
If the given event has not happened, the value will be null
.
voter_data
Attribute
During setup of your vote, use the "Import from a CSV" option to create your list of eligible voters in the "Voters" step of setup. The UI is pictured below.
After mapping the CSV columns to the required properties, select the "All Columns" option, before you click the "Import" button. The option is circled in red in the image below.
A CSV with column names "CRM Identifier", "Password", "Full Name", "Employer", "Department" and "Email", with only the "Email" column mapped to a required field will resemble the eligible voter list in the image below.
When one of these eligble voters completes a ballot the webhook payload will
have a property voter_data
resembeling the structure shown on the right:
{
"organization_id": 1234,
// other properties
"voter_data": {
"email": "ouch@example.com",
"employer": "Acme Inc.",
"password": "foobar",
"full_name": "Coyote W",
"department": "Demolition",
"crm_identifier":"1234"
}
}
Voter Choices Overview
{
// ... other fields ...
"questions": [
{
"question": "Bylaw Amendment Approval of Article XLII",
"type": "plurality",
"answers": {
"regular": [
{
"title": "Yes - I approve the amendments",
"choice": "true"
}
],
"write_ins": []
}
}
]
}
If your Voter Anonymity settings allow administrators to view voter choices, then voter choices will be included in the payload.
There are six types of questions corresponding to the 6 voting system options available when adding Positions/Questions.
Correspondingly, The "type"
field on each object in the "questions"
array
will have one of the following values:
"plurality"
"cumulative"
"preferential"
"approval"
"nomination"
"scored"
The "answers"
field will have slightly different contents depending on the
"type"
. The overal shape will be the same but the "choice"
fields,
on the objects inside the "regular"
and/or "write_ins"
arrays, will have
different possible values and the proper way to interpret those values will be
different.
"title"
will always contain the name of the option the voter selected. For
example, the name of the candidate.
If write-in answers are allowed, (not available for all question types), then
any write-in answers will appear in the "write_ins"
array. All other answers
will show up in the "regular"
array. The written in answer will appear in the
"title"
field.
If multiple options were selected then multiple objects can show up in the appropriate array.
Plurality
{
// ... other fields ...
"questions": [
{
"question": "Council Leader",
"type": "plurality",
"answers": {
"regular": [
{
"title": "Jane Doe",
"choice": "true"
},
{
"title": "John Doe",
"choice": "true"
}
],
"write_ins": [
{
"title": "A. N. Other",
"choice": "true"
}
]
}
}
]
}
For this question type, "choice"
will always be "true"
, assuming abstaining
is not allowed. Otherwise, see the Abstention section.
Cumulative
{
// ... other fields ...
"questions": [
{
"question": "Council Leader",
"type": "cumulative",
"answers": {
"regular": [
{
"title": "Jane Doe",
"choice": "2"
},
{
"title": "John Doe",
"choice": "1"
}
],
"write_ins": [
{
"title": "A. N. Other",
"choice": "1"
}
]
}
}
]
}
For this question type, "choice"
will be a number, (represented as a string),
assuming abstaining is not allowed. Otherwise, see
the Abstention section.
The numbers here represents how many votes the voter assigned to the option.
Options with zero votes assigned will not show up.
Preferential
{
// ... other fields ...
"questions": [
{
"question": "Council Leader",
"type": "preferential",
"answers": {
"regular": [
{
"title": "Jane Doe",
"choice": "2"
},
{
"title": "John Doe",
"choice": "1"
}
],
"write_ins": [
{
"title": "A. N. Other",
"choice": "3"
}
]
}
}
]
}
For this question type, "choice"
will be a number, (represented as a string),
assuming abstaining is not allowed. Otherwise, see
the Abstention section.
The numbers here represents the relative ordering of options that was selected by the voter. Note that, unlike the numbering on the ballot, larger numbers indicate more preferred options, and lower numbers indicate less preferred options.
Approval
{
// ... other fields ...
"questions": [
{
"question": "Council Leader",
"type": "approval",
"answers": {
"regular": [
{
"title": "Jane Doe",
"choice": "true"
},
{
"title": "John Doe",
"choice": "true"
}
],
"write_ins": []
}
}
]
}
For this question type, "choice"
will always be "true"
, assuming abstaining
is not allowed. Otherwise, see the Abstention section.
Note that write-ins are not permitted for Approval voting, but we still send
down a "write_ins"
array.
Nomination
{
// ... other fields ...
"questions": [
{
"question": "Council Leader",
"type": "nomination",
"answers": {
"regular": [],
"write_ins": [
{
"title": "Jane Doe",
"choice": "true"
},
{
"title": "A. N. Other",
"choice": "true"
}
]
}
}
]
}
For this question type, "choice"
will always be "true"
, assuming abstaining
is not allowed. Otherwise, see the Abstention section.
Note that regular votes do not occur for Nomination voting, except for when a
voter abstains, (if enabled) but we still send down
a "regular"
array.
Scored
{
// ... other fields ...
"questions": [
{
"question": "Member Feedback: $4300 Excess Budget Spending",
"type": "scored",
"answers": {
"regular": [
{
"title": "Increase security/surveillance hours",
"choice": "3"
},
{
"title": "Hire a caterer for the Annual General Meeting",
"choice": "5"
}
],
"write_ins": []
}
}
]
}
For this question type, "choice"
will either be a number, (represented as a
string), or the string "not_applicable"
, assuming abstaining is not allowed.
Otherwise, see the Abstention section. The string
"not_applicable"
only shows up if the question allows that as an option.
The numbers here directly correspond to the numbers the voter selected on the
ballot. Multiple scores will appear within the same object in the "questions"
array, if multiple scores are asked for on the same question.
The default scale, without customization, is as follows:
- 1: Strongly disagree
- 2: Disagree
- 3: Neither agree nor disagree
- 4: Agree
- 5: Strongly agree
Note that write-ins are not permitted for Scored voting, but we still send
down a "write_ins"
array.
Abstention
{
// ... other fields ...
"questions": [
{
"question": "Executive Board",
"type": "nomination",
"answers": {
"regular": [
{
"title": "abstain",
"choice": "abstain"
}
],
"write_ins": []
}
}
],
}
If a question allows abstaining, then no matter what type of question it is,
when a voter abstains, then an object with a "choice"
field with the value
"abstain"
will be placed in the "regular"
array.