Skip to main content

Vairified Python SDK

Official Python SDK for the Vairified Partner API
Multi-sport player ratings, search, and bulk match submission

CI Version 0.3.0 Python 3.12+ PyPI


Async-first Python SDK for the Vairified Partner API. Built on httpx and pydantic v2, with a surface designed to feel native: properties instead of getters, dict-like access to rating splits, auto-paginating search, and a sub-resource layout that mirrors the REST API.

Installation

pip install vairified

Or with uv:

uv add vairified

Quick Start

import asyncio
from vairified import Vairified

async def main() -> None:
    async with Vairified(api_key="vair_pk_xxx") as client:
        member = await client.members.get("vair_mem_xxx")
        print(member.name, "rated", member.rating_for("pickleball"))

asyncio.run(main())

The client is an async context manager — opening it creates an httpx.AsyncClient, closing it releases the underlying connection pool.

Sub-resources

Every operation lives on a sub-resource that matches the REST path:

Sub-resource Operations
client.members get, get_bulk, get_by_email, search, find, rating_updates
client.matches submit, tournament_import, test_webhook
client.oauth authorize, exchange_token, refresh, revoke
client.leaderboard list, rank, categories
client.webhooks deliveries
client.usage() Rate-limit + request-count stats

Members

Get a connected member

async with Vairified(api_key="vair_pk_xxx") as client:
    member = await client.members.get("vair_mem_xxx")

    print(member.name)                      # Full name
    print(member.display_name)               # "Mike B."
    print(member.rating_for("pickleball"))   # 3.915

    # Dict-like access to rating splits + per-sport status for a sport
    pb = member.sport["pickleball"]
    print(pb.rating, pb.abbr)                # 3.915 VO
    print(pb.is_vairified)                   # True (per-sport, Vairified#783)
    print(pb["overall-open"].rating)         # 3.915
    print("singles-open" in pb)              # True
    for key, split in pb:
        print(key, split.rating)

Filter ratings to specific sports

# Just pickleball
member = await client.members.get("vair_mem_xxx", sport="pickleball")

# Multiple sports
member = await client.members.get(
    "vair_mem_xxx",
    sport=["pickleball", "padel"],
)

Auto-paginating search

search() is an async iterator — it streams results one member at a time, fetching pages lazily as you iterate. break early when you have what you need, or cap with max_results.

async with Vairified(api_key="vair_pk_xxx") as client:
    async for member in client.members.search(
        city="Austin",
        state="TX",
        rating_min=3.5,
        rating_max=4.5,
        vairified_only=True,
    ):
        print(member.name, member.rating_for("pickleball"))

    # Find first N across pages
    top_20: list = []
    async for m in client.members.search(name="Smith", max_results=20):
        top_20.append(m)

Find by name (first hit only)

mike = await client.members.find("Mike Barker")
if mike:
    print(mike.rating_for("pickleball"))

Bulk member lookup

Fetch up to 100 members in one call by their integer member IDs:

members = await client.members.get_bulk([4873327, 4873328, 4873329])
for m in members:
    print(m.name, m.rating_for("pickleball"))

# Filter to a specific sport
pb = await client.members.get_bulk([4873327], sport="pickleball")

Unknown IDs are silently omitted — the list may be shorter than the input.

Look members up by email

Resolve up to 100 members by their exact email address — useful for linking your users to their VAIR identity when you hold their email but not their member ID, instead of waiting for each player to complete SSO:

result = await client.members.get_by_email(["ada@example.com", "nobody@example.com"])

for match in result.matched:
    member = match.sole  # None when the address is ambiguous
    if member:
        print(match.email, "->", member.member_id)

# Read not_found directly — don't diff your input against the results
print("no VAIR account found for:", result.not_found)

Requires the key:player:lookup scope, granted per partner on approval — holding key:player:search does not imply it. Ask VAIR to enable it for your app.

Unlike get_bulk, nothing is silently dropped: every address you supply comes back in either matched or not_found. Matching is exact and case-insensitive (no partial or fuzzy matching), and match.members is a list because an email is not a unique key in VAIR — use .sole or check .is_ambiguous rather than assuming a single result. A not_found address is not proof the person has no VAIR account: unclaimed imported records are excluded from this lookup.

Rating change notifications

updates = await client.members.rating_updates()
for update in updates:
    arrow = "↑" if update.improved else "↓"
    print(f"{update.display_name} {arrow} delta={update.delta:+.3f}")

Match Submission

Matches are submitted as a MatchBatch — defaults at the batch level apply to every match unless overridden. The shape is n-team × n-game, so singles, doubles, and round-robin all go through the same path.

from vairified import Vairified, MatchBatch, Match, Game

