Python SDK for the Visor Public API
Project description
visor-python
visor-python is an unofficial community Python SDK for the Visor Public API — a vehicle inventory search platform covering new, used, and certified pre-owned listings from dealers across the US. It provides a thin, fully-typed wrapper around the REST API with sync and async clients, Pydantic response models, and auto-pagination helpers.
Disclaimer: This is an unofficial community SDK and is not affiliated with or endorsed by Visor (Currents Systems Inc.).
Pre-1.0 notice: This package is in initial development (
0.x). Minor version bumps may include breaking changes. Pin to a specific minor version in production and review the CHANGELOG before upgrading.
Install
pip install visor-python
Requires Python 3.10+ and no non-standard runtime dependencies beyond httpx and pydantic.
Quick start
from visor import VisorClient, ListingsFilter, iter_listings
with VisorClient() as client: # reads VISOR_API_KEY from env
# Search for used Toyota Tacomas in Texas under $40k
page = client.filter_listings(
ListingsFilter(
make=["Toyota"],
model=["Tacoma"],
inventory_type=["used"],
state=["TX"],
max_price=40_000,
)
)
for listing in page.data:
price = f"${listing.price:,}" if listing.price is not None else "N/A"
print(f"{listing.year} {listing.make} {listing.model} — {price}")
# Look up a specific VIN
vin = client.lookup_vin("4T1DAACKXTU765422", include=["price_history"])
msrp = f"${vin.build.combined_msrp:,}" if vin.build.combined_msrp is not None else "N/A"
print(msrp)
Geo filtering
Pass a postal_code and radius (miles) to search near a location:
page = client.filter_listings(
ListingsFilter(
make=["Honda"],
model=["CR-V"],
postal_code="90210",
radius=50,
)
)
radius requires exactly one of postal_code or latitude/longitude. Passing radius alone (or with neither) raises ValueError before any network call.
Paginating all results
iter_listings (sync) and paginate_listings (async) iterate every page automatically:
from visor import VisorClient, ListingsFilter, iter_listings
with VisorClient() as client:
for listing in iter_listings(
client,
ListingsFilter(make=["Ford"], state=["TX"]),
):
print(listing.vin, listing.price)
For dealers, use iter_dealers / paginate_dealers in the same way.
client.filter_listings(...) returns a single page (ListingsPage). Use the
iter_* / paginate_* helpers when you need all results.
Async
import asyncio
from visor import AsyncVisorClient, ListingsFilter, paginate_listings
async def main() -> None:
async with AsyncVisorClient() as client:
# Single page
page = await client.filter_listings(
ListingsFilter(make=["Toyota"], state=["TX"], max_price=40_000)
)
for listing in page.data:
price = f"${listing.price:,}" if listing.price is not None else "N/A"
print(listing.vin, price)
# All pages
async for listing in paginate_listings(
client,
ListingsFilter(make=["Toyota"], state=["TX"]),
):
print(listing.vin)
asyncio.run(main())
Configuration
API key
Pass your key explicitly or export VISOR_API_KEY before running:
client = VisorClient(api_key="vsr_live_...")
# or
# export VISOR_API_KEY=vsr_live_...
client = VisorClient()
You need your own Visor API key — see api.visor.vin for details. Use of the API is governed by Visor's API terms; you are responsible for complying with them.
Timeout
Default request timeout is 30 seconds. Override at construction time:
client = VisorClient(timeout=10.0)
Base URL (advanced)
base_url defaults to the production API. Override it for local testing or staging:
client = VisorClient(base_url="http://localhost:8080")
Key concepts
ListingsFilter is shared
ListingsFilter is accepted by both filter_listings() and dealer_inventory(). Build one filter object and reuse it across both methods.
fields is response projection, not filtering
ListingsFilter.fields controls which fields the API returns — it does not filter which listings match. Example:
filter = ListingsFilter(
make=["Toyota"],
fields=["vin", "price", "miles"],
)
id and vin are always returned by the API regardless of the fields projection.
Responses are Pydantic models
All responses — ListingsPage, ListingDetail, VinDetail, etc. — are Pydantic v2 models. Access fields as attributes and use standard Pydantic methods (.model_dump(), .model_json_schema(), etc.) as needed.
Error handling
All methods raise typed exceptions from visor.exceptions. The SDK does not retry automatically — RateLimitError.retry_after gives you the hint to build your own retry logic.
import time
from visor import VisorClient, ListingsFilter, RateLimitError, VisorAPIError
def fetch_with_backoff(client: VisorClient, f: ListingsFilter) -> object:
for attempt in range(4):
try:
return client.filter_listings(f)
except RateLimitError as e:
wait = e.retry_after if e.retry_after is not None else 2 ** attempt
print(f"Rate limited — waiting {wait}s")
time.sleep(wait)
except VisorAPIError as e:
raise # surface non-rate-limit errors immediately
raise RuntimeError("Exhausted retries")
Exception hierarchy:
| Exception | When |
|---|---|
VisorAPIError |
Base for all API errors; has .status_code |
AuthError |
401 — invalid or missing API key |
NotFoundError |
404 — resource does not exist |
RateLimitError |
429 — includes .retry_after (seconds, or None) |
Debugging
Inspect the exception — VisorAPIError carries .status_code and a message from the API.
Check retry_after — for RateLimitError, .retry_after is the number of seconds to wait (or None if the API did not provide a value).
Request-level logging — visor-python uses httpx internally. Enable httpx logging to see raw requests and responses:
import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("httpx").setLevel(logging.DEBUG)
Community
- CONTRIBUTING.md — how to contribute
- CODE_OF_CONDUCT.md — community standards
- GitHub Issues — bug reports and feature requests
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file visor_python-0.1.0.tar.gz.
File metadata
- Download URL: visor_python-0.1.0.tar.gz
- Upload date:
- Size: 20.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1a90a926662fd269b78ba07a43a30a40e1aadac23dd72118948c246fc5f879f2
|
|
| MD5 |
35f57955138f4f72ae35ed7161fca028
|
|
| BLAKE2b-256 |
abf900b2a114a33b9e84250e337c1674424cc8090475b53507ee55b914b052c9
|
Provenance
The following attestation bundles were made for visor_python-0.1.0.tar.gz:
Publisher:
publish.yml on whitewalls86/visor-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
visor_python-0.1.0.tar.gz -
Subject digest:
1a90a926662fd269b78ba07a43a30a40e1aadac23dd72118948c246fc5f879f2 - Sigstore transparency entry: 1810288354
- Sigstore integration time:
-
Permalink:
whitewalls86/visor-python@1d2444325fc4349191cbbafc91d018c61693900c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/whitewalls86
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1d2444325fc4349191cbbafc91d018c61693900c -
Trigger Event:
push
-
Statement type:
File details
Details for the file visor_python-0.1.0-py3-none-any.whl.
File metadata
- Download URL: visor_python-0.1.0-py3-none-any.whl
- Upload date:
- Size: 20.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
23f49e50768ccb19f5e37e7efa3c6c97c14c0058f38abc22d4f78547b7344136
|
|
| MD5 |
c4eb1a6c85d2d4ae1f6712a8e28694d8
|
|
| BLAKE2b-256 |
b49aca18ac1a054434b185790649040444bbaffdd0c30d395addba89a4f3ab12
|
Provenance
The following attestation bundles were made for visor_python-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on whitewalls86/visor-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
visor_python-0.1.0-py3-none-any.whl -
Subject digest:
23f49e50768ccb19f5e37e7efa3c6c97c14c0058f38abc22d4f78547b7344136 - Sigstore transparency entry: 1810288369
- Sigstore integration time:
-
Permalink:
whitewalls86/visor-python@1d2444325fc4349191cbbafc91d018c61693900c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/whitewalls86
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1d2444325fc4349191cbbafc91d018c61693900c -
Trigger Event:
push
-
Statement type: