Zum Inhalt springen

AppView-API

Stand: 2026-11-07. Referenz der Lese-API für Clients, von Hand gepflegt. Quelle der Wahrheit ist der Code in services/appview/src/routes/. Konzept und Einordnung: Architektur §5.2. Die Admin-API für Clients (Registrar) steht auf der Startseite.

Die AppView beantwortet Abfragen aus dem Index. Sie schreibt keine Records: Beiträge schreibt der Client ins Repo der Person, der Store übernimmt sie über den Firehose (ATProto in atvoto).

Basis-URL (test): https://api.test.atvoto.ch. Alle Methoden liegen unter /xrpc/<NSID>.

Aufruf: GET mit Query-Parametern, POST mit JSON-Body. Listen-Parameter werden wiederholt (?dossiers=sv-1&dossiers=sv-2). CORS ist für alle Origins offen, ohne Cookies.

Dossier: Parameter dossier ist die Dossier-ID inklusive Client-Präfix, z.B. sv-663.

Fehler: JSON mit Code und Text, z.B. {"error": "not_found", "message": "Argument not found"}. Bei Auth-Fehlern und einigen Eingabefehlern liefert FastAPI stattdessen {"detail": "…"}.

Status Bedeutung
400 Parameter fehlt oder ungültig (invalid_request)
401 Service-Auth-Token fehlt (bei Pflicht) oder ist ungültig
403 / 409 Peer Review: nicht eingeladen, nicht eingecheckt, geschlossen, zu spät
404 Objekt unbekannt (not_found)
429 Rate-Limit überschritten
503 DID der Person nicht auflösbar

Argumente, Kommentare, Themen und Suche liefern Texte in einer Sprache: Parameter lang, sonst der erste passende Eintrag von Accept-Language, sonst de-CH. Unterstützt: de-CH, fr-CH, it-CH, rm, en-GB (ATVOTO_LANGUAGES); de oder de-DE werden auf de-CH abgebildet.

Ist der Text nicht in der Originalsprache vorhanden, kommt die Übersetzung. Die Antwort sagt, was geliefert wurde:

Feld Inhalt
record.langs Originalsprache(n)
availableLangs alle verfügbaren Sprachen (Original + Übersetzungen)
translationSource ai oder manual, wenn übersetzt
record.translatedFrom Originalsprache, wenn übersetzt

Es gibt drei Stufen:

Stufe Verhalten
öffentlich ohne Token
Viewer optional ohne Token anonym; mit Token zusätzlich Felder zur Person (viewer, eigene Reihenfolge). Ein ungültiges Token ist immer 401.
Viewer nötig 401 ohne gültiges Token

Das Token ist ein Service-Auth-JWT, ausgestellt vom PDS der Person (iss = DID der Person, aud = DID der AppView, lxm = aufgerufene Methode). Zwei Wege:

  1. Über den PDS (üblich): Der Client ruft die Methode beim PDS auf, mit dem Access-Token der Person und dem Header atproto-proxy: did:web:api.test.atvoto.ch#atvoto_appview. Der PDS stellt das JWT aus und leitet weiter.
  2. Direkt: Token mit com.atproto.server.getServiceAuth (aud = AppView-DID, lxm = Methode) holen und als Authorization: Bearer <jwt> direkt an die AppView schicken.

PDS-Access-Tokens selbst werden abgelehnt. Die AppView-DID und ihr Service-Eintrag stehen in /.well-known/did.json.

Gezählt pro Person (DID aus dem Token), ohne Token pro IP.

Methode Limit
argument.findSimilar 20/Minute und 200/Stunde
peerreview.duplicateCandidate 20/Minute
com.atproto.label.queryLabels 60/Minute

argument.list (Standard sort=random), comment.list und taxonomy.get mischen die Einträge stabil pro Person: Jede Person sieht ihre eigene, feste Reihenfolge, die sich nicht ändert, wenn neue Beiträge dazukommen. Anonym gilt eine gemeinsame Reihenfolge.

Von der Moderation ausgeblendete Beiträge (hidden) und Beiträge zurückgezogener Dossiers (withdrawn) erscheinen in keiner Antwort. Ein ausgeblendeter Kommentar mit sichtbaren Antworten bleibt als Platzhalter {uri, hidden: true, parentUri, argumentUri} stehen, damit der Gesprächsverlauf erhalten bleibt.


Alle Methoden mit Präfix ch.atvoto..

