NAV
ruby python php

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 all elections belonging to an organization, including sister organizations (i.e. sharing the same billing 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.

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 in eid, exp, mid, and signature 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.

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.

a UI for configuring overall webhook settings

If enabled on an election-by-election basis, the webhooks can be configured on the voter list section fo the vote setup.

a UI for configuring webhook settings for a single vote

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. the user interface to choose your voter list importation method

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. the user interface to with option to include all columns in import

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. the user interface to showing list of eligible voters

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.

radio buttons offering three choices: Secret Ballot, Poll, and Show of Hands

There are six types of questions corresponding to the 6 voting system options available when adding Positions/Questions.

A drop down offering 6 choices: Plurality, Cumulative, Preferential, Approval, Nomination, and Scored

Correspondingly, The "type" field on each object in the "questions" array will have one of the following values:

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:

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.