Election Data Feed Usage

Election results data feed

The City of Chilliwack publishes its election results as a machine-readable feed in JSON and XML. It is anonymous: no API key, no registration and no quota, within a published rate limit.

The feed carries the same figures as the City's published results statement and nothing more. It changes only when the City publishes or republishes results, and never exposes figures that have not been published.

Endpoints

All four are GET, anonymous, and return HTTP 200. The two without an id serve whichever election is currently published.

The four feed endpoints
Format Returns URL
JSON The election currently published https://api.chilliwack.com/v1/election-results/export/json Open this feed
XML The election currently published https://api.chilliwack.com/v1/election-results/export/xml
JSON One specific election https://api.chilliwack.com/v1/election-results/export/json/{electionId}
XML One specific election https://api.chilliwack.com/v1/election-results/export/xml/{electionId}

Replace {electionId} with an id from the table below — keep the braces out.

Finding an election id

Use an election id only for an older election. The endpoints without an id follow whichever election the City has published.

These elections have published results right now:

Elections with published results
Election Date Election id
Media Feed Test Election 2026-10-03 D15E7E57-0000-4000-8000-000000002026 Open as JSON

Read from the API as this page loads; published elections only. An election set up but not yet published does not appear here.

Fields

JSON and XML render the same document. Field names, nesting and ordering are identical. Three things differ:

  • Types. Every XML value is element text — a number as 9711, a boolean as true or false — so cast them yourself.
  • Ids are attributes. electionId, raceId, candidateId and locationId become an id attribute on the corresponding element. format and version are attributes on the <electionResults> root.
  • Null. A JSON null is an empty element, not a missing one. Test for empty content, not absence.

generatedAt and publishedAt are ISO-8601 UTC with the Z suffix, e.g. 2026-10-18T03:42:00Z; parse them as an instant. The City's own results page and printed statement show the same instant in Chilliwack local time (Pacific), so 03:42Z appears there as 8:42 PM the previous evening.

Fields are always present unless a row says otherwise. These tables are authoritative, more so than the samples further down.

Top level

The document root. In XML these are children of <electionResults>, except format and version, which are attributes ON that element.

Top level fields
Field Type Can be null Notes
format string No Always chilliwack-election-results. Absent from the not-published response.
version string No Contract version, currently 1.0.
generatedAt string No When this response was rendered. Changes on every request, so not a signal of new figures.
publishedAt string Yes When the figures were last published. Null means no publish time recorded -- unknown, not never.
election object No Which election this is.
totals object No Election-wide figures.
races array No One entry per race. Empty array if the snapshot carries none.
locations array No One entry per voting location. May be empty.

election

Identifies the election the figures belong to.

election fields
Field Type Can be null Notes
electionId string No The id for the /{electionId} URLs. In XML, the id attribute on <election>.
name string No Unset arrives as an empty string, never null.
date string No Election day, as YYYY-MM-DD.
statusName string Yes The stage these figures are published under, e.g. Preliminary Results. Null if no stage named.

totals

Election-wide figures, read from the published snapshot -- not summed from locations[].

totals fields
Field Type Can be null Notes
ballotsCastTotal number No Ballots cast across the election.
stationsReporting number No Voting locations that have reported, election-wide.
stationCount number No Voting locations in the election.
machinesReporting number No Tabulators that have reported, election-wide.
machineCount number No Tabulators in the election.

races[]

One race. The four reporting figures repeat at race level and legitimately differ from totals.

races[] fields
Field Type Can be null Notes
raceId string No Stable id. In XML, the id attribute on <race>; the same id appears in locations[].races[].
name string No The race name as published.
seatsToElect number No How many seats this race fills.
totalVotes number No Votes cast in this race. Divide a candidate's votes by this figure for a share of the vote.
stationsReporting number No Voting locations reporting FOR THIS RACE.
stationCount number No Voting locations that vote in this race.
machinesReporting number No Tabulators reporting for this race.
machineCount number No Tabulators for this race.
candidates array No The candidates, in VOTE order, most votes first.

races[].candidates[]

One candidate's election-wide result in one race.

races[].candidates[] fields
Field Type Can be null Notes
candidateId string No Stable id. In XML, the id attribute on <candidate>; the same id appears in every location.
name string No As it appeared on the ballot.
votes number No Votes across the whole election. Divide by the race's totalVotes for a share of the vote.
ballotOrder number No The candidate's position on the printed ballot.
rank number No 1 is the most votes in this race. Ties broken by ballot order.
isElected boolean No True when the City has determined this candidate holds a seat. Render the flag; do not derive winners from the vote column.
isTiedAtBoundary boolean No True when this candidate is in an unresolved tie across the last available seat, so the seat is NOT final. See Elected and tie flags below.

