CivorenRequest access

Query layer

Cypher

Data LiveAPI Planned

Ask the graph questions we never anticipated, writing the traversal yourself while we enforce the guardrails, which is something no other election data provider offers.

When to reach for it

REST answers the questions we chose in advance, whereas Cypher answers yours. It earns its keep on multi-hop questions, the sort that would take three joins and a subquery in SQL and cannot be expressed in a flat export at all.

  • Every Democratic candidate for county office in Arizona who has filed but not yet qualified for the ballot.
  • Contests where an incumbent is unopposed, grouped by jurisdiction tier.
  • People who have run for more than one office across cycles, and which offices.
  • Which authority administers elections for each municipality inside a given county.

Your first query

Post a statement together with any parameters it needs, and the response comes back as row-oriented JSON.

POST /v1/cypher
MATCH (c:Candidacy)-[:RUNS_IN]->(:Contest)-[:FILLS]->(o:Office)
WHERE o.level = 'congressional-district'
MATCH (c)-[:IS_CANDIDACY_OF]->(p:Person)
MATCH (o)-[:SCOPED_TO]->(r:Region)
WHERE r.state_fips = $state_fips
OPTIONAL MATCH (c)-[:NOMINATED_BY]->(party:Party)
RETURN p.name AS person, party.name AS party,
       r.name AS district, c.filing_status AS filing_status,
       c.confidence AS confidence
ORDER BY district LIMIT 25
200 OK, real rows from the production graph
{
  "columns": ["person", "party", "district", "filing_status", "confidence"],
  "rows": [
    ["Amish Shah", "Democratic Party", "Arizona's 1st Congressional District", null, 0.85],
    ["Monica Alponte", "Libertarian Party", "Arizona's 1st Congressional District", null, 0.85],
    ["Oren Davis", "Independent", "Arizona's 1st Congressional District", "pending", 0.85],
    ["Andres Barraza", "Democratic Party", "Arizona's 1st Congressional District", "pending", 0.85]
  ],
  "stats": { "rows": 25, "elapsed_ms": 184, "db_hits": 9412 }
}

Guardrails

The endpoint is read-only by construction rather than by convention. Statements are checked and rejected before any database session opens, and the session itself is opened in read-only mode against a replica, so anything that somehow cleared the first check is still refused at the database.

RuleBehaviour
Write clausesCREATE, MERGE, SET, DELETE, REMOVE, DROP, DETACH, FOREACH and LOAD CSV are rejected before execution
IntrospectionCALL db.*, CALL dbms.*, CALL apoc.* and keys() are rejected, since the documented schema is the supported way to learn a node's shape
Entitlement checkA statement naming a field your key cannot read is rejected rather than silently emptied
Wall clockQueries exceeding your plan's budget are cancelled and return a timeout
Row capResults are capped at your plan's limit whatever LIMIT you wrote. Use bulk export for volume
ParametersPass values via parameters, which lets the database reuse its query plan and is therefore both safer and faster than string concatenation
CardinalityStatements whose estimated cost exceeds your plan's ceiling are refused before any rows are produced
403 Forbidden
{
  "error": "entitlement_required",
  "message": "Statement references 'campaign_emails', which requires the 'contact' entitlement.",
  "field": "campaign_emails",
  "entitlement": "contact"
}

Why Cypher rather than only GraphQL. GraphQL selects fields from a shape we defined, whereas Cypher lets you define the shape yourself. For a dataset whose value lies in its relationships that difference is the whole product, and because it is the language the graph is natively stored in there is no translation layer to lose fidelity in.