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.
| 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:
| 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 astrueorfalse— so cast them yourself. - Ids are attributes.
electionId,raceId,candidateIdandlocationIdbecome anidattribute on the corresponding element.formatandversionare attributes on the<electionResults>root. - Null. A JSON
nullis 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.
| 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.
| 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[].
| 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.
| 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.
| 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.
| 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.
| 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.
| 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:
| 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].