Methode HTTP Auth Zweck
argument.list GET optional Argumente eines Dossiers
argument.get GET optional ein Argument, mit Themenpfaden
argument.findSimilar POST nötig ähnliche Argumente zu einem Entwurf
comment.list GET optional Kommentare zu einem Argument
comment.get GET optional ein Kommentar mit seinem Argument
activity.list GET optional Aktivitäten eines Dossiers
activity.markSeen POST nötig Aktivitäten als gesehen markieren
dossier.getStats GET öffentlich Zähler pro Dossier
dossier.search GET optional Suche in einem Dossier
dossier.getPermissions GET nötig was die Person im Dossier darf
quota.get GET nötig Kontingent der Person
taxonomy.get GET optional Themenbaum mit Argumenten
peerreview.criteria GET optional Kriterien des Gutachtens
peerreview.list GET optional* Gutachten eines Dossiers
peerreview.pending GET nötig offene Einladungen der Person
peerreview.status GET nötig Stand eines Gutachtens
peerreview.duplicateCandidate GET nötig ähnlichstes Argument für Gutachter:innen
peerreview.checkIn POST nötig Platz im Gutachten belegen
peerreview.activity POST nötig Nachfrist bei Aktivität verlängern

* scope=mine braucht ein Token, scope=all nicht.

Dazu: Bluesky-kompatible Endpunkte, Labels, DID-Dokument und Health.


GET /xrpc/ch.atvoto.argument.list · Viewer optional

Parameter Pflicht Beschreibung
dossier ja Dossier-ID
sort random (Standard, stabil pro Person), top (Likes), new, discussed (Kommentare)
type PRO oder CONTRA
source user, official, organization; ohne = alle
limit Standard 100
lang Sprache, siehe Sprache

Antwort {"arguments": [Argument, …]}. Auch abgelehnte Argumente sind enthalten (peerreviewStatus: "rejected"), der Client kennzeichnet sie.

{
"uri": "at://did:plc:dossier…/ch.atvoto.dossier.argument/3f9c…",
"cid": "bafyrei…",
"record": {
"$type": "ch.atvoto.dossier.argument",
"title": "…",
"body": "…",
"type": "PRO",
"dossier": "at://…",
"createdAt": "2026-10-30T08:12:00Z",
"langs": ["de-CH"],
"source": {
"$type": "ch.atvoto.dossier.argument#sourceUser",
"authorDid": "did:plc:…",
"originUri": "at://did:plc:…/ch.atvoto.dossier.argument/3l…",
"originCid": "bafyrei…"
}
},
"author": {"did": "did:plc:…", "displayName": "…"},
"likeCount": 4,
"commentCount": 2,
"peerreviewStatus": "approved",
"indexedAt": "2026-10-30T08:12:03Z",
"viewer": {"like": "at://…", "preference": 80},
"availableLangs": ["de-CH", "fr-CH"],
"translationSource": "ai"
}
Feld Hinweis
record.source #sourceUser (mit authorDid, originUri, originCid), #sourceOfficial (documentRef, section) oder #sourceOrganization (orgKey, documentRef, verifiedDid)
author nur bei sourceUser; displayName = Pseudonym aus dem Profil
peerreviewStatus preliminary, approved, rejected; fehlt, wenn das Argument kein Peer Review hat
viewer nur mit Token: like = AT-URI der eigenen Bewertung, preference = deren Wert 0–100

Felder ohne Wert fehlen in der Antwort.

GET /xrpc/ch.atvoto.argument.get · Viewer optional

Parameter Pflicht Beschreibung
dossier ja Dossier-ID
rkey ja rkey der kanonischen Kopie
lang Sprache

Antwort {"argument": Argument} wie oben, zusätzlich topicPaths: die Themen, denen das Argument zugeordnet ist, je als Pfad von oben nach unten ([[{"name", "key", "description"}, …], …]). 404, wenn unbekannt.

POST /xrpc/ch.atvoto.argument.findSimilar · Viewer nötig · 20/min, 200/h

Findet bestehende Argumente, die einem Entwurf ähneln (Embeddings). Neutral: keine Einschätzung zu Position, Thema oder Ton. Der Entwurfstext geht zur Berechnung an den Embedding-Anbieter des Stores. Technik: Duplikat-Check.

{"dossier": "sv-663", "title": "…", "body": "…", "type": "PRO", "lang": "de-CH", "limit": 3}

title oder body genügt; type beschränkt auf die gleiche Position; limit 1–10 (Standard 1).