async with Vairified(api_key="vair_pk_xxx") as client:
    batch = MatchBatch(
        sport="pickleball",
        win_score=11,
        win_by=2,
        bracket="4.0 Doubles",
        event="Weekly League",
        match_date="2026-04-11T14:00:00Z",
        matches=[
            Match(
                identifier="m1",
                teams=[["vair_mem_aaa", "vair_mem_bbb"],
                       ["vair_mem_ccc", "vair_mem_ddd"]],
                games=[Game(scores=[11, 8]), Game(scores=[11, 5])],
            ),
            Match(
                identifier="m2",
                teams=[["vair_mem_eee"], ["vair_mem_fff"]],   # singles
                games=[Game(scores=[11, 9]), Game(scores=[11, 7])],
            ),
        ],
    )
    result = await client.matches.submit(batch)
    if result.ok:
        print(f"Submitted {result.num_games} games in {result.num_matches} matches")

Set batch.dry_run = True to validate without persisting. No special scope needed — any key with key:match:submit can dry-run.

Tournament import

Import historical tournament results with automatic player matching:

result = await client.matches.tournament_import({
    "tournamentName": "Austin Open 2026",
    "sport": "pickleball",
    "winScore": 11,
    "winBy": 2,
    "matches": [...]
})
print(f"Imported {result.matches_imported} matches, {result.ghost_players_created} ghosts")

Webhook Deliveries

Inspect recent webhook delivery attempts for your app:

result = await client.webhooks.deliveries(status="failed", limit=10)
for d in result.deliveries:
    print(d.event, d.status_code, d.error_message)

# Filter by event type
rating_events = await client.webhooks.deliveries(event="rating.updated")
print(f"{rating_events.total} total rating.updated deliveries")

OAuth Connect Flow

import secrets
from vairified import Vairified, OAuthError

async with Vairified(api_key="vair_pk_xxx") as client:
    # Step 1 — start authorization
    state = secrets.token_urlsafe(32)
    auth = await client.oauth.authorize(
        redirect_uri="https://myapp.com/oauth/callback",
        scopes=["user:profile:read", "user:rating:read", "user:match:submit"],
        state=state,
    )
    redirect_to = auth.authorization_url

    # Step 2 — exchange the callback code
    tokens = await client.oauth.exchange_token(
        code="code-from-callback",
        redirect_uri="https://myapp.com/oauth/callback",
    )
    access = tokens.access_token
    refresh = tokens.refresh_token
    player_id = tokens.player_id

    # Step 3 — refresh when the access token expires
    try:
        new_tokens = await client.oauth.refresh(refresh)
    except OAuthError as e:
        if e.error_code == "invalid_grant":
            ...  # user must re-authorize

    # Step 4 — revoke the connection
    await client.oauth.revoke(player_id)

Available scopes

Scope Description
user:profile:read Name, location, verification status
user:profile:email Email address
user:rating:read Current rating and rating splits
user:rating:history Complete rating history
user:match:submit Submit matches on behalf of user
user:webhook:subscribe Rating change notifications

Leaderboards

async with Vairified(api_key="vair_pk_xxx") as client:
    # Global doubles leaderboard
    lb = await client.leaderboard.list()

    # Texas singles, verified only
    tx = await client.leaderboard.list(
        category="singles",
        scope="state",
        state="TX",
        verified_only=True,
        limit=50,
    )

    # A specific player's rank with 5 players on either side
    rank = await client.leaderboard.rank(
        "vair_mem_xxx",
        category="doubles",
        context_size=5,
    )
    print(f"#{rank['rank']} (top {rank['percentile']:.1f}%)")

    # Available categories, brackets, scopes
    categories = await client.leaderboard.categories()

Models

All response models are immutable pydantic v2 BaseModels. They accept both snake_case (Python) and camelCase (wire) field names, and tolerate new fields from the server so your code keeps working across API additions.

Member

member.member_id                     # Numeric member ID
member.id                            # UUID
member.name                          # Full name (property)
member.display_name                  # "Mike B."
member.first_name                    # "Mike"
member.last_name                     # "Barker"
member.gender                        # Gender enum (MALE | FEMALE | OTHER | UNKNOWN)
member.age
member.city / state / zip / country
member.status.is_ambassador          # Global status flags
member.status.is_connected
member.sport                         # dict[str, SportRating]
member.sports                        # ["pickleball", "padel"] (property)
member.sport["pickleball"].is_vairified      # per-sport (Vairified#783)
member.sport["pickleball"].is_vair_pro       # per-sport
member.sport["pickleball"].is_vair_pro_status  # "ACTIVE" | "PENDING" | None
member.rating_for("pickleball")      # float | None
member.split("overall-open")         # RatingSplit | None

SportRating (dict-like)