locations[]

One voting location's breakdown.

locations[] fields
Field Type Can be null Notes
locationId string No Stable id for the location. In XML, the id attribute on <location>.
name string No The location's name.
shortName string Yes A shorter label for this location, for tight layouts. Null when none is set -- fall back to name.
isAdvanceVoting boolean No True when this location is an advance voting location rather than an election-day-only one.
ballotsCast number Yes Ballots cast at this location. Null until a tabulator has reported a figure; null is NOT zero.
races array No Only the races this location votes in. Others are ABSENT, not zeroed.

locations[].races[]

One race as counted at one location.

locations[].races[] fields
Field Type Can be null Notes
raceId string No Matches a raceId in the top-level races array.
name string No The race name.
candidates array No The candidates, in BALLOT order -- not vote order.

locations[].races[].candidates[]

One candidate at one location. No rank or elected flag: those are election-wide only.

locations[].races[].candidates[] fields
Field Type Can be null Notes
candidateId string No Matches a candidateId in the top-level races array.
name string No The candidate's name.
votes number No Votes for this candidate at this location only.

Behaviour you must handle

“Not published” is a successful response

When there are no published results to serve, the feed returns HTTP 200 with a body containing exactly one field:

{"published": false}

and in XML:

<?xml version="1.0" encoding="UTF-8"?>
<electionResults>
  <published>false</published>
</electionResults>

Nothing else — no election name, no date, no counts, no timestamps. This is the normal response before results are released on election night, and a client must not treat it as an error.

An unknown id, an inactive election, and results withdrawn to correct a figure all return the same response; the cases are not distinguished.

Candidate order

races[].candidates[] is in vote order, highest first, ties broken by ballot position. locations[].races[].candidates[] is in ballot order, so the same list of names reads down every location in the same sequence.

Every candidate carries ballotOrder, its position on the printed ballot. Sort by it for the order used in the City's published statement.

Elected and tie flags

isElected is true when the City has determined the candidate holds a seat. Render the flag; do not derive winners from the vote column.

isTiedAtBoundary is true when a candidate is in an unresolved tie across the last available seat. Under the Local Government Act that seat is decided by lot, so a tied candidate has not won it however the votes read. A tie flag means the seat is not final. Report the seat as undecided, declare neither candidate, and do not resolve the tie yourself.

Reporting fractions are per race

The four reporting figures — stationsReporting, stationCount, machinesReporting and machineCount — appear in totals, election-wide, and again on each race. They legitimately differ, because some locations vote in only some races. Show each race's own fraction beside that race rather than one election-wide fraction over a screen of races.

Versioning

Every published response carries format, always chilliwack-election-results, and version, currently 1.0.

A new field is not a breaking change and will not bump the version, so parse tolerantly and ignore fields you do not recognise. A rename, removal or change of meaning bumps the version, and the City will tell consumers beforehand.

Polling

The feed changes only when the City publishes or republishes results. Between publishes, every request returns the same document.

Conditional requests

A published response carries an ETag. Send it back in an If-None-Match header on the next request; if nothing has been republished you get HTTP 304 Not Modified with an empty body. Between publishes, 304 is the expected outcome.

A published response also carries Last-Modified, set to the publish time, for If-Modified-Since. The ETag is the more precise of the two: it moves on every republish, including two within the same second.

How often to poll

Published responses are sent with Cache-Control: public, max-age=30. Poll every 30 to 60 seconds.

The rate limit

The export endpoints accept 30 requests per minute. There is no key and no registration; the limit applies to everyone. A 30-second poll is 2 requests a minute.

The limit is counted per IP address, not per organisation and not per script — several systems behind one office connection share a single allowance of 30 between them. Where there are many consumers, have one of them poll the feed and hand the document to the others internally.

Requests over the limit are answered with HTTP 429 Too Many Requests, with the error body in the format requested: JSON on a /json URL, XML on an /xml URL. Headers on a 429:

Headers on a 429 response
Header Meaning
Retry-After How many seconds to wait before requesting again — a plain number, not a date. A client must honour it: wait at least that many seconds, then resume its normal interval.
X-RateLimit-Limit The allowance for the current window.
X-RateLimit-Remaining Requests left in the current window. Zero on a 429.
X-RateLimit-Reset When the window resets, as a Unix timestamp in seconds. Sent when it is known.
Cache-Control no-store, so that nothing between us keeps serving the error after the window has cleared.

A 429 is about request volume only. It does not mean the results changed or were withdrawn; do not clear your display on one.

Do not cache a “not published” response

The not-published response is sent with Cache-Control: no-store and carries no ETag and no Last-Modified. Respect that in your own code and in any proxy or CDN in front of the feed: a shared cache holding it would go on serving “not published” after the results went live.

Do not carry an ETag from one election's response into another. Send If-None-Match only with a tag received from the same URL.

Sample responses

Illustrative only. Invented figures, shortened to one race, one candidate and one location. Where a sample and the field tables disagree, the field tables are correct.

JSON

{
  "format": "chilliwack-election-results",
  "version": "1.0",
  "generatedAt": "2026-10-18T03:15:03Z",
  "publishedAt": "2026-10-18T03:14:41Z",
  "election": {
    "electionId": "3f9a2c1e-...",
    "name": "2026 General Local Election",
    "date": "2026-10-17",
    "statusName": "Preliminary Results"
  },
  "totals": {
    "ballotsCastTotal": 18422,
    "stationsReporting": 25,
    "stationCount": 25,
    "machinesReporting": 31,
    "machineCount": 31
  },
  "races": [{
    "raceId": "b71d5e04-...",
    "name": "Mayor",
    "seatsToElect": 1,
    "totalVotes": 17980,
    "stationsReporting": 25,
    "stationCount": 25,
    "machinesReporting": 25,
    "machineCount": 25,
    "candidates": [{
      "candidateId": "c4e8a190-...",
      "name": "Example, Alex",
      "votes": 9711,
      "ballotOrder": 2,
      "rank": 1,
      "isElected": true,
      "isTiedAtBoundary": false
    }]
  }],
  "locations": [{
    "locationId": "9a02f7bb-...",
    "name": "Example Community Centre",
    "shortName": "Example CC",
    "isAdvanceVoting": false,
    "ballotsCast": 1204,
    "races": [{
      "raceId": "b71d5e04-...",
      "name": "Mayor",
      "candidates": [{
        "candidateId": "c4e8a190-...",
        "name": "Example, Alex",
        "votes": 612
      }]
    }]
  }]
}

XML

<?xml version="1.0" encoding="UTF-8"?>
<electionResults format="chilliwack-election-results" version="1.0">
  <generatedAt>2026-10-18T03:15:03Z</generatedAt>
  <publishedAt>2026-10-18T03:14:41Z</publishedAt>
  <election id="3f9a2c1e-...">
    <name>2026 General Local Election</name>
    <date>2026-10-17</date>
    <statusName>Preliminary Results</statusName>
  </election>
  <totals>
    <ballotsCastTotal>18422</ballotsCastTotal>
    <stationsReporting>25</stationsReporting>
    <stationCount>25</stationCount>
    <machinesReporting>31</machinesReporting>
    <machineCount>31</machineCount>
  </totals>
  <races>
    <race id="b71d5e04-...">
      <name>Mayor</name>
      <seatsToElect>1</seatsToElect>
      <totalVotes>17980</totalVotes>
      <stationsReporting>25</stationsReporting>
      <stationCount>25</stationCount>
      <machinesReporting>25</machinesReporting>
      <machineCount>25</machineCount>
      <candidates>
        <candidate id="c4e8a190-...">
          <name>Example, Alex</name>
          <votes>9711</votes>
          <ballotOrder>2</ballotOrder>
          <rank>1</rank>
          <isElected>true</isElected>
          <isTiedAtBoundary>false</isTiedAtBoundary>
        </candidate>
      </candidates>
    </race>
  </races>
  <locations>
    <location id="9a02f7bb-...">
      <name>Example Community Centre</name>
      <shortName>Example CC</shortName>
      <isAdvanceVoting>false</isAdvanceVoting>
      <ballotsCast>1204</ballotsCast>
      <races>
        <race id="b71d5e04-...">
          <name>Mayor</name>
          <candidates>
            <candidate id="c4e8a190-...">
              <name>Example, Alex</name>
              <votes>612</votes>
            </candidate>
          </candidates>
        </race>
      </races>
    </location>
  </locations>
</electionResults>

Questions

For questions about this feed, an election id you cannot find, or advance notice of a version change, email [email protected].