.NET client library wrapping pinball data sources: IFPA/WPPR, OPDB, IPDB, and MatchPlay.
OPDB shut its own API endpoints down on 1 October 2026. The database is still alive and still gets weekly updates, but the data now comes from the MatchPlay API.
OPDBApi,IOpdbApiand every model underPinballApi.Models.OPDBwere removed in 4.0.0. Those endpoints no longer answer, so the wrapper could not keep working. UseMatchPlayApiinstead. See OPDB & PinTips for the replacement calls and Migrating fromOPDBApifor the mapping.
| Source | What it provides | Auth required |
|---|---|---|
| IFPA | Player rankings, tournament results, series standings (WPPR system) | API key |
| MatchPlay Events | Tournament software, ratings, OPDB machine data, PinTips | API token |
| IPDB | Classic pinball machine database | None |
OPDB machine data is served through MatchPlay. The opdb.org API shut down on 1 October 2026 and this library no longer wraps it.
dotnet add package PinballApi
using PinballApi;
using PinballApi.Models.WPPR.Universal.Rankings;
var api = new PinballRankingApi("YOUR_IFPA_API_KEY");
// Player lookup
var player = await api.GetPlayer(16927);
Console.WriteLine($"{player.FirstName} {player.LastName} — Rank #{player.WpprRank}");
// Tournament search near a location
var tournaments = await api.TournamentSearch(
latitude: 41.8240, longitude: -71.4128, radius: 50,
distanceType: DistanceType.Miles,
startDate: DateTime.Now, endDate: DateTime.Now.AddMonths(3));
// Rankings
var wpprTop100 = await api.RankingSearch(RankingType.Wppr, count: 100);
var womenTop50 = await api.RankingSearch(RankingType.Women, RankingSystem.Open, count: 50);
// Series (e.g. NACS)
var regions = await api.GetRegions("NACS", DateTime.Now.Year);
var standings = await api.GetSeriesStandingsForRegion("NACS", "RI", 2024);
// Directory data
var countries = await api.GetCountriesList();
var stateProvs = await api.GetStateProvList();using PinballApi;
using PinballApi.Models.MatchPlay.Opdb;
var matchPlay = new MatchPlayApi("YOUR_MATCHPLAY_TOKEN");
// One machine, with credits and images
var machine = await matchPlay.GetOpdbEntry("G4ODR-MLzY7", includePeople: true, includeImages: true);
Console.WriteLine($"{machine.Name} ({machine.Manufacturer.Name}, {machine.Year})");
// Playing tips
var tips = await matchPlay.GetPinTipsByOpdbId("G4ODR");
// Split an OPDB id into its group, machine and alias parts
var parts = OpdbIdParts.Parse("G0l8P-M85d9-A1ZNY"); // parts.EntryType == OpdbEntryType.Aliasusing PinballApi;
var ipdb = new PinballMachineApi();
var machine = await ipdb.GetMachineByIpdbId(3648);using PinballApi;
var matchPlay = new MatchPlayApi("YOUR_MATCHPLAY_TOKEN");
var profile = await matchPlay.GetProfile(12345);
// Let the client wait out a rate limit window instead of throwing on HTTP 429.
// A wait can last a full minute, so leave this off when you cannot block.
var patient = new MatchPlayApi("YOUR_MATCHPLAY_TOKEN", rateLimitRetryCount: 2);Every call takes a CancellationToken, and every paged endpoint has an Enumerate twin that
walks the pages for you:
await foreach (var tournament in matchPlay.EnumerateTournaments(playedUserId: 12345, cancellationToken: token))
{
Console.WriteLine(tournament.Name);
}A failed call raises PinballApiException, so the HTTP layer stays out of your code:
try
{
var tournament = await matchPlay.GetTournament(999999999);
}
catch (PinballApiException ex) when (ex.IsNotFound)
{
// ex.StatusCode, ex.ResponseBody and ex.RequestUrl are all available.
// ex.IsRateLimited and ex.IsUnauthorized cover the other common cases.
}The PinballRankingApi class implements IPinballRankingApi and covers these endpoint groups from the IFPA API 2.1 spec:
| Method | IFPA Endpoint |
|---|---|
GetPlayer(id) |
GET /player/{id} |
GetPlayers(ids) |
GET /player |
PlayerSearch(name, country, stateProv, tournament, tournamentPosition) |
GET /player/search |
GetPlayerResults(id, system, type) |
GET /player/{id}/results/{ranking_system}/{type} |
GetPlayerHistory(id, system, activeOnly) |
GET /player/{id}/rank_history |
GetPlayerVersusPlayer(id, system) |
GET /player/{id}/pvp |
GetPlayerVersusPlayerComparison(id, id2) |
GET /player/{id}/pvp/{id2} |
| Method | IFPA Endpoint |
|---|---|
RankingSearch(type, system, count, startPos, country) |
GET /rankings/{type} |
ProRankingSearch(system) |
GET /rankings/pro/{ranking_system} |
GetRankingCountries() |
GET /rankings/country_list |
GetCustomRankings() |
GET /rankings/custom/list |
GetCustomRankingViewResult(id, count, startPos) |
GET /rankings/custom/{id} |
| Method | IFPA Endpoint |
|---|---|
GetTournament(id) |
GET /tournament/{id} |
TournamentSearch(...) |
GET /tournament/search |
GetTournamentResults(id) |
GET /tournament/{id}/results |
GetTournamentFormats() |
GET /tournament/formats |
GetRelatedTournaments(id) |
GET /tournament/{id}/related |
| Method | IFPA Endpoint |
|---|---|
GetSeries() |
GET /series/list |
GetRegions(code, year) |
GET /series/{code}/regions |
GetSeriesOverallStanding(code, year) |
GET /series/{code}/overall_standings |
GetSeriesStandingsForRegion(code, region, year) |
GET /series/{code}/standings |
GetSeriesTournamentsForRegion(code, region, year) |
GET /series/{code}/tournaments |
GetSeriesPlayerCard(playerId, code, region, year) |
GET /series/{code}/player_card/{playerId} |
GetSeriesWinners(code, region) |
GET /series/{code}/past_winners |
GetRegionReps(code) |
GET /series/{code}/region_reps |
GetSeriesStats(code, region, year) |
GET /series/{code}/stats |
| Method | IFPA Endpoint |
|---|---|
GetDirector(id) |
GET /director/{id} |
GetDirectorTournaments(id, period) |
GET /director/{id}/tournaments/{time_period} |
GetCountryDirectors() |
GET /director/country |
GetDirectorsBySearch(name, count) |
GET /director/search |
| Method | IFPA Endpoint |
|---|---|
GetOverallStatistics() |
GET /stats/overall |
GetEventsByYearStatistics(system) |
GET /stats/events_by_year |
GetLargestTournamentStatistics(system) |
GET /stats/largest_tournaments |
GetLucrativeTournamentStatistics(system) |
GET /stats/lucrative_tournaments |
GetPlayersByYearStatistics() |
GET /stats/players_by_year |
GetPlayersByStateStatistics(system) |
GET /stats/state_players |
GetTournamentsByStateStatistics(system) |
GET /stats/state_tournaments |
GetPlayersByCountryStatistics(system) |
GET /stats/country_players |
GetPlayersPointsByGivenPeriod(start, end, system, limit) |
GET /stats/points_given_period |
GetPlayersEventsAttendedByGivenPeriod(start, end, system, limit) |
GET /stats/events_attended_period |
| Method | IFPA Endpoint |
|---|---|
GetCountriesList() |
GET /other/countries |
GetStateProvList() |
GET /other/stateprovs |
MatchPlayApi implements IMatchPlayApi, so you can inject it and replace it in tests.
| Method | MatchPlay Endpoint |
|---|---|
GetTournaments(...) |
GET /api/tournaments |
GetTournament(id, include...) |
GET /api/tournaments/{id} |
GetStandings(id) |
GET /api/tournaments/{id}/standings |
GetRounds(id) |
GET /api/tournaments/{id}/rounds |
GetGames(...) |
GET /api/games |
GetSinglePlayerGames(...) |
GET /api/tournaments/{id}/single-player-games |
GetCards(...) |
GET /api/tournaments/{id}/cards |
GetIfpaEstimate(...) |
POST /api/ifpa/wppr-estimator |
MatchPlay returns bare playerId and arenaId values to keep responses small. Ask for the
tournament players and arenas with the include flags first, then resolve whatever ids are left.
Each call takes up to 25 ids (MatchPlayApi.MaxResolveIds).
| Method | MatchPlay Endpoint |
|---|---|
ResolveUnknownPlayers(ids) |
GET /api/players/resolve-unknown |
ResolveUnknownArenas(ids) |
GET /api/arenas/resolve-unknown |
ResolveUnknownUsers(ids) |
GET /api/users/resolve-unknown |
ResolveUnknownTournamentPlayers(id, ids) |
GET /api/tournaments/{id}/players/resolve-unknown |
ResolveUnknownTournamentArenas(id, ids) |
GET /api/tournaments/{id}/arenas/resolve-unknown |
The tournament variants also fill in the pivot data, such as the player seed and the arena label.
These need a completed tournament. MatchPlay returns an empty list for one that is still open.
| Method | MatchPlay Endpoint |
|---|---|
GetTournamentArenaSummary(id) |
GET /api/tournaments/{id}/summary/arenas |
GetTournamentPlayerArenaSummary(id) |
GET /api/tournaments/{id}/summary/player-arenas |
GetTournamentMatchSummary(id) |
GET /api/tournaments/{id}/summary/matches |
MatchPlayApi covers the OPDB and PinTips endpoints
that replaced the opdb.org API.
| Method | MatchPlay Endpoint |
|---|---|
GetOpdbEntry(opdbId, includePeople, includeImages) |
GET /api/opdb/entry/{opdbId} |
GetOpdbChangelog() |
GET /api/opdb/changelog |
GetPinTipsByOpdbId(opdbId) |
GET /api/pintips?opdbId= |
GetPinTipsByArenaId(arenaId) |
GET /api/pintips?arenaId= |
An OPDB entry is a machine group, a machine or an alias. Read OpdbEntry.EntryType to tell them
apart, or use the IsMachineGroup, IsMachine and IsAlias helpers. OpdbIdParts.Parse() and
OpdbIdParts.TryParse() split an OPDB id into its group, machine and alias parts.
MatchPlay asks that you do not call the per-entry endpoints in a loop. Download an export once, store it, and serve searches and typeaheads from your own store. The exports need no API token and are hosted on a CDN.
| Method | Contents |
|---|---|
GetOpdbExport() |
Every OPDB entry, about 5 MB. Returns List<OpdbEntry>. |
GetOpdbSlimExport() |
Name, manufacturer and backglass image only, about 2 MB. Returns List<OpdbSlimEntry>. |
GetPinTipsExport() |
Every PinTip, about 1 MB. Returns List<PinTip>. |
The raw URLs are also public as MatchPlayApi.OpdbExportUrl, MatchPlayApi.OpdbSlimExportUrl,
MatchPlayApi.PinTipsExportUrl and MatchPlayApi.OpdbLegacyExportUrl.
var machines = await matchPlay.GetOpdbSlimExport();
var backglass = machines
.Where(m => m.EntryType == OpdbEntryType.Machine && m.PrimaryBackglassImage != null)
.ToDictionary(m => m.OpdbId, m => m.PrimaryBackglassImage.Urls.Medium);OPDBApi and IOpdbApi were removed in 4.0.0. Swap the client for MatchPlayApi and use a
MatchPlay API token in place of the OPDB token.
| Removed call | Replacement |
|---|---|
GetMachineInfo(opdbId) |
MatchPlayApi.GetOpdbEntry(opdbId) |
Export() |
MatchPlayApi.GetOpdbExport() |
GetMachineInfoByIpdbId(ipdbId) |
No endpoint. Index GetOpdbExport() by OpdbEntry.IpdbId. |
Search(query) |
No endpoint. Search your own copy of GetOpdbExport(). |
TypeAheadSearch(query) |
Removed by OPDB on purpose. Serve typeahead from GetOpdbSlimExport(). |
The model shape also changed. PinballApi.Models.OPDB.PinballMachine became
PinballApi.Models.MatchPlay.Opdb.OpdbEntry:
| Old member | New member |
|---|---|
OpdbId |
OpdbId, plus OpdbGroup and OpdbMachine for the parent ids |
IsMachine / IsAlias |
EntryType, or the IsMachine, IsMachineGroup and IsAlias helpers |
PhysicalMachine (int) |
PhysicalMachine (bool) |
Shortname |
ShortName |
ManufactureDate (DateTime) |
ManufactureDate (DateTime?), plus Year |
Features (List<string>) |
Features (List<OpdbFeature>) |
Keywords |
Removed upstream. |
| — | New: People, NameSort, PinballPrimerUrl, PinballRulesUrl, PinballCardsUrl, BobsGuideUrl, CompetitionSetupUrl, CompetitionNotesUrl |
GetLeagues()— the endpoint (GET /tournament/leagues/{period}) is documented but was returning 404 at last check; method throwsNotImplementedException.GET /series/{code}/past_winners— used byGetSeriesWinners()but not in the official OpenAPI spec; works in practice.- Player search with multi-word names (e.g.
"Julia Randall") may not work correctly — IFPA API limitation. - MatchPlay
GET /api/rating-periodsreturns401 Not allowed (token)for some tokens. This is a permission on the MatchPlay side. - MatchPlay
GET /api/tournaments/{id}/queuesreturns 403 unless the token has scorekeeper scope. - MatchPlay rate limits several endpoints to 6 requests per minute, well under the documented 120. Confirmed on
/api/searchand/api/tournaments/{id}/summary/*. Read thex-ratelimit-*response headers. - MatchPlay reads boolean query params by presence. Sending
flag=falseturns the flag on. The wrapper sends a flag only when you set it. - Director search by name is currently broken on the API side.
PinballRankingApiV1 and PinballRankingApiV2 wrap the older versioned IFPA endpoints. The IFPA team recommends migrating to the Universal (unversioned) API — these are no longer updated upstream. Use PinballRankingApi (Universal) for all new work.
OPDBApi and IOpdbApi wrapped the opdb.org API. OPDB shut those endpoints down on
1 October 2026, so both types and their models were removed in 4.0.0. See
Migrating from OPDBApi.