va-lis-client
Python client for the Virginia Legislative Information System (LIS) REST API.
Built from live API responses — the official OpenAPI specs are unreliable (response envelope keys, field casing, and parameter behavior all differ from the docs).
- Developer portal: https://lis.virginia.gov/developers
- API key registration: https://lis.virginia.gov/apiregistration
- Help desk: lis@dlas.virginia.gov
Installation
pip install va-lis-client
With Redis rate limiting support:
pip install "va-lis-client[rate-limiting]"
Quick start
import os
from va_lis_client import LISClient
# Pass the key directly, or set the LIS_API_KEY environment variable
client = LISClient(api_key="your-api-key-here")
# or: os.environ["LIS_API_KEY"] = "your-key"; client = LISClient()
# The GA's current working session. During the interim this is the
# *upcoming* session, not the one that just ended — see
# "Bill identity & carry-over" below.
session = client.get_default_session()
# All bills in that session
bills = client.get_session_bills(session_code=int(session.SessionCode))
# Full detail for a bill
bill = client.get_bill(legislation_id=98525)
# Bill text (HTML) and summaries
texts = client.get_bill_text_detail(legislation_id=98525, session_code=20261)
summaries = client.get_bill_summaries(legislation_number="HB1", session_code=20261)
Authentication
Every request sends a WebAPIKey header with a partner GUID obtained at
https://lis.virginia.gov/apiregistration.
Set the key via environment variable:
export LIS_API_KEY=your-guid-here
Or pass it directly to the constructor:
client = LISClient(api_key="your-guid-here")
Important: Heartbeat endpoints accept any key — even garbage. Use
client.check_api_key() to actually validate a key via the
PartnerAuthentication service.
Data availability
- The session reference list reaches back to 1994.
- Bill data has been observed working for the 2024–2027 sessions (as of Aug 2026). Earlier guidance said only 2025–2026 was authorized via the API.
- Older session data is available as
legacylis.virginia.govCSV downloads. - No rate limits are documented anywhere.
Identifier cheat-sheet
Bills are uniquely identified by session + bill number, but the API uses several overlapping identifiers:
| Identifier | Example | Notes |
|---|---|---|
SessionCode |
20261 |
Year + sequence. 20261 = 2026 Regular, 20262 = Special I |
SessionID |
59 |
Surrogate PK. Either code or ID works as query param |
LegislationNumber |
"HB1" |
Must be unpadded — HB0001 returns 204 |
LegislationID |
98525 |
Surrogate PK for the logical bill — reused across carry-over (see below) |
DocumentCode |
"HB1ER" |
Bill number + version suffix |
LegislationTextID |
257719 |
PK for a specific text version |
CommitteeID |
14 |
Surrogate PK for a committee |
CommitteeNumber |
"H14" |
Chamber prefix + number, e.g. H14, S02 |
CalendarID |
21146 |
PK for a House floor calendar |
DocketID |
21114 |
PK for a Senate committee docket |
ScheduleID |
3626 |
PK for a scheduled meeting |
LegislationEventID |
1561089 |
PK for a bill history event |
Bill identity & carry-over
Virginia's even-year sessions may carry unfinished bills over into the
following odd-year session — never the reverse, never twice, and never across
a two-year General Assembly term. The API models this by reusing the
LegislationID (confirmed against the live API, 2026-08-12):
- A carried-over bill appears in both sessions'
get_session_billslists with the sameLegislationID(e.g. HB9, ID98631, in20261and20271). get_bill(legislation_id)returns top-levelSessionCode/SessionIDasNone. TheSessionslist carries one cross-ref per session the bill appears in — both the even and odd session for a carried-over bill — and is the explicit lineage record.- So
LegislationIDidentifies the logical bill within a GA term: at most two session appearances (even, then odd if continued). Session-scoped identity is(SessionCode, LegislationNumber)or(SessionCode, LegislationID). - An odd-year bill list starts as pure carry-over — as of Aug 2026,
20271returned 443 bills, every one sharing its ID with20261. Continued bills must be acted on by mid-November of the even year or they die, so expect that set to shrink before the odd session convenes.
Sync warning: if you mirror bills into a database, don't put a global
unique constraint on LegislationID — the first odd-year sync will violate it
on the first carried-over bill. Scope uniqueness to
(session, LegislationID).
Default session: get_default_session() tracks the GA's working
session, not the last one convened. Once a session wraps up (sine die, veto
session, enactments effective July 1), the default advances to the upcoming
session during the interim — by Aug 2026 it was already 20271, which is
where continued bills and (from mid-November) new prefiles accumulate. For
retrospective work on a just-ended session, pass its explicit session code.
Response envelope gotchas
The docs say ListItems everywhere. Reality:
| Endpoint | Docs say | Actual key |
|---|---|---|
| Sessions | ListItems |
Sessions |
| Legislation list | ListItems |
Legislations |
| Legislation detail | ListItems |
Legislations |
| Legislation statuses | ListItems |
References |
| Text list | ListItems |
LegislationTextList |
| Text detail | ListItems |
TextsList |
| Summaries | ListItems |
LegislationSummaries |
| Version refs | ListItems |
LegislationVersionList |
| Partner check | flat | PartnerList |
| Calendars | ListItems |
Calendars |
| Dockets | ListItems |
Dockets |
| Schedules | ListItems |
Schedules |
| Committees | ListItems |
Committees |
| Legislation events | ListItems |
LegislationEvents |
| Schedule types | ListItems |
ScheduleTypes |
| Meeting rooms | ListItems |
MeetingRooms |
API services
Currently implemented
Session (/Session/api/)
get_sessions(year)→ list of sessions for a yearget_default_session()→ the current/active session
Legislation (/Legislation/api/)
get_session_bills(session_code)→ lightweight bill list for a sessionget_bill(legislation_id)→ full bill detail with all patronsget_legislation_statuses()→ all 52 status reference entries
LegislationText (/LegislationText/api/)
get_legislation_versions()→ 13 version type references (Introduced, Enrolled, etc.)get_bill_texts(legislation_number, session_code)→ text version listget_bill_text_detail(legislation_id, session_code)→ full HTML bill text
LegislationSummary (/LegislationSummary/api/)
get_bill_summaries(legislation_number, session_code)→ bill summaries (HTML)
LegislationEvent (/LegislationEvent/api/)
get_bill_events(legislation_id)→ chronological action history for a billget_event_types()→ 3,912 event type referencesget_actor_types()→ 5 actor types (House, Senate, Committee, etc.)
Event join keys (verified 2026-08-13): an event's Status carries the
internal name from the 52-status vocabulary (a closed vocabulary, not
free text), and LegislationStatusID comes back null — join statuses on
the name. The event-type reference rows come back with
LegislationEventTypeID null — join event types on EventCode. 120 of
the 3,912 types are the committee continuance family (codes ending
40/41/42, e.g. H1940 = Continued to next session in Transportation);
floor and conference continuances are covered by statuses 46–48 instead.
EventDate is the true action date — a committee continuance is dated on
the committee vote, not at crossover.
Modeled but not yet wired to client methods
Schedule (/Schedule/api/)
The master meeting calendar. All committee hearings, caucuses, floor sessions.
| Endpoint | Description |
|---|---|
getschedulelistasync |
List meetings by date range, committee, type, room |
previewvcalfileasync |
Generate vCal file for calendar integration |
getmeetingroomsreferenceasync |
Room reference (by chamber) |
getscheduletypesreferenceasync |
Schedule type reference |
Key fields: ScheduleDate, ScheduleTime (often free-text like "15 minutes
after the Senate adjourns"), RoomDescription, OwnerName (committee),
IsCancelled, ScheduleType (Committee/Chamber/Conference/Caucus/Docket/Other).
Data quality warning: ScheduleTime is frequently free-text, not a
parseable time. The API faithfully reflects whatever the LIS clerks entered.
Calendar (/Calendar/api/)
Floor calendars (both chambers) and committee dockets (Senate only).
| Endpoint | Description |
|---|---|
getcalendarlistasync |
List calendars by session + chamber |
getcalendarsbyidasync |
Full calendar with categories, agendas, votes |
getdocketlistasync |
List dockets by committee (Senate only) |
getdocketlistbycommitteenumberasync |
Dockets by committee number (Senate only) |
getdocketsbyidasync |
Full docket with agenda items, bills, patrons |
getcalendaractionsreferenceasync |
Calendar action reference |
getcalendarcategorytypesreferenceasync |
Category type reference |
getcalendartypesreferenceasync |
Calendar type reference |
Important: Dockets are Senate-only. The House uses Calendars. Requesting
a docket with chamberCode=H returns HTTP 400 with the message
"Dockets can only be for ChamberCode = S".
Docket detail includes:
DocketCategories→DocketItems→ bills with summaries and patronsCommitteeMemberroster with roles (Chair, etc.)StaffassignmentsSchedules(linked back to the Schedule service for when/where)- PDF/JSON file downloads
Calendar detail includes:
CalendarCategories→Agendas→ bills with vote talliesAgendaItems→VoteMemberwith per-member vote responses (Y/N)- Order of business (Call to Order, Invocation, etc.)
Committee (/Committee/api/)
| Endpoint | Description |
|---|---|
getcommitteesasync |
Full committee detail (by number or date) |
getcommitteelistasync |
Shallow list by session + chamber |
getcommitteebyidasync |
Single committee by ID + session |
Key fields: CommitteeID, CommitteeNumber (e.g. H14, S02), Name,
ChamberCode, Abbreviation, MeetingNote, ParentCommitteeID (for
subcommittees).
Not yet explored
These services exist in the portal but haven't been investigated:
- AdvancedLegislationSearch
- CommunicationFileGeneration
- Contact
- LegislationByMember
- LegislationCollections
- LegislationCommunications
- LegislationFileGeneration
- LegislationPatron
- LegislationSubject
- Member
- MemberVoteSearch
- MembersByCommittee
- MinutesBook
- Organization
- Person
- Personnel
How meetings/dockets work
Committee ──→ Docket (Senate) or Calendar (House) ──→ Schedule ──→ Room
│ │
│ has DocketItems/Agendas │ has ScheduleDate,
│ (which bills are up) │ ScheduleTime (free-text!),
│ │ RoomDescription,
└────────────────────────────────────────│ IsCancelled
└──→ VoteRoom reference
- Schedule is the "when and where" — but
ScheduleTimeis often free-text like "15 minutes after adjournment" - Docket/Calendar is the "what" — which bills are on the agenda
- Committee is the "who" — membership, chair, staff
How a bill becomes law (Virginia)
A bill's lifecycle through the General Assembly, mapped to LIS status IDs. Virginia has a bicameral legislature (House of Delegates + Senate). A bill must pass both chambers in identical form before going to the Governor.
The happy path
ORIGINATING CHAMBER
───────────────────
┌─ Prefiled (Nov–Jan) ──→ Introduced (1)
│ │
│ ▼
│ In Committee (2)
│ ┌────┴────┐
│ │ │
│ In Subcommittee Left In Committee (20)
│ (19) = dead, session ends
│ │
│ ▼
│ Reported Out (3 or 52)
│ Committee votes to send
│ bill to the full chamber
│ │
│ ▼
│ Floor vote in originating chamber
│ Passed House (4) or Passed Senate (5)
│
│ CROSSING OVER
│ ─────────────
│ Bill goes to the other chamber
│ and repeats: Committee → Floor
│ │
│ ▼
│ Passed Both Chambers (6, 39, 40)
│ = "Enrolled" version created (ER)
│ │
│ ▼
│
│ GOVERNOR
│ ────────
│ Communicated (13) → With Governor (7)
│ │
│ ▼
│ Awaiting Governor's Action (38)
│ ┌────┼────────────┐
│ │ │ │
│ ▼ ▼ ▼
│ Approved Gov's Gov's Veto
│ (8) Recommend. (24, 26)
│ │ (23, 27) │
│ ▼ │ ▼
│ Enacted ▼ Override vote
│ (36) Chambers or bill dies
│ │ adopt? (33)
│ ▼ │
│ Acts of ▼
│ Assembly Back to
│ Chapter Enrolled
│ (25)
└────────────────────────────────────────────────
Key status groups
Filing & introduction:
| ID | DisplayName | Internal Name | What happened |
|---|---|---|---|
| 11 | Preview | Preview | Prefiled but session hasn't started yet |
| 1 | Introduced | Introduced | Formally introduced on the chamber floor |
Committee phase — where most bills die:
| ID | DisplayName | Internal Name | What happened |
|---|---|---|---|
| 2 | In Committee | In Committee | Referred to a standing committee |
| 37 | Committee Referral Pending | Committee Referral Pending | Awaiting committee assignment |
| 19 | In Subcommittee | In Subcommittee | Referred to a subcommittee |
| 20 | Left In Committee | Left In Committee | Bill was never voted on — effectively killed |
| 41 | In Committee | Continued From | Carried over from a previous session into committee |
| 3 | In House | Reported Out-House | Committee voted to advance (House side) |
| 52 | In Senate | Reported Out-Senate | Committee voted to advance (Senate side) |
| 30 | Incorporated | Incorporated | Merged into another bill |
Floor votes:
| ID | DisplayName | Internal Name | What happened |
|---|---|---|---|
| 4 | Passed House | Passed House | Full House approved |
| 5 | Passed Senate | Passed Senate | Full Senate approved |
| 12 | Engrossed | Engrossed | Passed one chamber, snapshot before crossover |
| 28 | Engrossed | Engrossed with Amendment | Same but with amendments applied |
| 29 | Engrossed | Reengrossed with Amendment | Re-amended after engrossment |
Conference — when the two chambers passed different versions:
| ID | DisplayName | Internal Name | What happened |
|---|---|---|---|
| 34 | Conference Requested | Conference Requested | One chamber asks for a conference committee |
| 16 | In Conference | In Conference | Conference committee is negotiating |
| 31 | In Conference | Conference Report Agreed | Conferees reached agreement |
| 32 | In Conference | Conference Report Rejected | Conferees' report was rejected |
| 39 | Passed | Conference Report Adopted | Both chambers accepted the conference version |
| 45 | Failed | Failed in Conference | Conferees couldn't agree — bill dies |
Passed both chambers:
| ID | DisplayName | Internal Name | What happened |
|---|---|---|---|
| 6 | Passed | Passed Both | Both chambers approved identical text (→ version 3, Enrolled) |
| 40 | Passed | Passed | Generic "passed" status |
| 35 | Enrolled | Enrolled-House | Final enrolled text prepared (House origin) |
| 44 | Enrolled | Enrolled-Senate | Final enrolled text prepared (Senate origin) |
Governor pipeline:
| ID | DisplayName | Internal Name | What happened |
|---|---|---|---|
| 13 | Communicated | Communicated | Bill formally transmitted to the Governor |
| 7 | With Governor | With Governor | Governor has received the bill |
| 14 | Awaiting Signature | Awaiting Signature | Waiting for physical signature |
| 38 | Awaiting Governor's Action | Awaiting Governor's Action | Governor is reviewing |
| 8 | Approved | Approved | Governor signed it (→ version 4, Chaptered) |
| 36 | Enacted | Enacted | Becomes law (→ version 4, Chaptered) |
| 25 | Acts of Assembly Chapter | Acts of Assembly Chapter | Assigned a chapter number |
| 23 | Governor's Recommendation | Governor's Recommendation | Sent back with proposed amendments (→ version 10) |
| 27 | Governor's Recommendation | Governor's Recommendation | Same (no version link) |
| 33 | Governor's Recommendation Adopted | Gov Recommendation Adopted | Chambers accepted the amendments |
| 24 | Governor's Veto | Governor's Veto | Governor vetoed (→ version 9, Veto Explanation) |
| 26 | Governor's Veto | Governor's Veto | Same (no version link) |
Terminal / carry-over:
| ID | DisplayName | Internal Name | What happened |
|---|---|---|---|
| 9 | Failed | Failed | Bill defeated on a floor vote |
| 10 | Continued | Continued To | Carried over to next session |
| 46 | Continued | Continued to House | Carried over, sitting in House |
| 47 | Continued | Continued to Senate | Carried over, sitting in Senate |
| 48 | Continued | Continued to Conference | Carried over, in conference |
Why there are 52 statuses for ~15 states
LIS uses separate status IDs to track how a bill entered a logical state.
For example, "In Conference" has four IDs distinguishing whether the conference
just started, the report was agreed, rejected, or continued. The Name
(internal) captures the sub-reason; the DisplayName (user-facing) collapses
them.
Version-linked statuses
Some statuses have a LegislationVersionID indicating which bill text version
corresponds to that state:
| Status | Version ID | Version Name |
|---|---|---|
| Passed Both (6) | 3 | Enrolled |
| Approved (8) | 4 | Chaptered |
| Enacted (36) | 4 | Chaptered |
| Engrossed with Amendment (28) | 2 | Engrossed |
| Reengrossed with Amendment (29) | 6 | Reengrossed |
| Governor's Recommendation (23) | 10 | Gov Recommendation |
| Governor's Veto (24) | 9 | Veto Explanation |
| Reenrolled-House (42) | 7 | Reenrolled |
| Reenrolled-Senate (43) | 7 | Reenrolled |
Rate limiting (optional)
If you're running many workers and want to share a global rate limit across
them, install the rate-limiting extra and use try_acquire_request_permit:
pip install "va-lis-client[rate-limiting]"
import redis
from va_lis_client.rate_limiter import try_acquire_request_permit
r = redis.from_url("redis://localhost:6379/0")
if try_acquire_request_permit(redis_client=r, rate_limit=100):
client.get_session_bills(session_code=20261)
else:
# back off
...
Or set REDIS_URL and LIS_RATE_LIMIT environment variables and call with
no arguments — the module picks them up automatically.
Windows / government CA roots
On Windows, Python's OpenSSL doesn't automatically load system certificates,
which can cause SSL errors when connecting to hosts with enterprise or
government CA roots. va-lis-client includes a SystemCertSSLAdapter that
loads the system certificate store automatically, so no manual cert bundling
is needed.
Package structure
src/va_lis_client/
├── __init__.py # re-exports LISClient, LISClientError
├── client.py # HTTP client — returns Pydantic models
├── exceptions.py # LISClientError
├── http.py # SystemCertSSLAdapter + shared requests session
├── rate_limiter.py # optional Redis-based rate limiter
└── models/
├── __init__.py # re-exports all models
├── common.py # Heartbeat, Partner
├── session.py # Session, SessionEvent
├── legislation.py # Patron, Legislation, LegislationStatus, ...
├── text.py # LegislationTextItem, LegislationTextDetail, ...
├── event.py # LegislationEvent, LegislationEventType, ActorType
├── committee.py # Committee, CommitteeMember, CommitteeAction
├── schedule.py # Schedule, ScheduleType, MeetingRoom
├── calendar.py # CalendarDetail, Agenda, VoteMember, ...
└── docket.py # DocketDetail, DocketItem, DocketCategory, ...
License
MIT
Release files for va-lis-client 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| va_lis_client-0.1.1.tar.gz | 38.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| va_lis_client-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size:74.8 kB
Release files / va_lis_client-0.1.1.tar.gz
| Download URL | va_lis_client-0.1.1.tar.gz |
|---|---|
| Size | 38.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9dc5f933c024f4d927aa40070e42ef0bf2a6eb25323ba469e815c1ba153f66a6
|
|
BLAKE2b-256 checksum How to use checksums |
f232f9f48704e0cdc03fab2fb366fa7dff6a2fd39c64a94b1ef23272369d489d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 13, 2026.
Transparency logRelease files / va_lis_client-0.1.1-py3-none-any.whl
| Download URL | va_lis_client-0.1.1-py3-none-any.whl |
|---|---|
| Size | 36.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a686eea35ceec907daf8fffe061af5529f92a5574932946b03eae5760469c18a
|
|
BLAKE2b-256 checksum How to use checksums |
542f98bb440178af663ebe3b864d2c581afeb566e14b8574c5eed4464c77118c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 13, 2026.
Transparency log