{"openapi":"3.1.0","info":{"title":"MonÉlu API","description":"The complete voting record of every deputy in the French Assemblée nationale,\n17th legislature (elected 2024-07-07). Data comes from the Assemblée's own open\ndata exports, is refreshed every weekday morning, and is served here as JSON.\n\n**Data horizon.** Production holds scrutins from **2025-07-01** onward, not from\nthe start of the legislature. A question about an earlier vote has no answer in\nthis dataset - say so rather than inferring one. Deputy profiles cover all 577\nseats regardless of that horizon.\n\n**`position` has four values, and two of them are not the same thing.**\n`pour`, `contre`, `abstention`, and `nonVotant`. An `abstention` is a formal,\ndeliberate abstention cast in the chamber. A `nonVotant` did not cast anything\non that scrutin. Never report a `nonVotant` as an abstention, and never add the\ntwo together.\n\n**`presence_rate` counts `nonVotant` as present, and you cannot recompute it\nfrom the response.** Its numerator is every recorded position including\n`nonVotant`; its denominator is the number of scrutins held during that deputy's\nown mandate window, which is not returned by any endpoint. In particular it is\n*not* `present_votes / total_votes`: `present_votes` uses the opposite\nconvention and excludes `nonVotant`. Quote `presence_rate` as published rather\nthan deriving your own.\n\nA deputy whose mandate window falls entirely outside the data horizon has an\nempty denominator, and `presence_rate` is then `0` by convention rather than a\nreal 0% - check `mandate_end` before reporting a zero as absenteeism. The\nPrésidente de l'Assemblée sits at 100% by design; she appears on every scrutin.\n\nFor participation in the votes that matter most politically, prefer\n`solennel_participation_rate`; for spread across sitting days,\n`voting_days_rate`.\n\n**Pagination.** `offset` is capped at 2000 on `/deputies` and `/votes` (not on\n`/themes/{slug}`). To walk past that ceiling on `/votes`, page with the opaque\n`next_cursor` returned by each response (`?before=<cursor>`), which has no depth\nlimit; `/deputies` has no cursor, but 577 seats fit inside the ceiling anyway.\n\n**Rates are 0-1 floats, not percentages.** `0.87` means 87%.\n\nMethodology, definitions and known caveats: https://mon-elu.vercel.app/methodologie\nData licence: Licence Ouverte / Open Licence 2.0 (Etalab 2.0), attribution required.\n","version":"0.1.0"},"paths":{"/deputies/":{"get":{"tags":["Deputies"],"summary":"List deputies, with name / department / party filters","description":"The roster of the 17th legislature: 577 seats, past and present holders.\n\nUse this to resolve a person's name to the `deputy_id` every other deputy\nendpoint takes. `search` matches anywhere in the full name and is\naccent-sensitive, so pass the name as the Assemblée spells it.\n\n`department` and `party` want the full stored label, not a code or an\nabbreviation: `\"Rassemblement National\"`, not `\"RN\"`; `\"Nord\"`, not `\"59\"`.\n`party_short` in the response is the abbreviation.\n\nIncludes deputies whose mandate has ended (`mandate_end` is set) unless\n`active=true`, which keeps today's Assemblée only (`total` is then the\nnumber of seats filled). Sorted by surname, ignoring accents and case\n(\"Écrivain\" among the E's, \"de Courson\" among the D's). `offset` is capped\nat 2000.","operationId":"listDeputies","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":2000,"minimum":0,"default":0,"title":"Offset"}},{"name":"search","in":"query","required":false,"schema":{"type":"string","description":"Filter by name (case-insensitive)","title":"Search"},"description":"Filter by name (case-insensitive)"},{"name":"department","in":"query","required":false,"schema":{"type":"string","description":"Exact match on the full department name","title":"Department"},"description":"Exact match on the full department name"},{"name":"party","in":"query","required":false,"schema":{"type":"string","description":"Exact match on party name (e.g. 'Rassemblement National')","title":"Party"},"description":"Exact match on party name (e.g. 'Rassemblement National')"},{"name":"active","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"true: only deputies whose mandate is current; false: only ended mandates; omitted: both","title":"Active"},"description":"true: only deputies whose mandate is current; false: only ended mandates; omitted: both"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeputyListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/deputies/stats":{"get":{"tags":["Deputies"],"summary":"Chamber-wide (or single-party) averages, for putting one deputy in context","description":"Mean rates across all deputies, or across one group when `party` is set.\n\nExists so a single deputy's figures can be reported against a baseline\ninstead of in isolation - a presence_rate means little until you know where\nthe chamber as a whole sits.\n\nEvery field is a 0-1 float, or null when the scorecard mart is empty. `party`\ntakes the full group label, the same spelling `/deputies` filters on. These\nare unweighted means over deputies, not chamber-wide totals: a deputy who sat\nfor two months counts as much as one who sat throughout.","operationId":"getDeputyStats","parameters":[{"name":"party","in":"query","required":false,"schema":{"type":"string","description":"Scope the averages to one party (for deputy-vs-party comparison, MON-92)","title":"Party"},"description":"Scope the averages to one party (for deputy-vs-party comparison, MON-92)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeputyStats"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/deputies/scorecards":{"get":{"tags":["Deputies"],"summary":"All deputies' scorecards in one response — feeds the dense table view (MON-97)","description":"Every deputy's scorecard in one unpaginated response, plus group and department.\n\nThe bulk alternative to calling `/deputies/{deputy_id}/scorecard` 577 times -\nuse this for rankings, distributions, or any cross-chamber comparison. Same\nfields and same caveats as the single-deputy scorecard; read that one's notes\non what `presence_rate` does and does not measure before ranking on it.\n\nOrdered by surname. Rate-limited harder than the list endpoints (10/min).","operationId":"listScorecards","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeputyScorecardListResponse"}}}}}}},"/deputies/scorecard.csv":{"get":{"tags":["Deputies"],"summary":"CSV export of every deputy's scorecard (MON-97)","description":"The same rows as `/deputies/scorecards`, as a CSV download.\n\nFor spreadsheets and notebooks. Programmatic callers should prefer the JSON\nendpoint, which carries the field descriptions this format cannot.","operationId":"exportScorecardsCsv","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/deputies/{deputy_id}":{"get":{"tags":["Deputies"],"summary":"One deputy's profile","description":"Identity and mandate for a single deputy - name, group, constituency, photo.\n\n`party` is the deputy's **current** group, not the group they held at the\ntime of any given vote; deputies do change groups mid-legislature. A non-null\n`mandate_end` means the seat has been vacated, so the profile is historical.\n\nNo voting figures here - see `/deputies/{deputy_id}/scorecard`. 404 when the\nid is unknown.","operationId":"getDeputy","parameters":[{"name":"deputy_id","in":"path","required":true,"schema":{"type":"string","title":"Deputy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeputyDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/deputies/{deputy_id}/votes":{"get":{"tags":["Deputies"],"summary":"How one deputy voted, most recent first","description":"One deputy's individual positions, newest first, with the vote's outcome.\n\n`position` is one of `pour`, `contre`, `abstention`, `nonVotant`. The last two\nare different things: an `abstention` was cast deliberately, a `nonVotant` was\nnot cast at all. Do not merge them.\n\n`total` counts every scrutin the deputy has a recorded position on, ignoring\n`since`; `items` is capped by `limit` (max 50) and is the only part `since`\nfilters. There is no offset here - narrow with `since` rather than paging, or\nuse the CSV export for the whole record.\n\n`result` is the scrutin's outcome, not the deputy's: a deputy can vote\n`contre` on a text that was adopted.\n\n`scrutin_kind` says what the scrutin decided - the whole text (`ensemble`),\na `motion`, one `amendement`, one `article`, or `autre` - so a reader can\ntell a final vote from a procedural one.","operationId":"getDeputyVotes","parameters":[{"name":"deputy_id","in":"path","required":true,"schema":{"type":"string","title":"Deputy Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":10,"title":"Limit"}},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Only return votes after this timestamp (ISO 8601) — used to surface what changed since a visitor's last visit.","title":"Since"},"description":"Only return votes after this timestamp (ISO 8601) — used to surface what changed since a visitor's last visit."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeputyVotesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/deputies/{deputy_id}/votes.csv":{"get":{"tags":["Deputies"],"summary":"CSV export of a deputy's full voting record (MON-97)","description":"One deputy's complete voting record as a CSV download - no limit, no paging.\n\nThe way to get the whole record in one request; the JSON endpoint caps at 50\nitems per call. 404 when the id is unknown.","operationId":"exportDeputyVotesCsv","parameters":[{"name":"deputy_id","in":"path","required":true,"schema":{"type":"string","title":"Deputy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/deputies/{deputy_id}/scorecard":{"get":{"tags":["Deputies"],"summary":"One deputy's participation and voting figures","description":"Computed activity figures for a single deputy. All rates are 0-1 floats.\n\n`presence_rate` is the platform's one canonical presence definition\n(ADR-019): every recorded position counts toward the numerator, `nonVotant`\nincluded, because a nonVotant deputy was in the chamber. The denominator is\nthe number of scrutins held during that deputy's own mandate window, so\nsomeone elected mid-legislature is not penalised for votes held before they\ntook their seat.\n\n**That denominator is not in this response, so `presence_rate` cannot be\nrecomputed from these fields.** `total_votes` is the numerator, and\n`present_votes` deliberately uses the opposite convention - it *excludes*\n`nonVotant`. Do not divide one by the other and call it presence.\n\nWhen the mandate window contains no scrutins in the dataset, the rate is `0`\nby convention rather than a measured 0%. That is common for deputies whose\nmandate ended before the 2025-07-01 data horizon, so check `mandate_end` on\nthe profile before reporting a zero as absenteeism.\n\nEven read correctly it is votes cast, not hemicycle attendance: much of the\nAssemblée's work happens in committee, and only a fraction of scrutins are\npolitically significant. `solennel_participation_rate` (scrutins solennels\nonly) and `voting_days_rate` (distinct sitting days with at least one vote\ncast) are the fairer measures for most questions, and `/deputies/stats` is\nthe baseline to read any of them against.\n\n`votes_for_pct` and `abstention_pct` divide by expressed positions only\n(`pour + contre + abstention`) - nonVotant is presence, not an opinion.\n\n404 when the id is unknown.","operationId":"getScorecard","parameters":[{"name":"deputy_id","in":"path","required":true,"schema":{"type":"string","title":"Deputy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeputyScorecard"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/deputies/{deputy_id}/alignment":{"get":{"tags":["Deputies"],"summary":"How often one deputy votes with their own group","description":"Party-line alignment for a single deputy, as 0-1 rates.\n\nA vote counts as dissident when the deputy's expressed position differs from\nthe majority position of their group on that same scrutin. Only expressed\npositions are counted on both sides - `nonVotant` never makes someone a\ndissident, and never dilutes the rate either.\n\nThe group majority is a plurality with ties broken alphabetically\n(abstention < contre < pour), so a genuinely three-way-split group still has\na defined majority. Alignment is computed against the deputy's *current*\ngroup for every vote in the window, which distorts the figure for anyone who\nchanged groups mid-legislature.\n\n404 when the id is unknown.","operationId":"getAlignment","parameters":[{"name":"deputy_id","in":"path","required":true,"schema":{"type":"string","title":"Deputy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeputyAlignment"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/deputies/{deputy_id}/dissident-votes":{"get":{"tags":["Deputies"],"summary":"The specific votes where one deputy broke with their group","description":"The scrutins behind `/alignment`'s dissident_rate, most recent first.\n\nEach item carries both `position` (the deputy's) and `majority_position`\n(their group's), so the divergence is citable rather than asserted. `total`\nis the full count of dissident votes; `items` is capped by `limit`.\n\nSame definition as `/alignment`: expressed positions only, group majority\ntie-broken alphabetically, current group applied across the whole window.","operationId":"getDissidentVotes","parameters":[{"name":"deputy_id","in":"path","required":true,"schema":{"type":"string","title":"Deputy Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":10,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeputyDissidentVotesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/deputies/{deputy_id}/diverging-votes":{"get":{"tags":["Deputies"],"summary":"Head-to-head: the votes where two deputies disagreed","description":"Scrutins where two named deputies cast different expressed positions.\n\n`position_a` belongs to the path deputy, `position_b` to `other_deputy_id`.\nBoth sides must have expressed a position, so a scrutin where either was\n`nonVotant` is not a disagreement and is excluded - `total` is therefore a\nfloor on how differently the two voted, not a complete difference count.\n\nRestricted to scrutins both were eligible for, so comparing deputies whose\nmandates barely overlap yields little. Most recent first.","operationId":"getDivergingVotes","parameters":[{"name":"deputy_id","in":"path","required":true,"schema":{"type":"string","title":"Deputy Id"}},{"name":"other_deputy_id","in":"query","required":true,"schema":{"type":"string","description":"The other deputy to compare against (MON-92)","title":"Other Deputy Id"},"description":"The other deputy to compare against (MON-92)"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":10,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeputyDivergingVotesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/departments/{code}":{"get":{"tags":["Departments"],"summary":"The deputies of one département, with local aggregates","description":"One département addressed by its INSEE code - `59`, `2A`, `971`, `75`.\n\nAnswers \"who represents this place\": the current deputies, the group\nbreakdown, and the recent scrutins where those deputies did not vote\ntogether. A département with a single seat has no split votes by definition.\n\n`split_votes` requires deputies on opposing sides of the same scrutin, so it\nreflects local disagreement rather than national controversy.\n`avg_presence_rate` and `most_dissident` are null when the analytics layer is\nunavailable; the roster still works.\n\nCodes are matched leniently (case, zero-padding), but must be the department\ncode, not the name and not a circonscription. 404 on an unknown code.","operationId":"getDepartment","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}},{"name":"split_votes_limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":10,"title":"Split Votes Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepartmentDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/groups":{"get":{"tags":["Groups"],"summary":"Every parliamentary group with its current seat count","description":"The 12 groups of the Assemblée, largest first, with the slug `GET /groups/{slug}` takes.\n\n`seat_count` counts **current** deputies only (`mandate_end` not set), so\nthe counts add up to the seats filled today, at most 577. A group with no\nsitting member is left out, as `GET /groups/{slug}` answers 404 for it.\n`non-inscrits` is listed like the others but is not a group in any political\nsense: its members belong to none.","operationId":"listGroups","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupListResponse"}}}}}}},"/groups/{slug}":{"get":{"tags":["Groups"],"summary":"A parliamentary group: roster, cohesion, and its most divided votes","description":"One parliamentary group, addressed by slug (`rn`, `lfi`, `epr`, `soc`, …).\n\nThere are 12 groups, including `ni` for the non-inscrits, who are not a group\nin any political sense - just deputies belonging to none. Treat any aggregate\nover `ni` as a statistical artefact rather than a position.\n\n**Current members only.** Deputies who have left the Assemblée or changed\ngroups are absent, and every rate here is computed over today's roster\napplied to the whole voting window. Group composition shifts during a\nlegislature, so this is a snapshot, not a history.\n\n`avg_dissident_rate` is the group's cohesion inverted: higher means members\nbreak ranks more often. `divided_votes` ranks by how close the internal\npour/contre split was, `recent_scrutins` is simply the newest. Both carry\n`majority_position`, the group's plurality on that scrutin (ties broken\nalphabetically), which is the same definition the per-deputy dissidence\nendpoints use.\n\n`seat_rank` is the group's place by current seats among the groups (1 is the\nlargest; equal counts share a rank), null for `ni`.\n\n`avg_presence_rate` and `avg_dissident_rate` are null when the analytics\nlayer is unavailable; the roster and the vote breakdowns still work. 404 on\nan unknown slug or a group with no sitting members.","operationId":"getGroup","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"dissidents_limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":5,"title":"Dissidents Limit"}},{"name":"divided_votes_limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":10,"title":"Divided Votes Limit"}},{"name":"recent_scrutins_limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":10,"title":"Recent Scrutins Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/themes/{slug}":{"get":{"tags":["Themes"],"summary":"Votes on one policy theme, with per-group positioning","description":"Every scrutin MonÉlu has filed under one theme, plus how each group voted on them.\n\n**Themes are MonÉlu's classification, not the Assemblée's.** They are\nassigned per scrutin during ingestion and are a reading aid, not an official\ntaxonomy - a vote's theme is one defensible label among several, and any\nfigure here inherits that judgement. Attribute it to MonÉlu when quoting it.\n\n`party_positions` gives, per group, the pour/contre/abstention split summed\nacross the theme's scrutins, with `pour_rate` over expressed positions only.\n`adoption_rate` is the share adopted among scrutins with a known result, and\nis null when none has one yet. `most_divided_vote` is the narrowest\npour/contre margin in the theme.\n\n404 on an unknown slug.","operationId":"getTheme","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThemeDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/quiz/questions":{"get":{"tags":["Quiz"],"summary":"Le questionnaire du quiz « Quel député vote comme vous ? »","description":"The curated questionnaire behind \"which deputy votes like you\".\n\nEach question is a real scrutin, hand-picked for being legible to a\nnon-specialist and genuinely divisive - one per theme. **The question text is\ncurated in the repository, not derived from the database**, so this is an\neditorial selection and not a representative sample of the Assemblée's work.\nDo not present agreement scores computed from it as a general measure of\nalignment with a deputy.\n\nLive tallies (`votes_for`, `votes_against`, `abstentions`, `result`,\n`vote_date`) are joined in per request, so they stay current even though the\nwording does not.","operationId":"getQuestions","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizQuestionsResponse"}}}}}}},"/quiz/weekly":{"get":{"tags":["Quiz"],"summary":"Le scrutin de la semaine — un scrutin qualifiant, choisi automatiquement","description":"This week's featured scrutin, as a single ready-made question.\n\nPicked by rule rather than curated: the most recent scrutin held **before**\nthe current week that clears the curation thresholds - a vote on a whole\ntext, enough voters, and a genuinely divided outcome - excluding anything\nalready in the `/quiz/questions` set. So it is a recent scrutin surfaced this\nweek, not necessarily one held this week.\n\nCached for the ISO week, so it is stable between calls and rotates on Monday.\n404 when nothing in the recent window qualifies, which happens after a\nrecess; that is a normal response, not a failure.","operationId":"getWeekly","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizWeeklyQuestion"}}}},"404":{"description":"Aucun scrutin qualifiant cette semaine"}}}},"/quiz/match":{"post":{"tags":["Quiz"],"summary":"Calcule votre accord avec chaque député et chaque groupe","description":"Score a set of quiz answers against every deputy and every group.\n\nStateless: nothing is persisted and nothing is logged. Send the answers, get\nthe agreement percentages back.\n\nThe scores are agreement over the handful of curated questions only - a\ndeputy at 90% agrees with you on those scrutins, not on their record. Treat\nthe ranking as a conversation starter, not as a measure of political\nproximity.\n\n`focus_deputy_id` adds one named deputy's match to the response even when\nthey fall below the ranking threshold; 422 when that id is unknown.","operationId":"match","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizMatchRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizMatchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/quiz/share":{"post":{"tags":["Quiz"],"summary":"Partagez votre résultat (recalculé côté serveur, snapshot immuable)","description":"Store a quiz result as an immutable snapshot, readable at `/quiz/s/<id>`.\n\nTakes the same body as `/quiz/match` and **recomputes the result\nserver-side** - percentages supplied by the caller are never trusted or\nstored.\n\nAnswers are excluded by default. `include_answers` opts into storing them so\na later visitor can compare their own result against the sharer's; the same\nflag gates the `themes` summary, which with one question per theme would\notherwise re-encode the answers. Leave it off and the snapshot carries only\nthe aggregate scores.","operationId":"shareResult","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizShareRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizShareResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/quiz/share/{share_id}":{"get":{"tags":["Quiz"],"summary":"Relisez un résultat partagé (aucun recalcul)","description":"Read back a stored quiz result by id. Nothing is recomputed.\n\nThe snapshot is frozen as it was created, so scores reflect the deputies'\nrecords at that moment rather than today's. `answers` and `themes` are\npresent only when the sharer opted in. 404 when the id is unknown.","operationId":"getQuizShare","parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Share Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizShareResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/votes/":{"get":{"tags":["Votes"],"summary":"List scrutins, with full-text search and result / theme / kind filters","description":"Recorded votes of the 17th legislature, newest first.\n\nThe entry point for anything about a bill or a vote: search here, then pass\n`vote_id` to `/votes/{vote_id}` for the per-deputy breakdown.\n\nProduction holds scrutins from **2025-07-01** onward. A question about an\nearlier vote has no answer in this dataset.\n\n`search` is Postgres full-text search over the title and the plain-French\nsummary, with French stemming - pass words, not a sentence, and expect no\nfuzzy matching on names. `result` takes the accented French values `adopté`\nor `rejeté`.\n\n`kind` keeps only scrutins of the given kinds: `ensemble` is a vote on a whole\ntext, `motion` a motion (censure, rejet préalable…), `amendement` and\n`article` the votes on one amendment or one article, which are most of the\ndossier-tagged scrutins, and `autre` everything else (including a scrutin not\nyet classified). `kind=ensemble` therefore lists the votes on whole texts.\nAn unknown value is a 422.\n\nTwo paging modes, and they do not mix. `offset` is capped at 2000; past that\nceiling, pass the `next_cursor` from the previous response as `before=`,\nwhich has no depth limit and silently overrides `offset` when set. `total` is\nthe full filtered count either way. A null `next_cursor` means the last page.\n\nEvery scrutin is a recorded vote. Much of the Assemblée's business passes by\nshow of hands and never appears here at all, so a bill's absence is not\nevidence it was not debated.","operationId":"listVotes","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":2000,"minimum":0,"default":0,"title":"Offset"}},{"name":"before","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Keyset cursor for deep pagination — pass the next_cursor from a previous response. Overrides offset when set, so it reaches votes beyond the offset ceiling.","title":"Before"},"description":"Keyset cursor for deep pagination — pass the next_cursor from a previous response. Overrides offset when set, so it reaches votes beyond the offset ceiling."},{"name":"result","in":"query","required":false,"schema":{"type":"string","description":"Filter by result: adopté | rejeté","title":"Result"},"description":"Filter by result: adopté | rejeté"},{"name":"theme","in":"query","required":false,"schema":{"type":"string","description":"Filter by theme category","title":"Theme"},"description":"Filter by theme category"},{"name":"search","in":"query","required":false,"schema":{"type":"string","description":"Full-text search over vote_title and summary_plain (French stemming)","title":"Search"},"description":"Full-text search over vote_title and summary_plain (French stemming)"},{"name":"kind","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Filter by what the scrutin decided: ensemble | motion | amendement | article | autre. Repeat the parameter or separate values with commas.","title":"Kind"},"description":"Filter by what the scrutin decided: ensemble | motion | amendement | article | autre. Repeat the parameter or separate values with commas."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoteListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/votes/latest":{"get":{"tags":["Votes"],"summary":"The 10 most recent scrutins","description":"The newest 10 scrutins as a bare array - no envelope, no paging, no filters.\n\nA convenience shorthand for \"what did the Assemblée just vote on\"; anything\nelse should use `/votes` with `limit`. Data refreshes on weekday mornings, so\na vote held today typically appears the next morning.","operationId":"latestVotes","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/VoteSummary"},"type":"array","title":"Response Latestvotes"}}}}}}},"/votes/{vote_id}/positions.csv":{"get":{"tags":["Votes"],"summary":"CSV export of every deputy's position on one vote (MON-97)","description":"Every deputy's position on one scrutin as a CSV download, with group and department.\n\nThe same 577-odd rows `/votes/{vote_id}` embeds, in a form spreadsheets can\nread. 404 when the scrutin is unknown.","operationId":"exportVotePositionsCsv","parameters":[{"name":"vote_id","in":"path","required":true,"schema":{"type":"string","title":"Vote Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/votes/{vote_id}":{"get":{"tags":["Votes"],"summary":"One scrutin in full, including how every deputy voted","description":"A single scrutin with its tallies and the complete per-deputy roll call.\n\n`positions` is the whole chamber in one array - typically 577 entries, not\npaginated. Each `position` is `pour`, `contre`, `abstention` or `nonVotant`;\n`abstention` and `nonVotant` are different and must not be summed.\n\nThe aggregate counters (`votes_for`, `votes_against`, `abstentions`,\n`total_voters`) come from the Assemblée's own synthesis rather than from\ncounting `positions`, so prefer them when quoting a tally. `summary_plain` is\nan LLM-generated plain-French gloss of the title and may be null.\n\n`dossier_id` links the scrutin to its bill, but is present on a minority of\nvotes: the Assemblée only began tagging scrutins with a dossier in March 2026.\nIts absence says nothing about the vote. When it is set, `dossier` carries\nthe bill's title and status and `lois_url`, its MonÉlu bill page - null when\nthat bill has no page (a legislature-16 dossier, for one). `dossier` is null\nwhen `dossier_id` is.\n\n404 when the id is unknown.","operationId":"getVote","parameters":[{"name":"vote_id","in":"path","required":true,"schema":{"type":"string","title":"Vote Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoteDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/lois":{"get":{"tags":["Lois"],"summary":"Bills with at least one recorded scrutin, most recently voted first","description":"Every dossier législatif that has a MonÉlu bill page, newest scrutin first.\n\nOnly bills with at least one scrutin linked to them are listed - on the order\nof 70, not the ~2 900 dossiers of the legislature. The rest were deposited\nand never debated in séance, and have no page (a 404 on `/lois/{dossier_uid}`).\n\n`status` is MonÉlu's reading of where the bill stands, one of `promulguee`,\n`conseil_constitutionnel`, `rejetee`, `adoptee_definitivement`, `deposee`,\n`en_commission`, `en_navette`; `status_label` is the Assemblée's own wording\nof the deciding step, verbatim. Quote the label when precision matters -\nthe status is derived from it by rule. An unknown `status` filter is a 422.\n\n`theme` is the most frequent theme among the bill's scrutins; themes are\nMonÉlu's classification, not the Assemblée's. `headline_scrutin_count`\nexcludes amendment and article votes, which dominate: most bills have one\nor two headline scrutins and can have hundreds of amendment votes.","operationId":"listLois","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by derived status, e.g. promulguee or en_navette","title":"Status"},"description":"Filter by derived status, e.g. promulguee or en_navette"},{"name":"theme","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by the bill's most frequent theme","title":"Theme"},"description":"Filter by the bill's most frequent theme"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":2000,"minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoiListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/lois/{dossier_uid}":{"get":{"tags":["Lois"],"summary":"One bill's full parcours, with its headline scrutins attached","description":"Where one bill stands: every procedural step from deposit onward, with votes attached.\n\n`parcours` is the primary array: every acte of the dossier (deposit,\ncommission work, séances, decisions, CMP, Conseil constitutionnel,\npromulgation) in `ordinal` order, flat. The tree is encoded by `parent_uid`\nand `depth`; `ordinal` is a pre-order walk, so a single pass rebuilds it.\nUndated actes are grouping nodes (a reading stage, \"Travaux des\ncommissions\").\n\nHeadline scrutins - votes on the whole text, motions, and other votes that\nare neither amendments nor single articles - sit on the acte they fall\nunder, in `scrutins`. Amendment and article votes are **never listed here**:\neach acte carries `amendement_count` and `article_count`, and\n`/lois/{dossier_uid}/amendements` lists them. A bill can have hundreds.\n\n**The parcours is complete; the scrutins are not.** The Assemblée only\nbegan linking scrutins to their bill on `scrutin_coverage_start`\n(2026-03-26). When `parcours_predates_coverage` is true, the bill's earlier\nstages have real dated actes but no scrutin attached - that is an upstream\ngap, not evidence that nothing was voted. Say so when describing an older\nbill. Scrutins also cover recorded votes only; much passes by show of hands.\n\n`status` is derived by rule from the Assemblée's `status_label`, which is\nreturned verbatim alongside it. 404 for an unknown uid and for a dossier\nwith no scrutin at all, which has no page.","operationId":"getLoi","parameters":[{"name":"dossier_uid","in":"path","required":true,"schema":{"type":"string","title":"Dossier Uid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoiDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/lois/{dossier_uid}/amendements":{"get":{"tags":["Lois"],"summary":"One bill's amendment and article scrutins, on demand","description":"The amendment and single-article scrutins `/lois/{dossier_uid}` only counts.\n\nOldest first, each with the `acte_uid` of the parcours step it falls under -\npass `acte_uid` to list one séance's votes, matching that acte's\n`amendement_count` + `article_count`. `scrutin_kind` is `amendement` or\n`article`. An amendment vote's `result` is the fate of the amendment, not of\nthe bill.\n\nSame 404 rules as the bill page: unknown uid, or a dossier with no page.","operationId":"listLoiAmendements","parameters":[{"name":"dossier_uid","in":"path","required":true,"schema":{"type":"string","title":"Dossier Uid"}},{"name":"acte_uid","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Only the scrutins bound to this acte of the parcours","title":"Acte Uid"},"description":"Only the scrutins bound to this acte of the parcours"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoiAmendementsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/search/":{"post":{"tags":["Search"],"summary":"Posez une question sur les votes et les députés","description":"Ask a question in natural French and get an answer grounded in the corpus.\n\nRetrieval-augmented: the question is routed either to a SQL query over the\ntables or to a vector search over ~5 900 embedded chunks, then answered by an\nLLM over what was retrieved. `sources` carries what the answer was built\nfrom - surface them, because the answer is generated text and can be wrong\nwhere the retrieval was thin. `confidence` and `chunks_retrieved` are the\nsignals for how much to trust it.\n\n**Prefer the structured endpoints whenever the question maps onto one.** For\n\"how did X vote on Y\", `/deputies` then `/deputies/{id}/votes` gives an exact\nanswer; this endpoint gives a plausible one. It costs an LLM call, is limited\nto 10/min, and can take several seconds.\n\nA `suggested_action` of `verify` means the question reads as a factual claim\nrather than a question - `POST /verify/` is the endpoint that checks claims\nagainst the record. Nothing is verified here. 503 when the server has no LLM\ncredentials configured.","operationId":"search","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/search/share":{"post":{"tags":["Search"],"summary":"Partagez une réponse du chat (aucun appel IA - snapshot de la réponse déjà obtenue)","description":"Persist an answer already returned by `POST /search/` as a public snapshot.\n\nStores exactly what the caller passes, verbatim and immutable, and returns an\nid readable at `/chat/s/<id>` on the site. No LLM call, no re-checking: this\nis a save button, not a second opinion, and it will faithfully store a\ndoctored answer. Only pass back a payload this API produced.","operationId":"shareAnswer","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/search/share/{share_id}":{"get":{"tags":["Search"],"summary":"Relisez une réponse partagée (aucun appel IA)","description":"Read back a stored chat answer by id. No LLM call.\n\nSnapshots are immutable and frozen at their creation time, so an answer may\nno longer reflect current data - `created_at` is how far back it goes. 404\nwhen the id is unknown.","operationId":"getChatShare","parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Share Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/verify/":{"post":{"tags":["Verify"],"summary":"Vérifiez une affirmation sur les votes d'un député","description":"Check a factual claim about how a deputy voted, and store the verdict.\n\nGive it a claim, not a question (\"X a voté pour la loi Y\"). It resolves the\nnamed deputy, retrieves the relevant scrutins, joins their recorded\npositions, and returns one of four verdicts: `vrai`, `faux`, `trompeur`, or\n`inverifiable`.\n\n`inverifiable` is the deliberate fallback and is common: it is returned\nwhenever the deputy cannot be resolved, retrieval similarity is too low, or\nthe model produced a factual verdict without a citation that survives\nvalidation against the votes table. Read it as \"this API declines to rule\",\nnever as evidence the claim is false.\n\n`trompeur` is the mixed-record case - true of some scrutins on the bill and\nfalse of others, which a blanket claim obscures.\n\nEvery cited `vote_id` is checked against the database before the response is\nreturned, so citations point at real scrutins. Verdicts are stored immutably\nand readable at `/verifier/v/<id>`. Costs an LLM call; 10/min; 503 when the\nserver has no LLM credentials.","operationId":"verify","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/verify/{verification_id}":{"get":{"tags":["Verify"],"summary":"Relisez un verdict enregistré (aucun appel IA)","description":"Read back a stored verdict by id. No LLM call, no re-verification.\n\nThe verdict is frozen as it was issued; `data_horizon` records the window it\nwas judged against, which may be narrower than today's data. 404 when the id\nis unknown.","operationId":"getVerification","parameters":[{"name":"verification_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Verification Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/keys/usage":{"get":{"tags":["API Keys"],"summary":"Your own API key's usage over the last 30 days","description":"Request counts for the calling key, by endpoint and by day.\n\nRequires the `X-API-Key` header and reports only on that key - there is no\nway to read another key's usage. `rate_limit_multiplier` is the factor\napplied to the anonymous per-minute limits. 401 without a valid key.","operationId":"getKeyUsage","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyUsageResponse"}}}}}}},"/feedback/chat":{"post":{"tags":["Feedback"],"summary":"Envoyer un retour (pouce haut/bas) sur une réponse du chat","description":"Record a thumbs up/down on a chat answer. Write-only.\n\nFeeds a manual triage queue for tuning retrieval and prompts; nothing is\nserved back out and there is no read endpoint. Meant for the site's own UI.","operationId":"submitChatFeedback","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatFeedbackRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/feedback/report":{"post":{"tags":["Feedback"],"summary":"Signaler une erreur sur une page de données (MON-101)","description":"Report a data error on a deputy, vote or other entity page. Write-only.\n\nGoes to the same manual triage queue as chat feedback; nothing is served back\nout and there is no read endpoint. `email` is optional and only used to reply\nabout the report. Corrections are applied upstream, so a report does not\nchange what the API returns.","operationId":"submitErrorReport","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorReportRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/agenda":{"get":{"tags":["Agenda"],"summary":"Upcoming séance publique sittings, grouped by day","description":"What the Assemblée is scheduled to take up in the hemicycle.\n\nWindow defaults to the current ISO week; pass `from` and `to` as ISO dates to\nmove it. The span is capped at 90 days, and the feed's forward coverage is\nshort anyway - asking for months ahead returns thin days, not an error.\n\n**Forward-looking and volatile.** The ordre du jour is rewritten constantly:\nitems are cancelled, reordered and rescheduled. Cancelled items and anything\nmissing from the latest ingestion run are filtered out, so a point that\nvanishes between two calls was dropped upstream. Describe what is here as\nscheduled, never as certain.\n\n`vote_id` and `result` are filled in once a scrutin exists for the item's\ndossier, which turns a scheduled item into a settled one; otherwise\n`dossier_url` points at the official dossier page. Séance publique only -\ncommittee work is not covered.\n\n`dossier_title` is the bill's short official title (the dossier législatif's\n`titre`), null when the item has no dossier or the dossier is not in the\n17th-legislature export. It names the text; `objet` names this point of the\nordre du jour, and `summary_plain` is a machine-written one-line summary of\nit, null by design for a stub `objet` (only `point_type` is meaningful then).\n\n`lois_url` is the item's MonÉlu bill page, null unless the dossier has one -\na bill gets a page only once it has a scrutin (ADR-035), the same rule\n`GET /votes/{vote_id}` applies to its `dossier` block. `dossier_url` stays\nset either way.","operationId":"getAgenda","parameters":[{"name":"from","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"From"}},{"name":"to","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"To"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgendaResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/account/me":{"get":{"tags":["Account"],"summary":"The signed-in caller's own MonÉlu profile","description":"The profile of the account holder whose Supabase access token is presented.\n\nRequires `Authorization: Bearer <access token>`. The API verifies the token's\nsignature, expiry, audience and issuer itself; a forwarded user id or any\nother caller-asserted identity is ignored. 401 when the header is absent,\nthe token does not verify, or no MonÉlu profile exists for that identity\n(create one with `POST /account/me`). 503 when account authentication or\nthe account database is unavailable.\n\nReturns only the caller's own row - there is no way to address another\naccount. Nothing about public data requires an account: every other endpoint\nin this API stays anonymous.","operationId":"getMyProfile","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountProfile"}}}}},"security":[{"HTTPBearer":[]}]},"post":{"tags":["Account"],"summary":"Create the signed-in caller's MonÉlu profile","description":"Create the MonÉlu profile for a verified Supabase identity, on first sign-in.\n\nThis is not sign-up: Supabase Auth has already created and verified the\nidentity, and this only adds the MonÉlu row every other account route\nneeds. The body is optional and takes the same fields as `PATCH\n/account/me`; `preferred_language` defaults to `fr`. A `circonscription`\nrequires a `department_code` (a bare number, only meaningful inside a\ndepartment), and an unknown department code is 422.\n\nIdempotent: 201 with the new profile, or 200 with the existing profile left\nunchanged when one already exists (use `PATCH /account/me` to change it).\n401 without a valid access token.","operationId":"createMyProfile","requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ProfileFields"},{"type":"null"}],"title":"Body"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountProfile"}}}},"200":{"description":"The profile already existed and is returned unchanged"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]},"delete":{"tags":["Account"],"summary":"Delete the signed-in caller's account and all of its data","description":"Irreversibly delete the caller's MonÉlu profile and every row that depends on it.\n\nOne statement removes the profile, and the database cascades it to every\nfollowed deputy, followed theme, bookmark and notification preference - no\nother account is touched. Immediate and total (RGPD article 17, ADR-040\n§6); there is no soft delete and no grace period, so download\n`GET /account/export` first if the data is wanted.\n\nAfterwards the same access token answers 401 on every account route, since\nit no longer resolves to a profile. The sign-in identity itself (the email\naddress) is held by Supabase Auth, outside these tables, and this endpoint\ndoes not remove it: the API holds no Supabase admin credential. 204 on\nsuccess.","operationId":"deleteMyAccount","responses":{"204":{"description":"Successful Response"}},"security":[{"HTTPBearer":[]}]},"patch":{"tags":["Account"],"summary":"Update the signed-in caller's profile","description":"Change any of display name, preferred language, department and circonscription.\n\nOnly the fields present in the body change; send `null` to clear\n`display_name`, `department_code` or `circonscription`. `preferred_language`\nis `fr` or `en` and cannot be cleared. `department_code` is a code as\n`GET /departments/{code}` takes it (`\"83\"`, `\"2A\"`, `\"971\"`), and\n`circonscription` a bare number within it (`\"1\"`); a circonscription left\nwithout a department is 422. Changing `department_code` without also\nsending `circonscription` clears the stored circonscription, since the\nnumber belongs to the old department - so clearing `department_code` with\n`null` clears the circonscription too. A blank `display_name` is 422.\nTerritory is department + circonscription only (ADR-040 §6): no address or\ncommune is collected. Returns the updated\nprofile. 401 without a valid access token or profile.","operationId":"updateMyProfile","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileFields"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountProfile"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/account/export":{"get":{"tags":["Account"],"summary":"Download everything MonÉlu stores about the signed-in caller","description":"Every stored row belonging to the caller, as JSON (RGPD articles 15 and 20).\n\nThe profile (including the Supabase user id it is linked to), followed\ndeputies, followed themes, bookmarks and notification preferences, with the\ncolumn names of the tables they are stored in and no public data joined in.\n`notification_preferences` is null when none were ever saved. Nothing about\nany other account is included, and nothing is derived: MonÉlu computes no\nprofile or label from what a caller follows.\n\nServed with `Content-Disposition: attachment`, so a browser saves it as a\nfile. The email address used to sign in is held by Supabase Auth, outside\nthese tables, and is not part of this export. 401 without a valid access\ntoken or profile.","operationId":"exportMyData","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountExport"}}}}},"security":[{"HTTPBearer":[]}]}},"/account/follows/deputies":{"get":{"tags":["Account"],"summary":"Deputies the signed-in caller follows","description":"The deputies the caller follows, most recently followed first.\n\nEach entry carries the deputy's public identity fields (name, group,\ndepartment, circonscription, portrait) so a client can render the list\nwithout a second call, plus `followed_at`. A former deputy stays in the\nlist; `mandate_end` is on `GET /deputies/{id}`. 401 without a valid access\ntoken or profile.","operationId":"listFollowedDeputies","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/AccountFollowedDeputy"},"type":"array","title":"Response Listfolloweddeputies"}}}}},"security":[{"HTTPBearer":[]}]}},"/account/follows/deputies/{deputy_id}":{"put":{"tags":["Account"],"summary":"Follow a deputy","description":"Add a deputy to the caller's follows. Idempotent: following twice is a no-op.\n\n`deputy_id` is the Assemblée nationale identifier as `GET /deputies` returns\nit (`PA720892`). An id not in the deputies table is 422 and nothing is\nstored. Following records the choice and nothing else: no notification is\nsent on the basis of it. 204 on success; 401 without a valid access token or\nprofile.","operationId":"followDeputy","security":[{"HTTPBearer":[]}],"parameters":[{"name":"deputy_id","in":"path","required":true,"schema":{"type":"string","title":"Deputy Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Account"],"summary":"Unfollow a deputy","description":"Remove a deputy from the caller's follows.\n\nIdempotent: removing a deputy that is not followed (or does not exist) still\nanswers 204, so a client can retry freely. Only the caller's own follow is\never removed. 401 without a valid access token or profile.","operationId":"unfollowDeputy","security":[{"HTTPBearer":[]}],"parameters":[{"name":"deputy_id","in":"path","required":true,"schema":{"type":"string","title":"Deputy Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/account/follows/themes":{"get":{"tags":["Account"],"summary":"Themes the signed-in caller follows","description":"The themes the caller follows, most recently followed first.\n\nEach entry has the theme's `slug` (as `GET /themes/{slug}` takes it), its\ndisplay `name`, and `followed_at`. Followed themes are stored exactly as\nchosen and never aggregated into an inferred political profile (ADR-040\n§6). 401 without a valid access token or profile.","operationId":"listFollowedThemes","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/AccountFollowedTheme"},"type":"array","title":"Response Listfollowedthemes"}}}}},"security":[{"HTTPBearer":[]}]}},"/account/follows/themes/{slug}":{"put":{"tags":["Account"],"summary":"Follow a theme","description":"Add a theme to the caller's follows. Idempotent: following twice is a no-op.\n\n`slug` is one of the ten theme slugs `GET /themes/{slug}` serves\n(`economie-budget`, `sante-social`, ...). Any other value is 422 and nothing\nis stored. 204 on success; 401 without a valid access token or profile.","operationId":"followTheme","security":[{"HTTPBearer":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Account"],"summary":"Unfollow a theme","description":"Remove a theme from the caller's follows.\n\nIdempotent: removing a theme that is not followed still answers 204. Only\nthe caller's own follow is ever removed. 401 without a valid access token or\nprofile.","operationId":"unfollowTheme","security":[{"HTTPBearer":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/account/bookmarks":{"get":{"tags":["Account"],"summary":"Votes the signed-in caller saved","description":"The scrutins the caller bookmarked, most recently saved first.\n\nEach entry carries the vote's public title, date and result (`adopté` /\n`rejeté`) so a client can render the list without a second call, plus\n`bookmarked_at`. Production holds votes from 2025-07-01 onward. 401 without\na valid access token or profile.","operationId":"listBookmarks","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/AccountBookmark"},"type":"array","title":"Response Listbookmarks"}}}}},"security":[{"HTTPBearer":[]}]}},"/account/bookmarks/{vote_id}":{"put":{"tags":["Account"],"summary":"Bookmark a vote","description":"Save a scrutin to the caller's bookmarks. Idempotent: saving twice is a no-op.\n\n`vote_id` is the scrutin identifier as `GET /votes` returns it\n(`VTANR5L17V1234`). An id not in the votes table is 422 and nothing is\nstored. 204 on success; 401 without a valid access token or profile.","operationId":"addBookmark","security":[{"HTTPBearer":[]}],"parameters":[{"name":"vote_id","in":"path","required":true,"schema":{"type":"string","title":"Vote Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Account"],"summary":"Remove a bookmarked vote","description":"Remove a scrutin from the caller's bookmarks.\n\nIdempotent: removing a vote that is not bookmarked still answers 204. Only\nthe caller's own bookmark is ever removed. 401 without a valid access token\nor profile.","operationId":"removeBookmark","security":[{"HTTPBearer":[]}],"parameters":[{"name":"vote_id","in":"path","required":true,"schema":{"type":"string","title":"Vote Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/account/preferences":{"get":{"tags":["Account"],"summary":"The signed-in caller's stored notification preferences","description":"The caller's notification preferences. **Nothing is sent on their basis.**\n\nMonÉlu sends no notifications, digests or alerts today: ADR-040 permits\nonly the transactional sign-in email, and the alerts work (#359) is on hold.\nThese flags are stored so a later feature can honour them, and a client\nmust say so rather than imply delivery. Until the caller first saves one,\nevery flag reads false and `updated_at` is null. 401 without a valid access\ntoken or profile.","operationId":"getPreferences","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountNotificationPreferences"}}}}},"security":[{"HTTPBearer":[]}]},"delete":{"tags":["Account"],"summary":"Reset the signed-in caller's notification preferences","description":"Delete the caller's stored notification preferences, returning them to defaults.\n\nRemoves the stored row, so `GET /account/preferences` reads every flag as\nfalse again and the export shows `notification_preferences: null`.\nIdempotent. 204 on success; 401 without a valid access token or profile.","operationId":"deletePreferences","responses":{"204":{"description":"Successful Response"}},"security":[{"HTTPBearer":[]}]},"patch":{"tags":["Account"],"summary":"Update the signed-in caller's notification preferences","description":"Store any of the three notification flags. **Nothing is sent on their basis.**\n\nFlags omitted from the body (or sent as `null`) keep their stored value, or\ntheir default, false, on first save. Storing a preference sends nothing,\nschedules nothing and enqueues nothing (ADR-040; #359's hold stands).\nReturns the stored preferences. 401 without a valid access token or profile.","operationId":"updatePreferences","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotificationPreferencesUpdate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountNotificationPreferences"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/app/config":{"get":{"tags":["App"],"summary":"Launch configuration for the MonÉlu mobile app","description":"What a MonÉlu mobile app reads at launch instead of hardcoding it.\n\n`min_ios_version` is the oldest app version (`MAJOR.MINOR.PATCH`) this API\nstill serves correctly: an older app must ask its user to update rather than\ncall the API. `features` are remote switches, `true` unless the operator has\nturned one off: `chat` says whether the app should offer `POST /search/`, and\n`verify` whether it should offer `POST /verify/`, both of which depend on a\nthird-party model provider. They are advice to the app, not enforcement -\nboth endpoints keep answering whatever the switches say.\n`data_horizon` is the first date production holds scrutins for (ISO date).\n`caveats` are the reading notes the website prints next to its figures,\nin French, with inline Markdown, each under a stable `id`.\n`site_url` is the website's origin, the base of the links the app shares,\nso a domain move needs no app release.\n\nNothing here depends on the caller, and no account is needed.","operationId":"getAppConfig","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppConfig"}}}}}}},"/health":{"get":{"tags":["Health"],"summary":"Service health, record counts, and data freshness","description":"Liveness plus the numbers that say whether the data behind it is current.\n\n`last_ingestion` is the date of the most recent scrutin in the database\n(`MAX(voted_at)`), not the clock time of the last pipeline run - the honest\nanswer to \"how fresh is this data\" rather than \"when did the job last\nsucceed\". So it moves when the Assemblée sits, and a recess legitimately\nstalls it for weeks; the cron-death signal is dbt source freshness over\n`deputies.ingested_at`, not this field. Two consumers depend on the\ndistinction: the frontend's freshness badge prints it, and its cache tag\nrides the `votes` invalidation scope for exactly this reason (GH #353).\n\n`services.dbt_marts` reports `degraded` when the analytics layer is missing.\nThe API stays up in that state, but the scorecard, alignment and vote-summary\nendpoints return 503 and the group and department pages serve null rates.\n\n`rag_staging_chunks` is normally `null`. A number means the RAG index's\nstaging table exists: either a rebuild is running right now, or one was\nkilled mid-flight and left a second copy of the vectors behind. It clears\non the next successful rebuild.\n\n`groq` comes from a probe of Groq's model catalog, cached for 10 minutes: `ok`\nmeans the key is accepted and every configured model is still served;\n`failing` means the key was rejected or a model was decommissioned, so chat\nand fact-check are down; `degraded` means no usable key is configured;\n`unknown` means the probe itself could not reach Groq, and does not change\n`status`. `groq_detail` says why whenever `groq` is not `ok`.\n\nReturns 200 when every service is ok and 207 when one is degraded; the body\nhas the same shape either way.","operationId":"health","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"AccountBookmark":{"properties":{"vote_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vote Id"},"vote_title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vote Title"},"voted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Voted At"},"result":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Result"},"bookmarked_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Bookmarked At"}},"type":"object","title":"AccountBookmark","description":"A vote the caller saved, with the public fields needed to render it.","example":{"bookmarked_at":"2026-09-25T08:33:00Z","result":"adopté","vote_id":"VTANR5L17V1234","vote_title":"l'ensemble du projet de loi de finances pour 2026","voted_at":"2026-09-18T15:04:00Z"}},"AccountExport":{"properties":{"exported_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Exported At"},"profile":{"anyOf":[{"$ref":"#/components/schemas/AccountExportProfile"},{"type":"null"}]},"followed_deputies":{"items":{"$ref":"#/components/schemas/AccountExportFollowedDeputy"},"type":"array","title":"Followed Deputies","default":[]},"followed_themes":{"items":{"$ref":"#/components/schemas/AccountExportFollowedTheme"},"type":"array","title":"Followed Themes","default":[]},"bookmarks":{"items":{"$ref":"#/components/schemas/AccountExportBookmark"},"type":"array","title":"Bookmarks","default":[]},"notification_preferences":{"anyOf":[{"$ref":"#/components/schemas/AccountExportNotificationPreferences"},{"type":"null"}]}},"type":"object","title":"AccountExport","description":"Every row MonÉlu stores about the caller, as stored (RGPD article 20).\n\nColumn names match the `app_private` tables, and no public data is joined\nin: this is the caller's data, not a rendering of it.","example":{"bookmarks":[{"created_at":"2026-09-25T08:33:00Z","vote_id":"VTANR5L17V1234"}],"exported_at":"2026-09-25T09:00:00Z","followed_deputies":[{"created_at":"2026-09-25T08:31:00Z","deputy_id":"PA720892"}],"followed_themes":[{"created_at":"2026-09-25T08:32:00Z","theme_slug":"energie-environnement"}],"notification_preferences":{"followed_deputy_votes":true,"followed_theme_votes":false,"updated_at":"2026-09-25T08:34:00Z","weekly_digest":false},"profile":{"auth_user_id":"0b6f3c5e-1111-4a2b-9c3d-000000000001","circonscription":"1","created_at":"2026-09-25T08:30:00Z","department_code":"83","display_name":"Camille","id":"7c1d9a52-3f0e-4c1b-9a7e-2b8f6d4e1a30","preferred_language":"fr","updated_at":"2026-09-25T08:30:00Z"}}},"AccountExportBookmark":{"properties":{"vote_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vote Id"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"}},"type":"object","title":"AccountExportBookmark"},"AccountExportFollowedDeputy":{"properties":{"deputy_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deputy Id"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"}},"type":"object","title":"AccountExportFollowedDeputy"},"AccountExportFollowedTheme":{"properties":{"theme_slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Theme Slug"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"}},"type":"object","title":"AccountExportFollowedTheme"},"AccountExportNotificationPreferences":{"properties":{"followed_deputy_votes":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Followed Deputy Votes"},"followed_theme_votes":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Followed Theme Votes"},"weekly_digest":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Weekly Digest"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"}},"type":"object","title":"AccountExportNotificationPreferences"},"AccountExportProfile":{"properties":{"id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Id"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"preferred_language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Preferred Language"},"department_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department Code"},"circonscription":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Circonscription"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"},"auth_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Auth User Id"}},"type":"object","title":"AccountExportProfile","description":"The stored profile row, including the Supabase user id it is linked to.","example":{"auth_user_id":"0b6f3c5e-1111-4a2b-9c3d-000000000001","circonscription":"1","created_at":"2026-09-25T08:30:00Z","department_code":"83","display_name":"Camille","id":"7c1d9a52-3f0e-4c1b-9a7e-2b8f6d4e1a30","preferred_language":"fr","updated_at":"2026-09-25T08:30:00Z"}},"AccountFollowedDeputy":{"properties":{"deputy_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deputy Id"},"full_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Full Name"},"party":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Party"},"party_short":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Party Short"},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"},"circonscription":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Circonscription"},"photo_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Photo Url"},"followed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Followed At"}},"type":"object","title":"AccountFollowedDeputy","description":"A deputy the caller follows, with the public fields needed to render it.","example":{"circonscription":"10","department":"Val-de-Marne","deputy_id":"PA720892","followed_at":"2026-09-25T08:31:00Z","full_name":"Mathilde Panot","party":"La France insoumise - Nouveau Front Populaire","party_short":"LFI","photo_url":"https://www.assemblee-nationale.fr/dyn/static/tribun/17/photos/carre/720892.jpg"}},"AccountFollowedTheme":{"properties":{"slug":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Slug"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"followed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Followed At"}},"type":"object","title":"AccountFollowedTheme","description":"A theme the caller follows. `slug` is one of the ten in /themes/{slug}.","example":{"followed_at":"2026-09-25T08:32:00Z","name":"Énergie & Environnement","slug":"energie-environnement"}},"AccountNotificationPreferences":{"properties":{"followed_deputy_votes":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Followed Deputy Votes"},"followed_theme_votes":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Followed Theme Votes"},"weekly_digest":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Weekly Digest"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"}},"type":"object","title":"AccountNotificationPreferences","description":"Stored notification choices. Nothing is sent on their basis (ADR-040).\n\n`updated_at` is absent until the caller first saves a preference; until then\nevery flag reads as its default, false.","example":{"followed_deputy_votes":true,"followed_theme_votes":false,"updated_at":"2026-09-25T08:34:00Z","weekly_digest":false}},"AccountProfile":{"properties":{"id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Id"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"preferred_language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Preferred Language"},"department_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department Code"},"circonscription":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Circonscription"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"}},"type":"object","title":"AccountProfile","description":"The signed-in caller's own profile, read through the restricted role.\n\n`id` is MonÉlu's profile id, never the Supabase user id. No token, claim or\nemail is ever part of this response.","example":{"circonscription":"1","created_at":"2026-09-25T08:30:00Z","department_code":"83","display_name":"Camille","id":"7c1d9a52-3f0e-4c1b-9a7e-2b8f6d4e1a30","preferred_language":"fr","updated_at":"2026-09-25T08:30:00Z"}},"AgendaDay":{"properties":{"sitting_date":{"type":"string","format":"date","title":"Sitting Date"},"items":{"items":{"$ref":"#/components/schemas/AgendaItem"},"type":"array","title":"Items"}},"type":"object","required":["sitting_date","items"],"title":"AgendaDay"},"AgendaItem":{"properties":{"point_uid":{"type":"string","title":"Point Uid"},"sitting_start":{"type":"string","format":"date-time","title":"Sitting Start"},"sitting_end":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Sitting End"},"objet":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Objet"},"point_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Point Type"},"summary_plain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary Plain"},"theme":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Theme"},"dossier_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dossier Id"},"dossier_title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dossier Title","description":"The bill's short official title, when the dossier is known (#525)"},"dossier_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dossier Url","description":"Official AN dossier page; set whenever dossier_id is known"},"lois_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lois Url","description":"The bill's MonÉlu page; null unless the dossier has one (ADR-035, #511)"},"vote_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vote Id","description":"Set once a scrutin exists for this item's dossier"},"result":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Result"}},"type":"object","required":["point_uid","sitting_start"],"title":"AgendaItem"},"AgendaResponse":{"properties":{"from_date":{"type":"string","format":"date","title":"From Date"},"to_date":{"type":"string","format":"date","title":"To Date"},"days":{"items":{"$ref":"#/components/schemas/AgendaDay"},"type":"array","title":"Days"}},"type":"object","required":["from_date","to_date","days"],"title":"AgendaResponse","example":{"days":[{"items":[{"dossier_id":"DLR5L17N52985","dossier_title":"Lutte contre les fraudes sociales et fiscales","dossier_url":"https://www.assemblee-nationale.fr/dyn/17/dossiers/DLR5L17N52985","lois_url":"https://mon-elu.vercel.app/lois/DLR5L17N52985","objet":"Projet de loi relatif à la lutte contre les fraudes sociales et fiscales (première lecture)","point_type":"Discussion générale","point_uid":"PTOD17_123456","sitting_end":"2026-09-02T20:00:00Z","sitting_start":"2026-09-02T15:00:00Z"}],"sitting_date":"2026-09-02"}],"from_date":"2026-09-01","to_date":"2026-09-07"}},"ApiKeyUsageDay":{"properties":{"endpoint":{"type":"string","title":"Endpoint"},"day":{"type":"string","format":"date","title":"Day"},"request_count":{"type":"integer","title":"Request Count"}},"type":"object","required":["endpoint","day","request_count"],"title":"ApiKeyUsageDay"},"ApiKeyUsageResponse":{"properties":{"label":{"type":"string","title":"Label"},"rate_limit_multiplier":{"type":"integer","title":"Rate Limit Multiplier"},"items":{"items":{"$ref":"#/components/schemas/ApiKeyUsageDay"},"type":"array","title":"Items"}},"type":"object","required":["label","rate_limit_multiplier","items"],"title":"ApiKeyUsageResponse"},"AppCaveat":{"properties":{"id":{"type":"string","title":"Id"},"text":{"type":"string","title":"Text"}},"type":"object","required":["id","text"],"title":"AppCaveat","description":"One caveat, keyed by a stable id so the app can place it by the number it qualifies."},"AppConfig":{"properties":{"min_ios_version":{"type":"string","title":"Min Ios Version"},"features":{"$ref":"#/components/schemas/AppFeatures"},"data_horizon":{"type":"string","format":"date","title":"Data Horizon"},"caveats":{"items":{"$ref":"#/components/schemas/AppCaveat"},"type":"array","title":"Caveats","default":[]},"site_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Site Url","description":"The public website's origin, without a trailing slash: the base of every link the app shares (`<site_url>/votes/<vote_id>`)"}},"type":"object","required":["min_ios_version","features","data_horizon"],"title":"AppConfig","description":"Configuration a native app reads at launch instead of hardcoding it.","example":{"caveats":[{"id":"vote_result","text":"Le résultat d'un scrutin (adopté / rejeté) est repris tel quel de l'Assemblée nationale, jamais recalculé."}],"data_horizon":"2025-07-01","features":{"chat":true,"verify":true},"min_ios_version":"1.0.0","site_url":"https://mon-elu.vercel.app"}},"AppFeatures":{"properties":{"chat":{"type":"boolean","title":"Chat","default":true},"verify":{"type":"boolean","title":"Verify","default":true}},"type":"object","title":"AppFeatures","description":"Remote switches for features that depend on a third party."},"ChatFeedbackRequest":{"properties":{"vote":{"type":"string","pattern":"^(up|down)$","title":"Vote"},"question":{"type":"string","maxLength":500,"minLength":1,"title":"Question"},"answer":{"type":"string","maxLength":8000,"minLength":1,"title":"Answer"},"sources":{"items":{"$ref":"#/components/schemas/ShareSourceItem"},"type":"array","maxItems":10,"title":"Sources"}},"type":"object","required":["vote","question","answer"],"title":"ChatFeedbackRequest"},"CitationItem":{"properties":{"vote_id":{"type":"string","title":"Vote Id"},"title":{"type":"string","title":"Title"},"voted_at":{"type":"string","title":"Voted At"},"result":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Result"},"deputy_position":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deputy Position"}},"type":"object","required":["vote_id","title","voted_at"],"title":"CitationItem"},"DepartmentDeputy":{"properties":{"deputy_id":{"type":"string","title":"Deputy Id"},"full_name":{"type":"string","title":"Full Name"},"party":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Party"},"party_short":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Party Short"},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"},"circonscription":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Circonscription"},"photo_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Photo Url"},"presence_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Presence Rate"},"solennel_participation_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Solennel Participation Rate"},"party_alignment_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Party Alignment Rate"},"dissident_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Dissident Rate"}},"type":"object","required":["deputy_id","full_name"],"title":"DepartmentDeputy","description":"Deputy row on a department page — summary plus scorecard highlights.\n\nMart-derived rates are None when the dbt marts are absent (the page\ndegrades gracefully instead of failing).","example":{"circonscription":"10","department":"Val-de-Marne","deputy_id":"PA720892","dissident_rate":0.0874,"full_name":"Mathilde Panot","party":"La France insoumise - Nouveau Front Populaire","party_alignment_rate":0.9126,"party_short":"LFI","photo_url":"https://www.assemblee-nationale.fr/dyn/static/tribun/17/photos/carre/720892.jpg","presence_rate":0.954,"solennel_participation_rate":0.875}},"DepartmentDetail":{"properties":{"code":{"type":"string","title":"Code"},"name":{"type":"string","title":"Name"},"deputy_count":{"type":"integer","title":"Deputy Count"},"deputies":{"items":{"$ref":"#/components/schemas/DepartmentDeputy"},"type":"array","title":"Deputies"},"avg_presence_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Presence Rate","description":"Mean presence_rate across the department's deputies; None when the scorecard mart is unavailable."},"party_distribution":{"items":{"$ref":"#/components/schemas/DepartmentPartyCount"},"type":"array","title":"Party Distribution"},"most_dissident":{"anyOf":[{"$ref":"#/components/schemas/DepartmentDeputy"},{"type":"null"}],"description":"Deputy with the highest dissident_rate; None when the alignment mart is unavailable or the department is empty."},"split_votes":{"items":{"$ref":"#/components/schemas/DepartmentSplitVote"},"type":"array","title":"Split Votes"}},"type":"object","required":["code","name","deputy_count","deputies","party_distribution","split_votes"],"title":"DepartmentDetail"},"DepartmentPartyCount":{"properties":{"party":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Party"},"count":{"type":"integer","title":"Count"}},"type":"object","required":["count"],"title":"DepartmentPartyCount"},"DepartmentSplitVote":{"properties":{"vote_id":{"type":"string","title":"Vote Id"},"voted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Voted At"},"vote_title":{"type":"string","title":"Vote Title"},"result":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Result"},"pour":{"type":"integer","title":"Pour"},"contre":{"type":"integer","title":"Contre"},"abstention":{"type":"integer","title":"Abstention"}},"type":"object","required":["vote_id","vote_title","pour","contre","abstention"],"title":"DepartmentSplitVote","description":"A recent vote where the department's deputies expressed opposing positions."},"DeputyAlignment":{"properties":{"deputy_id":{"type":"string","title":"Deputy Id"},"full_name":{"type":"string","title":"Full Name"},"party":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Party"},"total_votes":{"type":"integer","title":"Total Votes","description":"Votes counted toward alignment (pour/contre/abstention)"},"aligned_votes":{"type":"integer","title":"Aligned Votes"},"dissident_votes":{"type":"integer","title":"Dissident Votes"},"party_alignment_rate":{"type":"number","title":"Party Alignment Rate","description":"aligned_votes / total_votes, 0-1"},"dissident_rate":{"type":"number","title":"Dissident Rate","description":"dissident_votes / total_votes, 0-1"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"}},"type":"object","required":["deputy_id","full_name","total_votes","aligned_votes","dissident_votes","party_alignment_rate","dissident_rate"],"title":"DeputyAlignment","description":"Party alignment / dissident rate for a single deputy (mart_party_alignment).","example":{"aligned_votes":407,"deputy_id":"PA720892","dissident_rate":0.0874,"dissident_votes":39,"full_name":"Mathilde Panot","party":"La France insoumise - Nouveau Front Populaire","party_alignment_rate":0.9126,"total_votes":446,"updated_at":"2026-08-20T06:52:54Z"}},"DeputyDetail":{"properties":{"deputy_id":{"type":"string","title":"Deputy Id"},"full_name":{"type":"string","title":"Full Name"},"party":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Party"},"party_short":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Party Short"},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"},"circonscription":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Circonscription"},"photo_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Photo Url"},"first_name":{"type":"string","title":"First Name"},"last_name":{"type":"string","title":"Last Name"},"mandate_start":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Mandate Start"},"mandate_end":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Mandate End"},"ingested_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ingested At"}},"type":"object","required":["deputy_id","full_name","first_name","last_name"],"title":"DeputyDetail","description":"Full deputy profile — used in GET /deputies/{deputy_id}.","example":{"circonscription":"10","department":"Val-de-Marne","deputy_id":"PA720892","first_name":"Mathilde","full_name":"Mathilde Panot","ingested_at":"2026-08-20T06:52:54Z","last_name":"Panot","mandate_start":"2024-07-07","party":"La France insoumise - Nouveau Front Populaire","party_short":"LFI","photo_url":"https://www.assemblee-nationale.fr/dyn/static/tribun/17/photos/carre/720892.jpg"}},"DeputyDissidentVotesResponse":{"properties":{"deputy_id":{"type":"string","title":"Deputy Id"},"total":{"type":"integer","title":"Total"},"items":{"items":{"$ref":"#/components/schemas/DissidentVoteItem"},"type":"array","title":"Items"}},"type":"object","required":["deputy_id","total","items"],"title":"DeputyDissidentVotesResponse"},"DeputyDivergingVotesResponse":{"properties":{"deputy_a_id":{"type":"string","title":"Deputy A Id"},"deputy_b_id":{"type":"string","title":"Deputy B Id"},"total":{"type":"integer","title":"Total"},"items":{"items":{"$ref":"#/components/schemas/DivergingVoteItem"},"type":"array","title":"Items"}},"type":"object","required":["deputy_a_id","deputy_b_id","total","items"],"title":"DeputyDivergingVotesResponse"},"DeputyListItem":{"properties":{"deputy_id":{"type":"string","title":"Deputy Id"},"full_name":{"type":"string","title":"Full Name"},"party":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Party"},"party_short":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Party Short"},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"},"circonscription":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Circonscription"},"photo_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Photo Url"},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name","description":"Surname as the Assemblée files it; the list's sort key"}},"type":"object","required":["deputy_id","full_name"],"title":"DeputyListItem","description":"A deputy in `GET /deputies`, with the surname the list is sorted by.","example":{"circonscription":"10","department":"Val-de-Marne","deputy_id":"PA720892","full_name":"Mathilde Panot","last_name":"Panot","party":"La France insoumise - Nouveau Front Populaire","party_short":"LFI","photo_url":"https://www.assemblee-nationale.fr/dyn/static/tribun/17/photos/carre/720892.jpg"}},"DeputyListResponse":{"properties":{"total":{"type":"integer","title":"Total"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"items":{"items":{"$ref":"#/components/schemas/DeputyListItem"},"type":"array","title":"Items"}},"type":"object","required":["total","limit","offset","items"],"title":"DeputyListResponse","example":{"items":[{"circonscription":"10","department":"Val-de-Marne","deputy_id":"PA720892","full_name":"Mathilde Panot","last_name":"Panot","party":"La France insoumise - Nouveau Front Populaire","party_short":"LFI","photo_url":"https://www.assemblee-nationale.fr/dyn/static/tribun/17/photos/carre/720892.jpg"}],"limit":50,"offset":0,"total":1}},"DeputyRef":{"properties":{"deputy_id":{"type":"string","title":"Deputy Id"},"name":{"type":"string","title":"Name"},"party":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Party"}},"type":"object","required":["deputy_id","name"],"title":"DeputyRef"},"DeputyScorecard":{"properties":{"deputy_id":{"type":"string","title":"Deputy Id"},"full_name":{"type":"string","title":"Full Name"},"total_votes":{"type":"integer","title":"Total Votes","description":"Scrutins the deputy has a recorded position on, nonVotant included. This is presence_rate's numerator, not its denominator."},"present_votes":{"type":"integer","title":"Present Votes","description":"Recorded positions excluding 'nonVotant' (in chamber but did not vote). Deliberately the opposite convention to presence_rate — not its numerator."},"presence_rate":{"type":"number","title":"Presence Rate","description":"Canonical presence (ADR-019), 0–1: recorded positions (nonVotant included) over the scrutins held during this deputy's mandate window. That denominator is not returned, so this is NOT present_votes / total_votes — present_votes uses the opposite convention. 0 when the mandate window contains no scrutins in the dataset."},"votes_for":{"type":"integer","title":"Votes For"},"votes_against":{"type":"integer","title":"Votes Against"},"abstentions":{"type":"integer","title":"Abstentions"},"votes_for_pct":{"type":"number","title":"Votes For Pct","description":"votes_for / present_votes, 0–1"},"abstention_pct":{"type":"number","title":"Abstention Pct","description":"abstentions / present_votes, 0–1"},"eligible_solennels":{"type":"integer","title":"Eligible Solennels","description":"Scrutins solennels held during the deputy's mandate window"},"solennels_cast":{"type":"integer","title":"Solennels Cast","description":"Of those, how many the deputy voted on"},"solennel_participation_rate":{"type":"number","title":"Solennel Participation Rate","description":"solennels_cast / eligible_solennels, 0–1 (see MON-124)"},"eligible_voting_days":{"type":"integer","title":"Eligible Voting Days","description":"Distinct calendar days with at least one scrutin during the mandate window"},"voting_days_present":{"type":"integer","title":"Voting Days Present","description":"Of those, how many days the deputy cast at least one vote"},"voting_days_rate":{"type":"number","title":"Voting Days Rate","description":"voting_days_present / eligible_voting_days, 0–1 (see MON-124)"}},"type":"object","required":["deputy_id","full_name","total_votes","present_votes","presence_rate","votes_for","votes_against","abstentions","votes_for_pct","abstention_pct","eligible_solennels","solennels_cast","solennel_participation_rate","eligible_voting_days","voting_days_present","voting_days_rate"],"title":"DeputyScorecard","description":"Computed voting stats for a single deputy.","example":{"abstention_pct":0.0737,"abstentions":30,"deputy_id":"PA720892","eligible_solennels":24,"eligible_voting_days":62,"full_name":"Mathilde Panot","presence_rate":0.954,"present_votes":407,"solennel_participation_rate":0.875,"solennels_cast":21,"total_votes":446,"votes_against":261,"votes_for":116,"votes_for_pct":0.285,"voting_days_present":41,"voting_days_rate":0.6613}},"DeputyScorecardListResponse":{"properties":{"total":{"type":"integer","title":"Total"},"items":{"items":{"$ref":"#/components/schemas/DeputyScorecardRow"},"type":"array","title":"Items"}},"type":"object","required":["total","items"],"title":"DeputyScorecardListResponse"},"DeputyScorecardRow":{"properties":{"deputy_id":{"type":"string","title":"Deputy Id"},"full_name":{"type":"string","title":"Full Name"},"total_votes":{"type":"integer","title":"Total Votes","description":"Scrutins the deputy has a recorded position on, nonVotant included. This is presence_rate's numerator, not its denominator."},"present_votes":{"type":"integer","title":"Present Votes","description":"Recorded positions excluding 'nonVotant' (in chamber but did not vote). Deliberately the opposite convention to presence_rate — not its numerator."},"presence_rate":{"type":"number","title":"Presence Rate","description":"Canonical presence (ADR-019), 0–1: recorded positions (nonVotant included) over the scrutins held during this deputy's mandate window. That denominator is not returned, so this is NOT present_votes / total_votes — present_votes uses the opposite convention. 0 when the mandate window contains no scrutins in the dataset."},"votes_for":{"type":"integer","title":"Votes For"},"votes_against":{"type":"integer","title":"Votes Against"},"abstentions":{"type":"integer","title":"Abstentions"},"votes_for_pct":{"type":"number","title":"Votes For Pct","description":"votes_for / present_votes, 0–1"},"abstention_pct":{"type":"number","title":"Abstention Pct","description":"abstentions / present_votes, 0–1"},"eligible_solennels":{"type":"integer","title":"Eligible Solennels","description":"Scrutins solennels held during the deputy's mandate window"},"solennels_cast":{"type":"integer","title":"Solennels Cast","description":"Of those, how many the deputy voted on"},"solennel_participation_rate":{"type":"number","title":"Solennel Participation Rate","description":"solennels_cast / eligible_solennels, 0–1 (see MON-124)"},"eligible_voting_days":{"type":"integer","title":"Eligible Voting Days","description":"Distinct calendar days with at least one scrutin during the mandate window"},"voting_days_present":{"type":"integer","title":"Voting Days Present","description":"Of those, how many days the deputy cast at least one vote"},"voting_days_rate":{"type":"number","title":"Voting Days Rate","description":"voting_days_present / eligible_voting_days, 0–1 (see MON-124)"},"party":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Party"},"party_short":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Party Short"},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"}},"type":"object","required":["deputy_id","full_name","total_votes","present_votes","presence_rate","votes_for","votes_against","abstentions","votes_for_pct","abstention_pct","eligible_solennels","solennels_cast","solennel_participation_rate","eligible_voting_days","voting_days_present","voting_days_rate"],"title":"DeputyScorecardRow","description":"Scorecard plus party/department context — one row of the dense table (MON-97).","example":{"abstention_pct":0.0737,"abstentions":30,"department":"Val-de-Marne","deputy_id":"PA720892","eligible_solennels":24,"eligible_voting_days":62,"full_name":"Mathilde Panot","party":"La France insoumise - Nouveau Front Populaire","party_short":"LFI","presence_rate":0.954,"present_votes":407,"solennel_participation_rate":0.875,"solennels_cast":21,"total_votes":446,"votes_against":261,"votes_for":116,"votes_for_pct":0.285,"voting_days_present":41,"voting_days_rate":0.6613}},"DeputyStats":{"properties":{"avg_presence_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Presence Rate","description":"Average presence_rate across all deputies, 0–1; null when the mart is empty"},"avg_solennel_participation_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Solennel Participation Rate","description":"Average solennel_participation_rate across all deputies, 0–1 (MON-124)"},"avg_voting_days_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Voting Days Rate","description":"Average voting_days_rate across all deputies, 0–1 (MON-124)"},"avg_votes_for_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Votes For Pct","description":"Average votes_for_pct, 0–1; scoped to `party` when set (MON-92)"},"avg_abstention_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Abstention Pct","description":"Average abstention_pct, 0–1; scoped to `party` when set (MON-92)"}},"type":"object","title":"DeputyStats","example":{"avg_abstention_pct":0.0554,"avg_presence_rate":0.2564,"avg_solennel_participation_rate":0.8447,"avg_votes_for_pct":0.4209,"avg_voting_days_rate":0.5553}},"DeputyVoteItem":{"properties":{"vote_id":{"type":"string","title":"Vote Id"},"voted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Voted At"},"vote_title":{"type":"string","title":"Vote Title"},"result":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Result"},"position":{"type":"string","title":"Position"},"summary_plain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary Plain"},"scrutin_kind":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scrutin Kind","description":"What the scrutin decided: ensemble | motion | amendement | article | autre; null for a scrutin not yet classified"}},"type":"object","required":["vote_id","vote_title","position"],"title":"DeputyVoteItem"},"DeputyVotesResponse":{"properties":{"deputy_id":{"type":"string","title":"Deputy Id"},"total":{"type":"integer","title":"Total"},"items":{"items":{"$ref":"#/components/schemas/DeputyVoteItem"},"type":"array","title":"Items"}},"type":"object","required":["deputy_id","total","items"],"title":"DeputyVotesResponse","example":{"deputy_id":"PA720892","items":[{"position":"contre","result":"adopté","scrutin_kind":"ensemble","summary_plain":"Le texte issu de la commission mixte paritaire a été adopté.","vote_id":"VTANR5L17V8433","vote_title":"l'ensemble du projet de loi visant à offrir des réponses immédiates aux phénomènes troublant l'ordre public (texte de la commission mixte paritaire).","voted_at":"2026-07-21T00:00:00Z"}],"total":446}},"DissidentVoteItem":{"properties":{"vote_id":{"type":"string","title":"Vote Id"},"voted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Voted At"},"vote_title":{"type":"string","title":"Vote Title"},"result":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Result"},"position":{"type":"string","title":"Position"},"majority_position":{"type":"string","title":"Majority Position"}},"type":"object","required":["vote_id","vote_title","position","majority_position"],"title":"DissidentVoteItem","description":"A single vote where the deputy diverged from their party's majority position."},"DivergingVoteItem":{"properties":{"vote_id":{"type":"string","title":"Vote Id"},"voted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Voted At"},"vote_title":{"type":"string","title":"Vote Title"},"result":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Result"},"summary_plain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary Plain"},"position_a":{"type":"string","title":"Position A"},"position_b":{"type":"string","title":"Position B"}},"type":"object","required":["vote_id","vote_title","position_a","position_b"],"title":"DivergingVoteItem","description":"A vote where two deputies (a and b) cast opposite expressed positions."},"ErrorReportRequest":{"properties":{"entity_type":{"type":"string","pattern":"^(deputy|vote|page)$","title":"Entity Type"},"entity_id":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Entity Id"},"entity_label":{"anyOf":[{"type":"string","maxLength":300},{"type":"null"}],"title":"Entity Label"},"page_url":{"anyOf":[{"type":"string","maxLength":300},{"type":"null"}],"title":"Page Url"},"message":{"type":"string","maxLength":2000,"minLength":1,"title":"Message"},"email":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Email"}},"type":"object","required":["entity_type","message"],"title":"ErrorReportRequest"},"FeedbackResponse":{"properties":{"status":{"type":"string","title":"Status","default":"ok"}},"type":"object","title":"FeedbackResponse"},"GroupDetail":{"properties":{"slug":{"type":"string","title":"Slug"},"name":{"type":"string","title":"Name"},"member_count":{"type":"integer","title":"Member Count"},"seat_rank":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Seat Rank","description":"The group's place by current seats among the groups (1 = largest); None for the non-inscrits (#526)."},"members":{"items":{"$ref":"#/components/schemas/GroupMember"},"type":"array","title":"Members"},"avg_presence_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Presence Rate","description":"Mean presence_rate across the group's current members; None when the scorecard mart is unavailable."},"avg_dissident_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Dissident Rate","description":"Mean dissident_rate across the group's current members — the group's cohesion score, inverted (higher = less cohesive); None when the alignment mart is unavailable."},"most_dissident_members":{"items":{"$ref":"#/components/schemas/GroupMember"},"type":"array","title":"Most Dissident Members","description":"Current members with the highest dissident_rate, descending."},"divided_votes":{"items":{"$ref":"#/components/schemas/GroupVoteBreakdown"},"type":"array","title":"Divided Votes","description":"Recent votes where the group's members split furthest (smallest of pour/contre is largest), most divided first."},"recent_scrutins":{"items":{"$ref":"#/components/schemas/GroupVoteBreakdown"},"type":"array","title":"Recent Scrutins","description":"The group's most recent scrutins with its aggregate position, newest first."}},"type":"object","required":["slug","name","member_count","members"],"title":"GroupDetail"},"GroupListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/GroupSummary"},"type":"array","title":"Items"}},"type":"object","required":["items"],"title":"GroupListResponse","example":{"items":[{"name":"Rassemblement National","party_short":"RN","seat_count":123,"slug":"rassemblement-national"},{"name":"Ensemble pour la République","party_short":"EPR","seat_count":91,"slug":"ensemble-pour-la-republique"}]}},"GroupMember":{"properties":{"deputy_id":{"type":"string","title":"Deputy Id"},"full_name":{"type":"string","title":"Full Name"},"party":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Party"},"party_short":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Party Short"},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"},"circonscription":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Circonscription"},"photo_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Photo Url"},"presence_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Presence Rate"},"dissident_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Dissident Rate"}},"type":"object","required":["deputy_id","full_name"],"title":"GroupMember","description":"A group's current deputy — summary plus scorecard highlights.\n\nMart-derived rates are None when the dbt marts are absent (the page\ndegrades gracefully instead of failing), mirroring DepartmentDeputy.","example":{"circonscription":"10","department":"Val-de-Marne","deputy_id":"PA720892","dissident_rate":0.0874,"full_name":"Mathilde Panot","party":"La France insoumise - Nouveau Front Populaire","party_short":"LFI","photo_url":"https://www.assemblee-nationale.fr/dyn/static/tribun/17/photos/carre/720892.jpg","presence_rate":0.954}},"GroupSummary":{"properties":{"slug":{"type":"string","title":"Slug"},"name":{"type":"string","title":"Name"},"party_short":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Party Short","description":"The group's abbreviation, e.g. `RN`"},"seat_count":{"type":"integer","title":"Seat Count","description":"Deputies sitting in the group today (mandate not ended)"}},"type":"object","required":["slug","name","seat_count"],"title":"GroupSummary","description":"One parliamentary group and how many seats it holds today."},"GroupVoteBreakdown":{"properties":{"vote_id":{"type":"string","title":"Vote Id"},"voted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Voted At"},"vote_title":{"type":"string","title":"Vote Title"},"result":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Result"},"pour":{"type":"integer","title":"Pour"},"contre":{"type":"integer","title":"Contre"},"abstention":{"type":"integer","title":"Abstention"},"majority_position":{"type":"string","title":"Majority Position","description":"pour/contre/abstention — whichever the group cast most of on this vote"}},"type":"object","required":["vote_id","vote_title","pour","contre","abstention","majority_position"],"title":"GroupVoteBreakdown","description":"A vote's outcome among a group's current members: how the group split."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"LoiActe":{"properties":{"acte_uid":{"type":"string","title":"Acte Uid"},"parent_uid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parent Uid"},"depth":{"type":"integer","title":"Depth"},"ordinal":{"type":"integer","title":"Ordinal"},"code_acte":{"type":"string","title":"Code Acte"},"libelle":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Libelle"},"date_acte":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Acte","description":"Null on grouping nodes such as a reading stage"},"statut_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Statut Label","description":"The AN's verbatim outcome, on decision actes"},"scrutins":{"items":{"$ref":"#/components/schemas/LoiScrutin"},"type":"array","title":"Scrutins","default":[]},"amendement_count":{"type":"integer","title":"Amendement Count","default":0},"article_count":{"type":"integer","title":"Article Count","default":0}},"type":"object","required":["acte_uid","depth","ordinal","code_acte"],"title":"LoiActe","description":"One step of the parcours. The tree is flat: rebuild it from parent_uid / depth."},"LoiAmendementScrutin":{"properties":{"vote_id":{"type":"string","title":"Vote Id"},"voted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Voted At"},"vote_title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vote Title"},"scrutin_kind":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scrutin Kind","description":"amendement | article"},"result":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Result","description":"The fate of the amendment or article, not of the bill"},"votes_for":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Votes For"},"votes_against":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Votes Against"},"abstentions":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Abstentions"},"total_voters":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Voters"},"acte_uid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Acte Uid","description":"The parcours step it falls under"}},"type":"object","required":["vote_id"],"title":"LoiAmendementScrutin"},"LoiAmendementsResponse":{"properties":{"dossier_uid":{"type":"string","title":"Dossier Uid"},"acte_uid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Acte Uid"},"total":{"type":"integer","title":"Total"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"items":{"items":{"$ref":"#/components/schemas/LoiAmendementScrutin"},"type":"array","title":"Items"}},"type":"object","required":["dossier_uid","total","limit","offset","items"],"title":"LoiAmendementsResponse","example":{"acte_uid":"L17-VD232363S52856","dossier_uid":"DLR5L17N51670","items":[{"abstentions":2,"acte_uid":"L17-VD232363S52856","result":"rejeté","scrutin_kind":"amendement","total_voters":200,"vote_id":"VTANR5L17V7433","vote_title":"l'amendement n° 31 de M. Bentz et les amendements identiques suivants de suppression de l'article premier (...).","voted_at":"2026-06-22T00:00:00Z","votes_against":110,"votes_for":88}],"limit":100,"offset":0,"total":21}},"LoiDetail":{"properties":{"dossier_uid":{"type":"string","title":"Dossier Uid"},"titre":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Titre"},"procedure_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Procedure Label"},"initiateur":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Initiateur"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"Derived by ADR-035 §5's rules"},"status_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status Label","description":"The AN's verbatim wording of the deciding acte"},"current_stage":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Stage"},"parcours_start":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Parcours Start"},"parcours_end":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Parcours End"},"an_dossier_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"An Dossier Url"},"scrutin_coverage_start":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Scrutin Coverage Start","description":"First date the AN linked scrutins to their bill"},"parcours_predates_coverage":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Parcours Predates Coverage","description":"True when earlier stages cannot carry scrutins - an upstream gap"},"first_scrutin_at":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"First Scrutin At"},"amendement_count":{"type":"integer","title":"Amendement Count","default":0},"article_count":{"type":"integer","title":"Article Count","default":0},"parcours":{"items":{"$ref":"#/components/schemas/LoiActe"},"type":"array","title":"Parcours","default":[]},"unattached_scrutins":{"items":{"$ref":"#/components/schemas/LoiScrutin"},"type":"array","title":"Unattached Scrutins","description":"Headline scrutins no AN séance acte precedes; normally empty","default":[]},"unattached_collapsed_count":{"type":"integer","title":"Unattached Collapsed Count","description":"Amendment and article scrutins no AN séance acte precedes; normally 0","default":0}},"type":"object","required":["dossier_uid"],"title":"LoiDetail","example":{"amendement_count":358,"an_dossier_url":"https://www.assemblee-nationale.fr/dyn/17/dossiers/DLR5L17N51670","article_count":19,"current_stage":"PROM","dossier_uid":"DLR5L17N51670","first_scrutin_at":"2026-06-22","initiateur":"PA605694","parcours":[{"acte_uid":"L17-VD224407DI","amendement_count":0,"article_count":0,"code_acte":"AN1-DEPOT","date_acte":"2025-03-11","depth":1,"libelle":"1er dépôt d'une initiative.","ordinal":1,"parent_uid":"L17-AN1-51670","scrutins":[]},{"acte_uid":"L17-VD232853DEC","amendement_count":0,"article_count":0,"code_acte":"ANNLEC-DEBATS-DEC","date_acte":"2026-06-30","depth":2,"libelle":"Décision","ordinal":133,"parent_uid":"L17-ANNLEC-DEBATS-51670","scrutins":[{"abstentions":35,"result":"adopté","scrutin_kind":"ensemble","summary_plain":"La proposition de loi sur l'aide à mourir est adoptée en nouvelle lecture.","theme":"Santé","total_voters":562,"vote_id":"VTANR5L17V7894","vote_title":"l'ensemble de la proposition de loi relative au droit à l'aide à mourir (nouvelle lecture).","voted_at":"2026-06-30T00:00:00Z","votes_against":232,"votes_for":295}],"statut_label":"adoptée"}],"parcours_end":"2026-08-18","parcours_predates_coverage":true,"parcours_start":"2025-03-11","procedure_label":"Proposition de loi ordinaire","scrutin_coverage_start":"2026-03-26","status":"promulguee","status_label":"adoptée","titre":"Fin de vie","unattached_collapsed_count":0,"unattached_scrutins":[]}},"LoiListItem":{"properties":{"dossier_uid":{"type":"string","title":"Dossier Uid"},"titre":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Titre"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"status_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status Label"},"current_stage":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Stage"},"parcours_start":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Parcours Start"},"parcours_end":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Parcours End"},"last_scrutin_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Scrutin At"},"scrutin_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Scrutin Count"},"headline_scrutin_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Headline Scrutin Count"},"theme":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Theme"},"lois_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lois Url"}},"type":"object","required":["dossier_uid"],"title":"LoiListItem"},"LoiListResponse":{"properties":{"total":{"type":"integer","title":"Total"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"items":{"items":{"$ref":"#/components/schemas/LoiListItem"},"type":"array","title":"Items"}},"type":"object","required":["total","limit","offset","items"],"title":"LoiListResponse","example":{"items":[{"current_stage":"PROM","dossier_uid":"DLR5L17N51670","headline_scrutin_count":3,"last_scrutin_at":"2026-07-15T00:00:00Z","lois_url":"https://mon-elu.vercel.app/lois/DLR5L17N51670","parcours_end":"2026-08-18","parcours_start":"2025-03-11","scrutin_count":380,"status":"promulguee","status_label":"adoptée","theme":"Santé","titre":"Fin de vie"}],"limit":50,"offset":0,"total":70}},"LoiScrutin":{"properties":{"vote_id":{"type":"string","title":"Vote Id"},"voted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Voted At"},"vote_title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vote Title"},"scrutin_kind":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scrutin Kind","description":"ensemble | motion | autre (amendment kinds are never listed here)"},"result":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Result"},"votes_for":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Votes For"},"votes_against":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Votes Against"},"abstentions":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Abstentions"},"total_voters":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Voters"},"summary_plain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary Plain"},"theme":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Theme"}},"type":"object","required":["vote_id"],"title":"LoiScrutin","description":"A headline scrutin - a vote on the whole text, a motion, or another non-amendment vote."},"NotificationPreferencesUpdate":{"properties":{"followed_deputy_votes":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Followed Deputy Votes"},"followed_theme_votes":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Followed Theme Votes"},"weekly_digest":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Weekly Digest"}},"additionalProperties":false,"type":"object","title":"NotificationPreferencesUpdate","description":"Omit a flag, or send `null`, to leave it unchanged.","example":{"followed_deputy_votes":true}},"ProfileFields":{"properties":{"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"preferred_language":{"anyOf":[{"type":"string","enum":["fr","en"]},{"type":"null"}],"title":"Preferred Language"},"department_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department Code"},"circonscription":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Circonscription"}},"additionalProperties":false,"type":"object","title":"ProfileFields","description":"Editable profile fields. Omit a field to leave it unchanged; send `null`\nto clear it (except `preferred_language`, which always has a value).","example":{"circonscription":"1","department_code":"83","display_name":"Camille","preferred_language":"fr"}},"QuizAnswer":{"properties":{"vote_id":{"type":"string","title":"Vote Id"},"position":{"type":"string","enum":["pour","contre","abstention"],"title":"Position"}},"type":"object","required":["vote_id","position"],"title":"QuizAnswer"},"QuizDepartmentResult":{"properties":{"code":{"type":"string","title":"Code"},"name":{"type":"string","title":"Name"},"deputies":{"items":{"$ref":"#/components/schemas/QuizDeputyMatch"},"type":"array","title":"Deputies"}},"type":"object","required":["code","name","deputies"],"title":"QuizDepartmentResult"},"QuizDeputyMatch":{"properties":{"deputy_id":{"type":"string","title":"Deputy Id"},"full_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Full Name"},"party":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Party"},"party_short":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Party Short"},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"},"photo_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Photo Url"},"agreement_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Agreement Pct"},"matches":{"type":"integer","title":"Matches"},"compared":{"type":"integer","title":"Compared"},"detail":{"anyOf":[{"items":{"$ref":"#/components/schemas/QuizVoteDetail"},"type":"array"},{"type":"null"}],"title":"Detail"}},"type":"object","required":["deputy_id","matches","compared"],"title":"QuizDeputyMatch"},"QuizGroupAlignment":{"properties":{"party":{"type":"string","title":"Party"},"party_short":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Party Short"},"agreement_pct":{"type":"number","title":"Agreement Pct"},"matches":{"type":"integer","title":"Matches"},"compared":{"type":"integer","title":"Compared"},"deputy_count":{"type":"integer","title":"Deputy Count"}},"type":"object","required":["party","agreement_pct","matches","compared","deputy_count"],"title":"QuizGroupAlignment"},"QuizMatchRequest":{"properties":{"answers":{"items":{"$ref":"#/components/schemas/QuizAnswer"},"type":"array","maxItems":10,"minItems":3,"title":"Answers","description":"Réponses au quiz — au moins 3 pour un résultat significatif"},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department","description":"Code de département pour la comparaison « vos députés » (optionnel)"},"focus_deputy_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Focus Deputy Id","description":"Identifiant d'un député à mettre en avant dans la réponse (entrée quiz personnalisée depuis sa page profil, MON-183)"}},"type":"object","required":["answers"],"title":"QuizMatchRequest"},"QuizMatchResponse":{"properties":{"version":{"type":"string","title":"Version"},"answered":{"type":"integer","title":"Answered"},"eligible_deputies":{"type":"integer","title":"Eligible Deputies"},"top_matches":{"items":{"$ref":"#/components/schemas/QuizDeputyMatch"},"type":"array","title":"Top Matches"},"opposite":{"anyOf":[{"$ref":"#/components/schemas/QuizDeputyMatch"},{"type":"null"}]},"groups":{"items":{"$ref":"#/components/schemas/QuizGroupAlignment"},"type":"array","title":"Groups"},"my_department":{"anyOf":[{"$ref":"#/components/schemas/QuizDepartmentResult"},{"type":"null"}]},"focus":{"anyOf":[{"$ref":"#/components/schemas/QuizDeputyMatch"},{"type":"null"}]},"themes":{"anyOf":[{"$ref":"#/components/schemas/QuizThemeSummary"},{"type":"null"}]}},"type":"object","required":["version","answered","eligible_deputies","top_matches","groups"],"title":"QuizMatchResponse"},"QuizQuestion":{"properties":{"vote_id":{"type":"string","title":"Vote Id"},"theme":{"type":"string","title":"Theme"},"question":{"type":"string","title":"Question"},"context":{"type":"string","title":"Context"},"votes_for":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Votes For"},"votes_against":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Votes Against"},"abstentions":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Abstentions"},"result":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Result"},"vote_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vote Date"}},"type":"object","required":["vote_id","theme","question","context"],"title":"QuizQuestion"},"QuizQuestionsResponse":{"properties":{"version":{"type":"string","title":"Version"},"count":{"type":"integer","title":"Count"},"questions":{"items":{"$ref":"#/components/schemas/QuizQuestion"},"type":"array","title":"Questions"}},"type":"object","required":["version","count","questions"],"title":"QuizQuestionsResponse"},"QuizShareRequest":{"properties":{"answers":{"items":{"$ref":"#/components/schemas/QuizAnswer"},"type":"array","maxItems":10,"minItems":3,"title":"Answers","description":"Réponses au quiz — au moins 3 pour un résultat significatif"},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department","description":"Code de département pour la comparaison « vos députés » (optionnel)"},"focus_deputy_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Focus Deputy Id","description":"Identifiant d'un député à mettre en avant dans la réponse (entrée quiz personnalisée depuis sa page profil, MON-183)"},"include_answers":{"type":"boolean","title":"Include Answers","description":"Inclut les réponses dans le snapshot pour permettre une comparaison amicale (ADR-028) — opt-in, refusé par défaut.","default":false}},"type":"object","required":["answers"],"title":"QuizShareRequest"},"QuizShareResponse":{"properties":{"id":{"type":"string","title":"Id"},"result":{"$ref":"#/components/schemas/QuizShareResult"},"shared_at":{"type":"string","title":"Shared At"},"share_url":{"type":"string","title":"Share Url"}},"type":"object","required":["id","result","shared_at","share_url"],"title":"QuizShareResponse"},"QuizShareResult":{"properties":{"version":{"type":"string","title":"Version"},"answered":{"type":"integer","title":"Answered"},"eligible_deputies":{"type":"integer","title":"Eligible Deputies"},"top_matches":{"items":{"$ref":"#/components/schemas/QuizDeputyMatch"},"type":"array","title":"Top Matches"},"opposite":{"anyOf":[{"$ref":"#/components/schemas/QuizDeputyMatch"},{"type":"null"}]},"groups":{"items":{"$ref":"#/components/schemas/QuizGroupAlignment"},"type":"array","title":"Groups"},"my_department":{"anyOf":[{"$ref":"#/components/schemas/QuizDepartmentResult"},{"type":"null"}]},"focus":{"anyOf":[{"$ref":"#/components/schemas/QuizDeputyMatch"},{"type":"null"}]},"themes":{"anyOf":[{"$ref":"#/components/schemas/QuizThemeSummary"},{"type":"null"}]},"answers":{"anyOf":[{"items":{"$ref":"#/components/schemas/QuizAnswer"},"type":"array"},{"type":"null"}],"title":"Answers"}},"type":"object","required":["version","answered","eligible_deputies","top_matches","groups"],"title":"QuizShareResult"},"QuizThemeSummary":{"properties":{"supported":{"items":{"type":"string"},"type":"array","title":"Supported"},"opposed":{"items":{"type":"string"},"type":"array","title":"Opposed"}},"type":"object","required":["supported","opposed"],"title":"QuizThemeSummary","description":"Which themes the taker voted for and against (MON-203).\n\nDerived from the answers and the curated question set only — no DB, no\nranking. The curated set carries exactly one question per theme, so there\nis nothing to aggregate and no \"dominant\" theme to compute: these are\nliterally the themes the taker answered pour / contre, in question-set\norder. Abstentions appear in neither list."},"QuizVoteDetail":{"properties":{"vote_id":{"type":"string","title":"Vote Id"},"deputy_position":{"anyOf":[{"type":"string","enum":["pour","contre","abstention"]},{"type":"null"}],"title":"Deputy Position"}},"type":"object","required":["vote_id"],"title":"QuizVoteDetail"},"QuizWeeklyQuestion":{"properties":{"vote_id":{"type":"string","title":"Vote Id"},"question":{"type":"string","title":"Question"},"vote_title":{"type":"string","title":"Vote Title"},"votes_for":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Votes For"},"votes_against":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Votes Against"},"abstentions":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Abstentions"},"result":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Result"},"vote_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vote Date"}},"type":"object","required":["vote_id","question","vote_title"],"title":"QuizWeeklyQuestion"},"SearchRequest":{"properties":{"question":{"type":"string","maxLength":500,"minLength":5,"title":"Question","description":"Question en français sur l'activité parlementaire"},"deputy_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deputy Id","description":"Filtrer par député (optionnel)"},"chunk_type":{"anyOf":[{"type":"string","enum":["vote","deputy"]},{"type":"null"}],"title":"Chunk Type","description":"'vote' ou 'deputy' (optionnel)"}},"type":"object","required":["question"],"title":"SearchRequest"},"SearchResponse":{"properties":{"answer":{"type":"string","title":"Answer"},"question":{"type":"string","title":"Question"},"chunks_retrieved":{"type":"integer","title":"Chunks Retrieved"},"sources":{"items":{"$ref":"#/components/schemas/SourceItem"},"type":"array","title":"Sources"},"query_type":{"type":"string","title":"Query Type","default":"rag"},"confidence":{"type":"string","title":"Confidence","default":"medium"},"data_source":{"type":"string","title":"Data Source","default":"RAG"},"caveat":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Caveat"},"suggested_action":{"anyOf":[{"type":"string","const":"verify"},{"type":"null"}],"title":"Suggested Action"}},"type":"object","required":["answer","question","chunks_retrieved","sources"],"title":"SearchResponse"},"ShareRequest":{"properties":{"question":{"type":"string","maxLength":500,"minLength":1,"title":"Question"},"answer":{"type":"string","maxLength":8000,"minLength":1,"title":"Answer"},"sources":{"items":{"$ref":"#/components/schemas/ShareSourceItem"},"type":"array","maxItems":20,"title":"Sources"},"confidence":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Confidence"},"data_source":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Data Source"},"caveat":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Caveat"}},"type":"object","required":["question","answer"],"title":"ShareRequest"},"ShareResponse":{"properties":{"id":{"type":"string","title":"Id"},"question":{"type":"string","title":"Question"},"answer":{"type":"string","title":"Answer"},"sources":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Sources"},"confidence":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Confidence"},"data_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Data Source"},"caveat":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Caveat"},"shared_at":{"type":"string","title":"Shared At"},"share_url":{"type":"string","title":"Share Url"}},"type":"object","required":["id","question","answer","sources","shared_at","share_url"],"title":"ShareResponse"},"ShareSourceItem":{"properties":{"content":{"type":"string","maxLength":4000,"title":"Content"},"metadata":{"additionalProperties":true,"type":"object","title":"Metadata"},"similarity":{"anyOf":[{"type":"number","maximum":1.0,"minimum":0.0},{"type":"null"}],"title":"Similarity"}},"type":"object","required":["content"],"title":"ShareSourceItem"},"SourceItem":{"properties":{"content":{"type":"string","title":"Content"},"metadata":{"additionalProperties":true,"type":"object","title":"Metadata"},"similarity":{"type":"number","title":"Similarity"}},"type":"object","required":["content","metadata","similarity"],"title":"SourceItem"},"ThemeDetail":{"properties":{"slug":{"type":"string","title":"Slug"},"name":{"type":"string","title":"Name"},"vote_count":{"type":"integer","title":"Vote Count"},"adoption_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Adoption Rate","description":"Share of votes with result='adopté' among votes with a known result, 0-1; None when no vote in the theme has a result yet"},"most_divided_vote":{"anyOf":[{"$ref":"#/components/schemas/ThemeMostDividedVote"},{"type":"null"}],"description":"Vote with the smallest absolute pour/contre margin in the theme"},"party_positions":{"items":{"$ref":"#/components/schemas/ThemePartyPosition"},"type":"array","title":"Party Positions"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"votes":{"items":{"$ref":"#/components/schemas/ThemeVoteItem"},"type":"array","title":"Votes"}},"type":"object","required":["slug","name","vote_count","party_positions","limit","offset","votes"],"title":"ThemeDetail"},"ThemeMostDividedVote":{"properties":{"vote_id":{"type":"string","title":"Vote Id"},"voted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Voted At"},"vote_title":{"type":"string","title":"Vote Title"},"votes_for":{"type":"integer","title":"Votes For"},"votes_against":{"type":"integer","title":"Votes Against"},"result":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Result","description":"`adopté` or `rejeté`, as recorded (#527)"}},"type":"object","required":["vote_id","vote_title","votes_for","votes_against"],"title":"ThemeMostDividedVote"},"ThemePartyPosition":{"properties":{"party_short":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Party Short"},"pour":{"type":"integer","title":"Pour"},"contre":{"type":"integer","title":"Contre"},"abstention":{"type":"integer","title":"Abstention"},"expressed":{"type":"integer","title":"Expressed","description":"pour + contre + abstention"},"pour_rate":{"type":"number","title":"Pour Rate","description":"pour / expressed, 0-1"}},"type":"object","required":["pour","contre","abstention","expressed","pour_rate"],"title":"ThemePartyPosition"},"ThemeVoteItem":{"properties":{"vote_id":{"type":"string","title":"Vote Id"},"voted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Voted At"},"vote_title":{"type":"string","title":"Vote Title"},"result":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Result"},"summary_plain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary Plain"}},"type":"object","required":["vote_id","vote_title"],"title":"ThemeVoteItem"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VerifyRequest":{"properties":{"claim":{"type":"string","maxLength":500,"minLength":10,"title":"Claim","description":"Affirmation à vérifier, en français (ex: 'le député X a voté contre Y')"}},"type":"object","required":["claim"],"title":"VerifyRequest"},"VerifyResponse":{"properties":{"id":{"type":"string","title":"Id"},"claim":{"type":"string","title":"Claim"},"verdict":{"type":"string","enum":["vrai","faux","trompeur","inverifiable"],"title":"Verdict"},"explanation":{"type":"string","title":"Explanation"},"deputy":{"anyOf":[{"$ref":"#/components/schemas/DeputyRef"},{"type":"null"}]},"citations":{"items":{"$ref":"#/components/schemas/CitationItem"},"type":"array","title":"Citations"},"confidence":{"type":"string","enum":["ÉLEVÉ","MOYEN","FAIBLE"],"title":"Confidence"},"data_horizon":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Data Horizon"},"verified_at":{"type":"string","title":"Verified At"},"share_url":{"type":"string","title":"Share Url"}},"type":"object","required":["id","claim","verdict","explanation","citations","confidence","verified_at","share_url"],"title":"VerifyResponse"},"VoteDetail":{"properties":{"vote_id":{"type":"string","title":"Vote Id"},"voted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Voted At"},"vote_title":{"type":"string","title":"Vote Title"},"result":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Result"},"votes_for":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Votes For"},"votes_against":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Votes Against"},"abstentions":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Abstentions"},"total_voters":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Voters"},"summary_plain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary Plain"},"theme":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Theme"},"vote_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vote Type"},"dossier_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dossier Id"},"dossier":{"anyOf":[{"$ref":"#/components/schemas/VoteDossierRef"},{"type":"null"}],"description":"The scrutin's bill; null when dossier_id is absent"},"ingested_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ingested At"},"positions":{"items":{"$ref":"#/components/schemas/VotePosition"},"type":"array","title":"Positions","default":[]}},"type":"object","required":["vote_id","vote_title"],"title":"VoteDetail","description":"Full vote — used in GET /votes/{vote_id}.","example":{"abstentions":7,"dossier":{"dossier_uid":"DLR5L17N53980","lois_url":"https://mon-elu.vercel.app/lois/DLR5L17N53980","status":"promulguee","titre":"Réponses immédiates aux phénomènes troublant l'ordre public"},"dossier_id":"DLR5L17N53980","ingested_at":"2026-07-22T06:12:03Z","positions":[{"deputy_id":"PA720892","full_name":"Mathilde Panot","party_short":"LFI","position":"contre"},{"deputy_id":"PA793214","full_name":"Audrey Abadie-Amiel","party_short":"LIOT","position":"abstention"}],"result":"adopté","summary_plain":"Le texte issu de la commission mixte paritaire a été adopté.","theme":"Justice & Sécurité","total_voters":537,"vote_id":"VTANR5L17V8433","vote_title":"l'ensemble du projet de loi visant à offrir des réponses immédiates aux phénomènes troublant l'ordre public (texte de la commission mixte paritaire).","vote_type":"sps","voted_at":"2026-07-21T00:00:00Z","votes_against":179,"votes_for":351}},"VoteDossierRef":{"properties":{"dossier_uid":{"type":"string","title":"Dossier Uid"},"titre":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Titre"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"lois_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lois Url","description":"MonÉlu bill page; null when the dossier has no page (no row, or no scrutin)"}},"type":"object","required":["dossier_uid"],"title":"VoteDossierRef","description":"The bill a scrutin belongs to, embedded in GET /votes/{vote_id} (#369)."},"VoteListResponse":{"properties":{"total":{"type":"integer","title":"Total"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"items":{"items":{"$ref":"#/components/schemas/VoteSummary"},"type":"array","title":"Items"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Opaque keyset cursor — pass as ?before= to fetch the next page; null when there are no more rows."}},"type":"object","required":["total","limit","offset","items"],"title":"VoteListResponse","example":{"items":[{"abstentions":7,"result":"adopté","summary_plain":"Le texte issu de la commission mixte paritaire a été adopté.","theme":"Justice & Sécurité","total_voters":537,"vote_id":"VTANR5L17V8433","vote_title":"l'ensemble du projet de loi visant à offrir des réponses immédiates aux phénomènes troublant l'ordre public (texte de la commission mixte paritaire).","voted_at":"2026-07-21T00:00:00Z","votes_against":179,"votes_for":351}],"limit":50,"next_cursor":"MjAyNi0wNy0yMVQwMDowMDowMCswMDowMHxWVEFOUjVMMTdWODQzMw==","offset":0,"total":5561}},"VotePosition":{"properties":{"deputy_id":{"type":"string","title":"Deputy Id"},"full_name":{"type":"string","title":"Full Name"},"party_short":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Party Short"},"position":{"type":"string","title":"Position"}},"type":"object","required":["deputy_id","full_name","position"],"title":"VotePosition","description":"A single deputy's position on a vote."},"VoteSummary":{"properties":{"vote_id":{"type":"string","title":"Vote Id"},"voted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Voted At"},"vote_title":{"type":"string","title":"Vote Title"},"result":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Result"},"votes_for":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Votes For"},"votes_against":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Votes Against"},"abstentions":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Abstentions"},"total_voters":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Voters"},"summary_plain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary Plain"},"theme":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Theme"}},"type":"object","required":["vote_id","vote_title"],"title":"VoteSummary","description":"Lightweight vote — used in list responses.","example":{"abstentions":7,"result":"adopté","summary_plain":"Le texte issu de la commission mixte paritaire a été adopté.","theme":"Justice & Sécurité","total_voters":537,"vote_id":"VTANR5L17V8433","vote_title":"l'ensemble du projet de loi visant à offrir des réponses immédiates aux phénomènes troublant l'ordre public (texte de la commission mixte paritaire).","voted_at":"2026-07-21T00:00:00Z","votes_against":179,"votes_for":351}}},"securitySchemes":{"HTTPBearer":{"type":"http","description":"Supabase access token, forwarded by the Next.js server (ADR-040).","scheme":"bearer"}}},"tags":[{"name":"Deputies","description":"Deputy profiles, voting records and computed scorecards. Start here to answer anything about one named person."},{"name":"Votes","description":"Scrutins (recorded votes): outcome, tallies, plain-French summary, and the per-deputy breakdown. Start here to answer anything about one bill or one vote."},{"name":"Lois","description":"Bills (dossiers législatifs): where one stands, from deposit to promulgation, with its headline scrutins attached. Only bills with at least one scrutin have a page; the AN linked scrutins to bills only from March 2026, so older stages carry no vote."},{"name":"Groups","description":"Parliamentary groups: roster, cohesion, and the votes that split the group hardest. Group membership is the deputy's current group only."},{"name":"Departments","description":"Deputies by département, with local aggregates and split votes."},{"name":"Themes","description":"Votes grouped by policy theme, with per-group positioning. Themes are assigned by MonÉlu, not by the Assemblée."},{"name":"Agenda","description":"Upcoming séance publique sittings. Forward-looking and volatile - items are cancelled and rescheduled routinely."},{"name":"Search","description":"Natural-language question answering over the corpus (RAG). Costs an LLM call; prefer the structured endpoints when the question maps onto one."},{"name":"Verify","description":"Fact-check a claim about how a deputy voted, with citations."},{"name":"Quiz","description":"The \"which deputy votes like you\" questionnaire and its stateless matching. Question content is curated, not derived from the database."},{"name":"API Keys","description":"Usage accounting for an issued API key."},{"name":"Feedback","description":"User-submitted feedback sinks. Write-only."},{"name":"Account","description":"The signed-in caller's own account (ADR-040): profile, followed deputies and themes, bookmarked votes, notification preferences (stored only - nothing is ever sent), data export and deletion. Requires a Supabase access token as a bearer; not needed for any public data, which stays anonymous."},{"name":"App","description":"Launch configuration for the MonÉlu mobile app (ADR-041): minimum supported version, remote feature switches, data horizon and caveat texts. Public and anonymous."},{"name":"Health","description":"Service status and data freshness. Check this before trusting a stale answer."}]}