Skip to content

Priorities and trade-offs

A participant says how much an issue matters to them and which trade-offs they would accept. Each is an opinion, sealed like any personal value, and shown to anyone else only inside a count with its label. One expression per participant per issue holds all of it.

Express

curl -s localhost:9000/me/issues/$ISSUE/expression/priority -H "Authorization: Bearer $ME" \
  -H 'Content-Type: application/json' -d '{"level":4}'
curl -s localhost:9000/me/issues/$ISSUE/expression/trade-offs -H "Authorization: Bearer $ME" \
  -H 'Content-Type: application/json' -d '{"option":"pol-01M57P…","accepts":"a longer walk","rejects":"a route with no pavement"}'
curl -s localhost:9000/me/issues/$ISSUE/expression/considerations -H "Authorization: Bearer $ME" \
  -H 'Content-Type: application/json' -d '{"option":"pol-01M57P…","text":"The path floods in winter."}'
{ "issue": "iss-01M57P…", "label": "opinion", "priority": 4, "tradeOffs": [ { "option": "pol-01M57P…", "accepts": "a longer walk", "rejects": "a route with no pavement" } ],
  "considerations": [ … ], "withdrawn": false,
  "notice": "This is your opinion. It is shown only inside counts of everyone who answered, never beside the evidence." }

A priority is a level from 1 to 5; outside it is refused with 422 and expression.priority.range. A trade-off or a consideration names one of the issue's options; another is refused with 404 and expression.option.not-held. Expressing a priority again replaces the earlier one. GET /me/issues/{issue}/expression answers the participant's own and no one else's.

POST /me/issues/{issue}/expression/withdraw withdraws the whole expression, so it is no longer counted. An amendment to a withdrawn expression is refused with 409 and expression.withdrawn; expressing again afterwards is a new expression.

The aggregate

The public issue page carries the aggregate, read from a view over expressions:

{
  "participants": { "count": 7, "suppressed": false },
  "method": "Self-selected: participants on Voice who chose to say how much this issue matters to them or which trade-offs they would accept.",
  "window": { "from": "2026-10-18T14:28:47Z", "to": "2026-10-19T07:12:03Z" },
  "deduplication": "One expression per participant per issue. An amended priority replaces the earlier one; a withdrawn expression is not counted.",
  "caveat": "These are statements by the participants who chose to answer. They are opinions, not evidence, and say nothing about the proportion of all constituents who hold them.",
  "threshold": 5,
  "priorities": [ { "key": "4", "count": 5, "suppressed": false }, { "key": "5", "suppressed": true }, … ],
  "options": [ { "optionId": "pol-01M57P…", "cells": [ { "key": "accepts", "count": 6, "suppressed": false }, { "key": "rejects", "suppressed": true } ] } ]
}

A cell with fewer participants than VOICE_SUPPRESSION_THRESHOLD, five by default, is omitted and marked suppressed; this is an answer, not an error. The view keys each row by a salted hash of the participant id, for deduplication, and reads the priority level as a plain integer carried beside the sealed value: the level alone is not personal, and the aggregate suppresses small cells. On erasure the row goes with the deleted entity.

The representative's dashboard

GET /staff/issues/{issue} answers the public page plus the contributions the representative's role may see, the participant-derived measures with their caveats, the unresolved questions and the contrary perspectives. Every measure is labelled participant-derived, and the page makes no claim about the population. Investigations and actions is what the representative does next.