3. APIs
The voting system exposes three HTTP servers and one WebSocket server. Routes are mounted at the root. There is no auth or ACL layer, so treat every endpoint as internal, trusted-network only.
| Server | Default address | Endpoints |
|---|---|---|
| Submission | http://<host>:5000 |
ยง3.2, ยง3.3 |
| Vote intake | http://<host>:8081 |
ยง3.4 (plus NATS, ยง5.3) |
| Query | http://<host>:8082 |
ยง3.5 |
| Live stream | ws://<host>:8765 |
ยง3.6 |
3.1 Response format
There is no shared envelope. Success responses return the payload directly. Failures return:
{ "error": "human readable message" }
Each endpoint below lists its own success shape and status codes.
3.2 How to create a social task
POST /submit-social-choice-task (submission server)
Parses the request into the three entities in ยง2, generates a
social_task_id, and inserts the three documents. No invitations are sent at
this point. They go out when the scheduler starts the task
(ยง1.2).
Request body. Three top-level objects. Every key listed is read with
payload[...], so it must be present unless marked optional.
| Path | Maps to | Notes |
|---|---|---|
social_task.created_by_subject_id |
created_by_subject_id |
|
social_task.created_by_subject_data |
created_by_subject_data |
object |
social_task.org_ids |
org_ids |
array of subject IDs |
social_task.access_type |
social_task_access_type |
"private" or "public" |
social_task.goal |
goal_data |
object |
social_task.topics |
social_tasks_topics |
array |
social_task.properties |
social_task_properties |
object |
social_task.creation_time |
creation_time |
ISO-8601, no Z |
social_task.scheduled_time |
scheduled_time |
optional. Naive ISO-8601 in UTC. Needed to start. |
social_task.duration |
duration |
optional. Public vote threshold. |
social_task.status |
status |
optional. Send "scheduled" for the task to start. |
social_task.report |
report_json |
optional, default {} |
social_task.enable_live_streaming |
enable_live_streaming |
optional, default false |
subject_spec.title |
topic_title |
|
subject_spec.description |
topic_description |
|
subject_spec.voting_options |
voting_options_map |
object, option ID โ label |
subject_spec.voting_metadata |
voting_option_metadata_map |
object |
subject_spec.supported_protocols |
supported_protocols |
array |
subject_spec.voting_request |
voting_message_request_creator |
function spec, or {} |
evaluation_spec.constraints_primary |
constraints_entry_id |
string, may be "" |
evaluation_spec.constraints_secondary |
constraints_entry_id_1 |
string, may be "" |
evaluation_spec.voting_pqt |
voting_pqt |
function spec |
evaluation_spec.evaluation |
choice_evaluation |
function spec |
evaluation_spec.tie_breaker |
tie_breaker |
function spec, or {} |
evaluation_spec.post_award |
post_awarding |
function spec |
Function specs are described in ยง2.5.
Private-task example. Three named reviewers choose between three translators. The task starts at 10:00 UTC.
curl -X POST http://localhost:5000/submit-social-choice-task \
-H "Content-Type: application/json" \
-d '{
"social_task": {
"created_by_subject_id": "planner-service",
"created_by_subject_data": {"team": "localisation"},
"org_ids": ["agent-reviewer-01", "agent-reviewer-02", "agent-reviewer-03"],
"access_type": "private",
"goal": {"objective": "Choose the lead translator for the Q4 handbook"},
"topics": ["translation", "fr"],
"properties": {"urgency": "high"},
"creation_time": "2026-10-22T09:00:00",
"scheduled_time": "2026-10-22T10:00:00",
"status": "scheduled",
"enable_live_streaming": true
},
"subject_spec": {
"title": "Lead translator, Q4 handbook",
"description": "Pick the agent that will lead the ENโFR translation of the Q4 handbook.",
"voting_options": {
"agent-translator-01": "Translator 01",
"agent-translator-02": "Translator 02",
"agent-translator-03": "Translator 03"
},
"voting_metadata": {},
"supported_protocols": ["http"],
"voting_request": {"mode": "function", "function_id": "voting-invite-builder:1.0-stable"}
},
"evaluation_spec": {
"constraints_primary": "",
"constraints_secondary": "",
"voting_pqt": {"mode": "dsl", "workflow_id": "vote-option-is-valid:1.0"},
"evaluation": {"mode": "dsl", "workflow_id": "plurality-count:1.0"},
"tie_breaker": {"mode": "dsl", "workflow_id": "earliest-vote-wins:1.0"},
"post_award": {"mode": "dsl", "workflow_id": "assign-lead-translator:1.0"}
}
}'
Public-task example. Changes from the private example: access_type is
"public", org_ids is empty, and evaluation fires at 10 qualified votes.
{
"social_task": {
"access_type": "public",
"org_ids": [],
"duration": 10,
"...": "same as above"
},
"subject_spec": { "...": "same as above" },
"evaluation_spec": { "...": "same as above" }
}
Response 201 Created:
{ "social_task_id": "task_3f9c6d2e" }
Error responses:
| Status | When |
|---|---|
400 |
Empty or non-JSON body; one of social_task, subject_spec, evaluation_spec is missing; creation_time or scheduled_time is not a valid ISO-8601 string (a trailing Z is rejected) |
500 |
A nested key is missing (KeyError), or the database insert fails. The body is always {"error": "Internal server error"}. Check the service log for the missing key. |
Creation does not validate
access_type, function specs, or whetherstatusandscheduled_timeare set. A task can be created successfully and still never start. See ยง1.4.
3.3 How to validate a task without creating it
POST /parse-social-choice-task (submission server)
Runs the same parser as ยง3.2 and returns the result without writing anything. Use it to check a payload and see the stored field names.
curl -X POST http://localhost:5000/parse-social-choice-task \
-H "Content-Type: application/json" \
-d @task.json
Response 200:
{
"parsed": {
"social_task": { "...": "SocialTaskCoreData.to_dict()" },
"subject_spec": { "...": "SocialChoiceSubjectSpecInput.to_dict()" },
"evaluation_spec": { "...": "SocialChoiceEvaluationInput.to_dict()" }
}
}
The social_task_id in the response is freshly generated and is not
reserved. ยง3.2 generates a different ID. Errors are the same as ยง3.2.
3.4 How to submit a vote
POST /submit-vote (vote-intake server, port 8081)
curl -X POST http://localhost:8081/submit-vote \
-H "Content-Type: application/json" \
-d '{
"vote_id": "7d0f2a9c-2b4e-4a51-9a3e-0c6f1b2d4e8a",
"social_task_id": "task_3f9c6d2e",
"submitter_subject_id": "agent-reviewer-01",
"vote_data": {"selected_option": "agent-translator-02"},
"submission_time": "2026-10-22T10:03:12",
"qualified": false
}'
The body must contain exactly the six Votes fields, no more and no
fewer, because it is parsed with Votes(**data). The server overwrites
submission_time and qualified, so any placeholder values work. The client
chooses vote_id.
Processing order inside VoteAcceptor.accept_vote:
- Reject if
submitter_subject_idalready has a vote on this task, qualified or not. - Reject if the task does not exist.
- Reject if the task status is not
"started". - Private only: reject if
submitter_subject_idis not inorg_ids. - Load the subject spec and evaluation spec. Reject if either is missing, or if no PQT is configured.
- Run the PQT with the task, both specs, and
vote_data(ยง4.3).qualifiedisbool(result["qualified"]). - Check whether evaluation should start, and if so, launch the Kubernetes Job (ยง1.2 step 7).
- If step 6 or step 7 raised, set
qualified = false. - Persist the vote with the server's
submission_timeand the computedqualified.
Response 200:
{ "status": "accepted", "qualified": true }
"status": "accepted" means the vote was stored. A disqualified vote also
returns 200, with "qualified": false, and still counts as this subject's
one vote.
Error responses:
| Status | When |
|---|---|
400 |
Any failure: a missing or extra field, a rejection in steps 1 to 5, or a database error. The message is in error, for example "Vote rejected: Vote already submitted for this task." |
Rejection messages from steps 1 to 5:
| Message | Step |
|---|---|
Vote rejected: Vote already submitted for this task. |
1 |
Vote rejected: Invalid social_task_id. |
2 |
Vote rejected: Voting task is not active yet. |
3 (also returned after the task is complete) |
Vote rejected: You are not authorized to vote in this private task. |
4 |
Voting task configuration is incomplete. |
5 |
Voting PQT DSL is not defined. |
5 |
The winner is not returned from this call. Poll
GET /get-status/<id>until it returns"complete", then readGET /get-report/<id>(ยง3.5). You can also subscribe to the winner's NATS subject (ยง5.4).
3.5 Query endpoints
All on the query server (port 8082).
| Method | Path | Purpose |
|---|---|---|
GET |
/get-status/<social_task_id> |
Current task status |
GET |
/get-report/<social_task_id> |
The final report_json |
GET |
/get-full-task-bundle/<social_task_id> |
Task, specs, and all votes |
GET |
/is-live-streaming-enabled/<social_task_id> |
Whether ยง3.6 will serve this task |
POST |
/generic-query/<collection_name> |
Raw MongoDB filter on one collection |
Every endpoint returns 400 {"error": "..."} on any failure, including an
unknown social_task_id (for example "Error fetching status: Task not
found."). None of them return 404.
GET /get-status/<social_task_id>
curl http://localhost:8082/get-status/task_3f9c6d2e
โ 200 { "status": "started" }. The value is null if the task was created
without a status.
GET /get-report/<social_task_id>
curl http://localhost:8082/get-report/task_3f9c6d2e
โ 200 with the report_json object itself, unwrapped
(ยง2.6). Before the task completes, this is
{}.
GET /get-full-task-bundle/<social_task_id>
curl http://localhost:8082/get-full-task-bundle/task_3f9c6d2e
โ 200:
{
"task": { "...": "SocialTaskCoreData.to_dict()" },
"subject_spec": { "...": "SocialChoiceSubjectSpecInput.to_dict()" },
"evaluation_input": { "...": "SocialChoiceEvaluationInput.to_dict()" },
"votes": [ { "...": "Votes.to_dict()" } ]
}
votes includes disqualified votes. Filter on qualified yourself. Note the
key is evaluation_input, not evaluation_spec as in the report.
GET /is-live-streaming-enabled/<social_task_id>
โ 200 { "enabled": true }
POST /generic-query/<collection_name>
The body is a raw MongoDB filter document passed to collection.find().
collection_name must be one of social_task_core_data,
social_choice_subject_spec, social_choice_evaluation_input, or votes.
Any other name returns 400 {"error": "Error during query: Unsupported
collection: ..."}.
# every task that is currently accepting votes
curl -X POST http://localhost:8082/generic-query/social_task_core_data \
-H "Content-Type: application/json" \
-d '{"status": "started"}'
# every vote a subject has cast
curl -X POST http://localhost:8082/generic-query/votes \
-H "Content-Type: application/json" \
-d '{"submitter_subject_id": "agent-reviewer-01"}'
This endpoint returns documents with MongoDB's
_id, which is not JSON-serializable. Today, any query that matches at least one document returns400, and only an empty result succeeds (200 []). See ยง1.5 gap 7. Until that is fixed, use the per-task endpoints above.
3.6 WebSocket live stream
ws://<host>:8765 (query system). The path is not checked.
The server pushes the full task bundle on a fixed interval, so dashboards can
watch votes arrive. It serves only tasks created with
enable_live_streaming: true.
1. Connect and send one JSON message:
{ "social_task_id": "task_3f9c6d2e", "refresh_interval": 5 }
| Field | Type | Default | Notes |
|---|---|---|---|
social_task_id |
string | required | |
refresh_interval |
int | 5 |
Seconds between pushes |
2. The server checks the task, then streams:
{
"type": "update",
"data": {
"task": { "...": "..." },
"subject_spec": { "...": "..." },
"evaluation_input": { "...": "..." },
"votes": [ "..." ]
}
}
data is the same object as GET /get-full-task-bundle (ยง3.5). One message
is sent every refresh_interval seconds until the client disconnects. The
stream does not stop when the task completes. Watch data.task.status and
disconnect yourself.
Errors. The server sends one message and closes:
| Message | When |
|---|---|
{"error": "Missing social_task_id"} |
The field is absent |
{"error": "Invalid request format. Must be JSON with 'social_task_id' and 'refresh_interval'"} |
The first message is not JSON, or refresh_interval is not an integer |
{"error": "Live streaming not enabled for this task"} |
enable_live_streaming is false |
An unknown social_task_id closes the connection without an error message.
If a single push fails, for example on a database error, the failure is
logged and the next push is attempted on schedule.
Example client:
import asyncio, json
import websockets
async def watch(task_id: str):
async with websockets.connect("ws://localhost:8765") as ws:
await ws.send(json.dumps({"social_task_id": task_id, "refresh_interval": 5}))
async for raw in ws:
msg = json.loads(raw)
if "error" in msg:
raise RuntimeError(msg["error"])
task = msg["data"]["task"]
votes = msg["data"]["votes"]
print(task.get("status"), len(votes), "votes")
if task.get("status") == "complete":
break
asyncio.run(watch("task_3f9c6d2e"))
โ Previous: Schema ยท Back to index ยท Next: Functions to write โ