pb = member.sport["pickleball"]
pb.rating                     # Primary rating for this sport
pb.abbr                       # "VO", "VG", etc.
pb["overall-open"].rating     # Any split key
len(pb)                       # Number of splits
"singles-40+" in pb           # Membership check
for key, split in pb: ...     # Iterate (yields (key, RatingSplit) tuples)

Match / MatchBatch

Request-side models use camelCase aliases (winScore, winBy, matchDate) when serialized — write Python in snake_case, the wire stays consistent.

batch = MatchBatch(
    sport="pickleball",
    win_score=11,
    win_by=2,
    match_date="2026-04-11T14:00:00Z",
    matches=[
        Match(
            identifier="m1",
            teams=[["p1", "p2"], ["p3", "p4"]],   # n-team × n-player
            games=[Game(scores=[11, 8]),           # n-game match
                   Game(scores=[11, 5])],
        ),
    ],
)

RatingUpdate

update.member_id
update.previous_rating
update.new_rating
update.delta        # new - previous (or None)
update.improved     # True if delta > 0
update.changed_at

Configuration

from vairified import Vairified

# Environment preset
client = Vairified(api_key="vair_pk_xxx", env="production")   # default
client = Vairified(api_key="vair_pk_xxx", env="staging")
client = Vairified(api_key="vair_pk_xxx", env="local")

# Custom base URL (overrides env)
client = Vairified(
    api_key="vair_pk_xxx",
    base_url="http://localhost:3001/api/v1",
    timeout=30.0,
)

Environment variables

export VAIRIFIED_API_KEY="vair_pk_xxx"
export VAIRIFIED_ENV="staging"   # optional; default: production
async with Vairified() as client:   # reads both env vars
    ...

API Key Scopes

Scope Access
key:admin Full access to all endpoints
key:write All read + write operations
key:read All read operations
key:leaderboard:read Leaderboard endpoints only
key:player:search Player search only
key:member:read Connected member data only
key:match:submit Submit match results
key:tournament:import Import tournament data

Error Handling

from vairified import (
    Vairified,
    VairifiedError,
    RateLimitError,
    AuthenticationError,
    NotFoundError,
    ValidationError,
    OAuthError,
)

async with Vairified(api_key="vair_pk_xxx") as client:
    try:
        member = await client.members.get("vair_mem_xxx")
    except RateLimitError as e:
        print(f"Rate limited; retry after {e.retry_after}s")
    except AuthenticationError:
        print("Invalid API key")
    except NotFoundError:
        print("Member not found")
    except ValidationError as e:
        print(f"Bad request: {e.message}")
    except OAuthError as e:
        print(f"OAuth error: {e.message} (code: {e.error_code})")
    except VairifiedError as e:
        print(f"API error: {e.message} (status: {e.status_code})")

Migrating from 0.1.x

Version 0.2.0 is a breaking rewrite. See the From 0.2.x → 0.3.0: All OAuth scope strings gained a user: prefix (profile:read → user:profile:read). Update any hardcoded scope arrays. New: members.get_bulk(), matches.tournament_import(), webhooks.deliveries().

From 0.1.x → 0.2.0: Full rewrite — see the migration guide for the full diff, and CHANGELOG.md for the release notes.

Development

git clone https://github.com/Vairified/vairified.py.git
cd vairified.py
uv sync --all-extras
uv run pytest

License

MIT — see LICENSE for details.


vairified.com · Documentation · Support

Release files for vairified 0.6.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 vairified 0.6.0
File Size Uploaded
vairified-0.6.0.tar.gz 101.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for vairified 0.6.0
File Interpreter ABI Platform
vairified-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 132.7 kB

Release files / vairified-0.6.0.tar.gz

Download URL vairified-0.6.0.tar.gz
Size 101.0 kB
Tags Source
SHA-256 checksum
How to use checksums
dcd06080a2354a854fb8623170c717e8b17e5bd0ca7583d4f9ad96baf30d77b7
BLAKE2b-256 checksum
How to use checksums
b89b7f79f5142fb955a8162729e0539c776099fbc40634f72807558a60048a27
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 25, 2026.

Transparency log

Release files / vairified-0.6.0-py3-none-any.whl

Download URL vairified-0.6.0-py3-none-any.whl
Size 31.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e582f6433254039aed89cb3a4a9be10f8ba95066bd7326a52ed0fd1751433cbe
BLAKE2b-256 checksum
How to use checksums
e1b81804b26914955c6505da00637198d37c79ccc77ac8f3b8ab52ff537ef967
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 25, 2026.

Transparency log

Release history Release notifications | RSS feed

0.8.0

2 release files

0.7.0

2 release files

This release

0.6.0 This release

2 release files

0.4.0

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

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