{"lang": "de-CH", "status": "ok", "items": [{"uri": "at://…", "title": "…", "body": "…", "type": "PRO", "similarity": 0.91}]}

status: "unavailable", wenn der Embedding-Dienst nicht erreichbar ist. Das soll das Einreichen nie blockieren.


GET /xrpc/ch.atvoto.comment.list · Viewer optional

Parameter Pflicht Beschreibung
argumentUri ja AT-URI des Arguments
limit Standard 50
lang Sprache

Antwort {"comments": [Comment, …]}, alle Ebenen in einer flachen Liste; den Baum baut der Client über parentUri.

{
"uri": "at://did:plc:…/ch.atvoto.dossier.comment/3l…",
"cid": "bafyrei…",
"record": {"title": "…", "body": "…", "argument": "at://…", "createdAt": "…", "langs": ["de-CH"]},
"author": {"did": "did:plc:…", "displayName": "…"},
"origin": "intern",
"parentUri": "at://…",
"argumentUri": "at://…",
"likeCount": 1,
"indexedAt": "…",
"viewer": {"like": "at://…"},
"availableLangs": ["de-CH", "fr-CH"],
"translationSource": "ai",
"collapsed": true
}
Feld Hinweis
origin intern (atvoto) oder extern (Antwort auf Bluesky; author dann mit handle)
parentUri fehlt bei Kommentaren direkt zum Argument
collapsed nur bei externen Kommentaren von Konten ohne Teilnahmerecht: eingeklappt anzeigen. Das ist keine Moderation.

Übersetzungen von Kommentaren liegen als eigene Records im Community-Konto (ch.atvoto.dossier.commentTranslation), weil der Store nicht in fremde Repos schreibt (Übersetzungen).

GET /xrpc/ch.atvoto.comment.get · Viewer optional

Parameter Pflicht Beschreibung
uri ja AT-URI des Kommentars
lang Sprache

Antwort {"comment": Comment, "argument": {uri, rkey, title, body, type, likeCount, commentCount, peerreviewStatus, dossierRkey}}. 404, wenn unbekannt.


GET /xrpc/ch.atvoto.activity.list · Viewer optional

Parameter Pflicht Beschreibung
dossier ja Dossier-ID
filter all (Standard), comments (Kommentare und Antworten), arguments (neue Argumente und Meilensteine)
limit Standard 30
cursor aus der vorherigen Antwort

Antwort {"activities": [Activity, …], "cursor": "…" | null}, neueste zuerst.

Feld Inhalt
type new_argument, milestone (Peer Review abgeschlossen: angenommen oder abgelehnt), comment, reply
activityUri, activityAt Kennung und Zeitpunkt; activityUri ist der Schlüssel für markSeen
actor {did, displayName}
argument {uri, rkey, title, body, type, likeCount, commentCount, peerreviewStatus}
comment bei comment/reply: {uri, text, likeCount, replyCount}
parent bei reply: der beantwortete Kommentar {uri, did, displayName, text, hasParent, likeCount, replyCount}
viewer nur mit Token: argumentLike (AT-URI), seen: true

POST /xrpc/ch.atvoto.activity.markSeen · Viewer nötig

Body {"uris": ["<activityUri>", …]} → {"success": true}.


GET /xrpc/ch.atvoto.dossier.getStats · öffentlich

dossiers (wiederholt, höchstens 100) → {"stats": {"sv-663": {"arguments": 12, "comments": 40}}}. Unbekannte Dossiers kommen mit Nullen. Gezählt werden nur interne Kommentare.

Titel, Beschreibung und Abstimmungsdatum eines Dossiers hält der Client, nicht der Store.

GET /xrpc/ch.atvoto.dossier.search · Viewer optional

Parameter Pflicht Beschreibung
dossier ja Dossier-ID
q ja Suchtext
type nur eine Gruppe: taxonomy, argument, comment
limit pro Gruppe, 1–25, Standard 8
lang Sprache

Sucht nur in der angefragten Sprache (Original oder vorhandene Übersetzung).

{
"query": "…", "lang": "de-CH",
"results": {
"taxonomy": [{"type": "taxonomy", "dossierRkey": "sv-663", "topic": "<key>", "title": "…", "snippet": "…", "matchField": "name"}],
"argument": [{"type": "argument", "rkey": "…", "uri": "…", "argType": "PRO", "title": "…", "snippet": "…", "matchField": "body"}],
"comment": [{"type": "comment", "uri": "…", "argumentUri": "…", "title": "…", "snippet": "…", "matchField": "text"}]
},
"counts": {"taxonomy": 1, "argument": 1, "comment": 1}
}

