Official TypeScript/JavaScript SDK for the Vairified Partner API
Multi-sport player ratings, search, and bulk match submission
Async-first TypeScript SDK for the Vairified Partner API. Built on
native fetch with zero runtime dependencies. Sub-resource layout, auto-paginating search,
n-team × n-game match submission, and await using lifecycle support on Node 24+.
npm install vairified
Or with any other package manager:
yarn add vairified
pnpm add vairified
bun add vairified
import { Vairified } from 'vairified';
await using client = new Vairified({ apiKey: 'vair_pk_xxx' });
const member = await client.members.get('vair_mem_xxx');
console.log(member.name, 'rated', member.ratingFor('pickleball'));
await using (TypeScript 5.2+, Node 20+) closes the client deterministically when the block
exits. If you can't use it, call await client.close() manually.
Every operation lives on a sub-resource that mirrors the REST path:
| Sub-resource | Operations |
|---|---|
client.members |
get, getBulk, search, find, ratingUpdates |
client.matches |
submit, tournamentImport, testWebhook |
client.oauth |
authorize, exchangeToken, refresh, revoke |
client.webhooks |
deliveries |
client.leaderboard |
list, rank, categories |
client.usage() |
Rate-limit + request-count stats |
const member = await client.members.get('vair_mem_xxx');
console.log(member.name); // Full name
console.log(member.displayName); // "Mike B."
console.log(member.ratingFor('pickleball')); // 3.915
console.log(member.status.isVairified); // true
// Dict-like access to rating splits for a specific sport
const pb = member.sport.get('pickleball');
if (pb) {
console.log(pb.rating, pb.abbr); // 3.915 VO
console.log(pb.get('overall-open')?.rating); // 3.915
console.log(pb.has('singles-open')); // true
for (const [key, split] of pb) {
console.log(key, split.rating);
}
}
Fetch up to 100 members in one call by their integer member IDs:
const members = await client.members.getBulk([4873327, 4873328, 4873329]);
for (const m of members) {
console.log(m.name, m.ratingFor('pickleball'));
}
// Filter to a specific sport
const pb = await client.members.getBulk([4873327], { sport: 'pickleball' });
Unknown IDs are silently omitted — the returned array may be shorter than the input.
// Just pickleball
const member = await client.members.get('vair_mem_xxx', { sport: 'pickleball' });
// Multiple sports
const member2 = await client.members.get('vair_mem_xxx', {
sport: ['pickleball', 'padel'],
});
search() is an async generator — iterate directly with for await. Pages are fetched
lazily, so memory usage stays bounded regardless of result count.
for await (const member of client.members.search({
city: 'Austin',
state: 'TX',
ratingMin: 3.5,
ratingMax: 4.5,
vairifiedOnly: true,
})) {
console.log(member.name, member.ratingFor('pickleball'));
}
// Cap with maxResults, or break out early
const top20: Member[] = [];
for await (const m of client.members.search({ name: 'Smith', maxResults: 20 })) {
top20.push(m);
}
const mike = await client.members.find('Mike Barker');
if (mike) {
console.log(mike.ratingFor('pickleball'));
}
const updates = await client.members.ratingUpdates();
for (const update of updates) {
const arrow = update.improved ? '↑' : '↓';
console.log(`${update.displayName} ${arrow} delta=${update.delta?.toFixed(3)}`);
}
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.
const result = await client.matches.submit({
sport: 'pickleball',
winScore: 11,
winBy: 2,
bracket: '4.0 Doubles',
event: 'Weekly League',
matchDate: '2026-04-11T14:00:00Z',
matches: [
{
identifier: 'm1',
teams: [
['vair_mem_aaa', 'vair_mem_bbb'],
['vair_mem_ccc', 'vair_mem_ddd'],
],
games: [{ scores: [11, 8] }, { scores: [11, 5] }],
},
{
identifier: 'm2',
teams: [['vair_mem_eee'], ['vair_mem_fff']], // singles
games: [{ scores: [11, 9] }, { scores: [11, 7] }],
},
],
});
if (result.ok) {
console.log(`Submitted ${result.numGames} games in ${result.numMatches} matches`);
}
Set dryRun: true on the batch to validate without persisting. No special scope
needed — any key with key:match:submit can dry-run.
Import historical tournament results with automatic player matching. Unmatched players become ghost accounts that can be claimed later.
const result = await client.matches.tournamentImport({
tournamentName: 'Austin Open 2026',
sport: 'pickleball',
winScore: 11,
winBy: 2,
matches: [
{
identifier: 'USAP-R1-M1',
event: 'Austin Open',
bracket: "Men's Pro Doubles",
format: 'DOUBLES',
matchDate: '2026-04-11T10:00:00Z',
teamA: {
player1: { firstName: 'Ben', lastName: 'Johns' },
player2: { firstName: 'Matt', lastName: 'Wright' },
game1: 11, game2: 11,
},
teamB: {
player1: { firstName: 'JW', lastName: 'Johnson' },
player2: { firstName: 'Dylan', lastName: 'Frazier' },
game1: 7, game2: 9,
},
},
],
});
console.log(`Imported ${result.matchesImported} matches, ${result.ghostPlayersCreated} ghosts`);
Inspect recent webhook delivery attempts for your app:
const result = await client.webhooks.deliveries({ status: 'failed', limit: 10 });
for (const d of result.deliveries) {
console.log(d.event, d.statusCode, d.errorMessage);
}
// Filter by event type
const ratingEvents = await client.webhooks.deliveries({ event: 'rating.updated' });
console.log(`${ratingEvents.total} total rating.updated deliveries`);
import { Vairified, OAuthError, generateState } from 'vairified';
await using client = new Vairified({ apiKey: 'vair_pk_xxx' });
// Step 1 — start authorization
const state = generateState();
const auth = await client.oauth.authorize({
redirectUri: 'https://myapp.com/oauth/callback',
scopes: ['user:profile:read', 'user:rating:read', 'user:match:submit'],
state,
});
// Redirect user to auth.authorizationUrl
// Step 2 — exchange the callback code
const tokens = await client.oauth.exchangeToken({
code: 'code-from-callback',
redirectUri: 'https://myapp.com/oauth/callback',
});
const { accessToken, refreshToken, playerId } = tokens;
// Step 3 — refresh when the access token expires
try {
const newTokens = await client.oauth.refresh(refreshToken!);
} catch (err) {
if (err instanceof OAuthError && err.errorCode === 'invalid_grant') {
// User must re-authorize
}
}
// Step 4 — revoke the connection
await client.oauth.revoke(playerId);
OAuthScope is a string literal union, so your editor will catch typos:
import type { OAuthScope } from 'vairified';
const scopes: OAuthScope[] = ['user:profile:read', 'user:rating:read']; // ok
const bad: OAuthScope[] = ['user:profile:read', 'rating']; // type error
| 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 |
// Global leaderboard
const lb = await client.leaderboard.list();
// Texas singles, verified only
const tx = await client.leaderboard.list({
category: 'singles',
scope: 'state',
state: 'TX',
verifiedOnly: true,
limit: 50,
});
// A specific player's rank with 5 players on either side
const rank = await client.leaderboard.rank('vair_mem_xxx', {
category: 'doubles',
contextSize: 5,
});
// Available categories, brackets, scopes
const categories = await client.leaderboard.categories();
import { Vairified } from 'vairified';
// Environment preset
const client = new Vairified({ apiKey: 'vair_pk_xxx', env: 'production' }); // default
const staging = new Vairified({ apiKey: 'vair_pk_xxx', env: 'staging' });
const local = new Vairified({ apiKey: 'vair_pk_xxx', env: 'local' });
// Custom base URL (overrides env)
const custom = new Vairified({
apiKey: 'vair_pk_xxx',
baseUrl: 'http://localhost:3001/api/v1',
timeoutMs: 30_000,
});
// Inject a custom fetch (test shims, non-Node environments)
const withFetch = new Vairified({
apiKey: 'vair_pk_xxx',
fetch: customFetchImpl,
});
export VAIRIFIED_API_KEY="vair_pk_xxx"
export VAIRIFIED_ENV="staging" # optional; default: production
const client = new Vairified(); // reads both env vars
The SDK maps HTTP status codes to typed exceptions. All typed exceptions inherit from
VairifiedError, so a single catch can handle everything.
import {
Vairified,
VairifiedError,
RateLimitError,
AuthenticationError,
NotFoundError,
ValidationError,
OAuthError,
} from 'vairified';
try {
const member = await client.members.get('vair_mem_xxx');
} catch (err) {
if (err instanceof RateLimitError) {
console.log(`Rate limited; retry after ${err.retryAfter}s`);
} else if (err instanceof AuthenticationError) {
console.log('Invalid API key');
} else if (err instanceof NotFoundError) {
console.log('Member not found');
} else if (err instanceof ValidationError) {
console.log(`Bad request: ${err.message}`);
} else if (err instanceof OAuthError) {
console.log(`OAuth error: ${err.message} (code: ${err.errorCode})`);
} else if (err instanceof VairifiedError) {
console.log(`API error: ${err.message} (status: ${err.statusCode})`);
} else {
throw err;
}
}
Response models are immutable classes wrapping the wire payload. They expose computed
getters (member.name, update.delta) and support iteration where it makes sense
(for (const [key, split] of sportRating)).
Membermember.memberId // Numeric member ID (public)
member.id // UUID | null
member.name // Full name (getter)
member.displayName // "Mike B."
member.firstName / lastName
member.gender // 'MALE' | 'FEMALE' | 'OTHER' | 'UNKNOWN' | null
member.age / city / state / zip / country
member.status.isVairified // grouped status flags
member.status.isConnected
member.sport // MemberSportMap
member.sports // readonly string[] of sport codes
member.ratingFor('pickleball') // number | null
member.split('overall-open') // RatingSplitWire | null
SportRating (dict-like)const pb = member.sport.get('pickleball');
pb?.rating // Primary rating for this sport
pb?.abbr // "VO", "VG", etc.
pb?.get('overall-open') // Any split key
pb?.size // Number of splits
pb?.has('singles-40+') // Membership check
for (const [key, split] of pb ?? []) { /* iterate */ }
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.getBulk(), matches.tournamentImport(), 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.
git clone https://github.com/Vairified/vairified.js.git
cd vairified.js
npm install
npm test
npm run build
MIT — see LICENSE for details.