Skip to main content

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).

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.gov CSV 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 unpaddedHB0001 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_bills lists with the same LegislationID (e.g. HB9, ID 98631, in 20261 and 20271).
  • get_bill(legislation_id) returns top-level SessionCode/SessionID as None. The Sessions list 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 LegislationID identifies 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, 20271 returned 443 bills, every one sharing its ID with 20261. 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 year
  • get_default_session() → the current/active session

Legislation (/Legislation/api/)

  • get_session_bills(session_code) → lightweight bill list for a session
  • get_bill(legislation_id) → full bill detail with all patrons
  • get_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 list
  • get_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 bill
  • get_event_types() → ~3,900 event type references
  • get_actor_types() → 5 actor types (House, Senate, Committee, etc.)

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:

  • DocketCategoriesDocketItems → bills with summaries and patrons
  • CommitteeMember roster with roles (Chair, etc.)
  • Staff assignments
  • Schedules (linked back to the Schedule service for when/where)
  • PDF/JSON file downloads

Calendar detail includes:

  • CalendarCategoriesAgendas → bills with vote tallies
  • AgendaItemsVoteMember with 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 ScheduleTime is 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.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for va-lis-client 0.1.0
File Size Uploaded
va_lis_client-0.1.0.tar.gz 37.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for va-lis-client 0.1.0
File Interpreter ABI Platform
va_lis_client-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size:72.2 kB

Release files / va_lis_client-0.1.0.tar.gz

Download URL va_lis_client-0.1.0.tar.gz
Size 37.3 kB
Tags Source
SHA-256 checksum
How to use checksums
d179e7c9087cdc3902d433a313329b54b426076723305f07f19127f26ec92604
BLAKE2b-256 checksum
How to use checksums
a9284c632f2e471f81e03d9db8a762854e57715f1539cfb589a18702ad98a09a
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

Release files / va_lis_client-0.1.0-py3-none-any.whl

Download URL va_lis_client-0.1.0-py3-none-any.whl
Size 34.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ceb8f9815dcf60e16dfdf7ab04fdac9048849199efa188b271fc127b7e84fe62
BLAKE2b-256 checksum
How to use checksums
81580ab2dfbb2b62b9006844b898099800134cb60ab0b40a9cdd6ddc2e9a0292
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

Release history Release notifications | RSS feed

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page