GET /xrpc/ch.atvoto.dossier.getPermissions · Viewer nötig

dossier oder subject (AT-URI eines Arguments oder Kommentars; das Dossier wird daraus bestimmt).

{
"dossier": "sv-663",
"status": "open",
"permissions": {
"comment": {"allowed": true},
"rate": {"allowed": true},
"argument.submit": {"allowed": false, "reason": "assurance_too_low"},
"peerreview": {"allowed": true}
}
}

Es ist dieselbe Prüfung wie in der Annahmeschleuse (registry.participation_check), damit der Client nur anbietet, was erlaubt ist. Verbindlich bleibt die Schleuse. Ausserhalb der Phase open ist nichts erlaubt, ausser Bewerten in closed; reason ist dann dossier_not_open, dossier_paused, dossier_closed oder dossier_withdrawn. Weitere Gründe kommen aus den Teilnahmeregeln (z.B. not_member, assurance_too_low), siehe Konten und Teilnahme.

GET /xrpc/ch.atvoto.quota.get · Viewer nötig

dossier → Verbrauch und Limits der Person für neue Argumente und Kommentare. Gelöschte Beiträge geben kein Kontingent zurück.

{
"arguments": {"dailyUsed": 1, "dailyLimit": 3, "dossierUsed": 2, "dossierLimit": 10},
"comments": {"dailyUsed": 0, "dailyLimit": 20, "dossierUsed": 5, "dossierLimit": 100}
}

daily = letzte 24 Stunden, dossier = insgesamt in diesem Dossier. Die Werte im Beispiel sind erfunden; die Limits sind Konfiguration.


GET /xrpc/ch.atvoto.taxonomy.get · Viewer optional

Parameter Pflicht Beschreibung
dossier ja Dossier-ID
topic key eines Themas: zeigt dessen Unterthemen statt der Hauptthemen
shape full: ganzer verschachtelter Baum ohne Argumentlisten (für Sunburst-Grafiken)
lang Sprache

Ohne shape kommt der Basisknoten mit genau einer Ebene Kindern; jedes Kind sammelt die Argumente seines ganzen Teilbaums.

{
"dossierRkey": "sv-663",
"breadcrumb": [{"name": "…", "key": "…", "description": "…"}],
"tree": {
"id": 1, "key": "…", "name": "…", "introduction": "…", "description": "…",
"depth": 0, "importance": 0.5, "argumentCount": 34, "availableLangs": ["de-CH"],
"arguments": [],
"children": [
{"key": "…", "name": "…", "argumentCount": 8,
"arguments": [{"uri": "…", "cid": "…", "rkey": "…", "title": "…", "type": "PRO", "sourceType": "user",
"likeCount": 3, "viewerPreference": 70, "availableLangs": ["de-CH"]}],
"children": []}
]
}
}

introduction ist für Abstimmende gedacht, description intern (Klassifikation). Mit Token trägt jeder Knoten zusätzlich proLeaning (−1 bis 1): die Neigung der Person für oder gegen die Vorlage, gewichtet nach ihren Bewertungen. 404, wenn das Dossier keinen Themenbaum hat. Herkunft der Daten: Snapshot ch.atvoto.taxonomy.snapshot, den der Client über den Registrar veröffentlicht.


Ablauf, Zustände und Parameter: Peer Review. Ein Gutachten hat die Zustände open → provisional_closed (Quorum erreicht, Nachfrist läuft) → closed. Das Gutachten selbst ist keine AppView-Methode: Der Client schreibt ch.atvoto.peerreview.response ins Repo der Person.

Typische Reihenfolge im Client: pending → status → checkIn → während des Schreibens activity → Record peerreview.response schreiben.

GET /xrpc/ch.atvoto.peerreview.criteria · Viewer optional

{"criteria": [{"key": "coherence", "label": "Stimmigkeit"}, {"key": "tone", …}, {"key": "topic", …}, {"key": "unity", …}, {"key": "non_duplication", "label": "Kein Duplikat"}]}. Siehe die fünf Kriterien.

GET /xrpc/ch.atvoto.peerreview.list

Parameter Pflicht Beschreibung
dossier ja Dossier-ID
scope mine (Standard, Token nötig): nur Gutachten, bei denen die Person eingeladen ist, geantwortet hat oder Autor:in ist. all: alle (Transparenz)

Antwort {"reviews": [...]}, laufende zuerst, dann geschlossene, je neueste zuerst. Je Eintrag: argumentUri, title, body, type, authorDid, state, peerreviewStatus, quorum, approvals, rejections, totalReviews, invitationCount, openedAt, provisionalClosedAt, graceUntil, closedAt sowie viewerInvited, viewerCheckedInAt, viewerResponded, viewerIsAuthor.

GET /xrpc/ch.atvoto.peerreview.pending · Viewer nötig

Einladungen, die die Person noch annehmen kann: aktiv eingeladen, Gutachten open, noch nicht geantwortet.

{"invitations": [{"invitationUri": "at://…", "argumentUri": "at://…", "invitedAt": "…",
"argument": {"title": "…", "body": "…", "type": "CONTRA", "dossierUri": "at://…", "dossierRkey": "sv-663", "authorDid": "did:plc:…"}}]}

GET /xrpc/ch.atvoto.peerreview.status · Viewer nötig

argumentUri → Stand eines Gutachtens: state, quorum, peerreviewStatus, approvals, rejections, totalReviews, invitationCount, provisionalClosedAt, graceUntil, closedAt, criteriaBreakdown ([{key, label, ok, flagged}], Zahl der Ok- und Beanstandungs-Stimmen je Kriterium) sowie für die Person checkedInAt und lastActivityAt. Ist die Person Autor:in des Arguments, kommen die einzelnen Gutachten dazu: reviews: [{reviewerDid, criteria, vote, createdAt}].

GET /xrpc/ch.atvoto.peerreview.duplicateCandidate · Viewer nötig · 20/min

argumentUri → das ähnlichste andere Argument gleicher Position im selben Dossier, falls es über der Anzeige-Schwelle liegt: {"status": "ok", "items": [...]} (0 oder 1 Eintrag). Nur wenn ein Kandidat kommt, zeigt der Client das Kriterium „Kein Duplikat“. {"status": "unavailable"} bei technischen Problemen; das Gutachten wird nie blockiert.

POST /xrpc/ch.atvoto.peerreview.checkIn · Viewer nötig

Body {"argumentUri": "at://…"}. Belegt einen Platz und ist vor dem Einreichen nötig. → {"state", "quorum", "graceUntil", …}.

Fehler Status
not_found (kein Gutachten) 404
not_invited 403
closed 409
too_late: Quorum erreicht, keine neuen Gutachter:innen mehr. Wer schon eingecheckt ist, bekommt den aktuellen Stand. 409

POST /xrpc/ch.atvoto.peerreview.activity · Viewer nötig

Body {"argumentUri": "at://…"}. Der Client meldet echtes Tippen (gedrosselt). In provisional_closed verlängert das die Nachfrist, damit niemand mitten im Satz abgeschnitten wird. → {"state", "graceUntil"} (graceUntil = null in open). Fehler: 404 not_found, 409 closed, 403 not_checked_in.


Methode Verhalten
app.bsky.feed.describeFeedGenerator beschreibt den atvoto-Feed
app.bsky.feed.getFeedSkeleton feed (URI des atvoto-Feeds), limit 1–100 (Standard 50), cursor: Crossposts von Argumenten und Kommentaren, neueste zuerst
app.bsky.actor.getProfile, getProfiles, searchActors, getPreferences an die Bluesky-AppView weitergereicht
app.bsky.feed.getTimeline, getAuthorFeed, getPostThread, getPosts, searchPosts, getLikes an die Bluesky-AppView weitergereicht

Hintergrund: Bluesky-Anbindung.

GET /xrpc/com.atproto.label.queryLabels · öffentlich · 60/min

Die Moderations-Labels des Stores, so wie der Registrar sie signiert hat, inklusive Rücknahmen (neg: true). Andere Dienste können sie übernehmen und die Signatur prüfen.

Parameter Pflicht Beschreibung
uriPatterns ja wiederholt; exakte AT-URI oder Präfix mit * am Ende (at://did:plc:…/*)
sources DIDs der Labeler; ohne den atvoto-Labeler ist die Antwort leer
limit 1–250, Standard 50
cursor aus der vorherigen Antwort

Antwort {"labels": [{ver, src, uri, cid?, val, neg?, cts, sig}], "cursor"?}. Konzept: Moderation.

  • GET /.well-known/did.json: DID-Dokument der AppView (did:web) mit den Services #atvoto_appview (für den atproto-proxy-Header) und #bsky_fg (Feed-Generator).
  • GET /healthz: {"status": "ok"} oder 503, wenn die Datenbank nicht erreichbar ist.