API 2.0 is now available

API 2.0 has been released for use by logged-in members and TDs.

To register an API key, use the TD Portal, which is open to TDs and logged-in members. The name field is the key name, which is not necessarily the member name. (Developers might choose to use a separate key for each app and use the app name as the key name.)

At this time, only the first tier has been implemented. This tier is intended primarily for 3rd party developers who need to be able to access ratings supplements and member data in their apps without going through the TD portal each time. Supplement and membership data is needed for tasks like validating tournament registrations or checking the membership status of players in a pairing app. Developers will have to have a valid US Chess membership and ID and a login to the US Chess website so their ID can be authenticated when a key is generated.

At this time, what we are thinking is that we will allow developers to register one or more API keys that they can use in their apps for all their users. That means the developers will be responsible for keeping their API Keys secure, such as by encrypting them or by not including the key in their code and fetching it from their server before each API call.

We will be tracking API usage by key and IP address. Excessive use may result in disabling an API key, or changes to the API protocols and rate limits

The alternative would probably be to provide API keys only to TDs and other members, which would require each app to have fields for entering that user’s API key in their profile settings.

Mike Nolan

2 Likes

I’m trying to figure out the best way to handle this. What has been the biggest issue for WinTD users has been that (for years) you could get the allratings file downloaded and installed by clicking on a menu item, picking the supplement you wanted and the software would download it, unzip it and copy it to a local directory. That could be done whether or not you were a US Chess member. For the past eight months, they have had to go to the TD portal, download a text file, then install the downloaded file. Obviously considerably less automated, plus it required someone to login to the TD portal each month. If I were to have a common API key that is hard-coded into the software that I use only to download supplements (not to do single member accesses) couldn’t someone just hack out the API key by monitoring the activity out of WinTD and publish it. (I’m too old to do hacking, but I would imagine it’s not that hard). I don’t see that as being a good idea. My plan was to require everyone to get their own API key (which is saved only on the local machine) and if they want to share it with the world, that’s their problem, not mine. That requires someone within the organization to go to the TD Portal (just once) and get an API key and let everyone authorized know what it is and WinTD could take it from there.

Actually, that was my preferred method of handling keys up front, but I guess Nick was worried about having several thousand keys to deal with. I’ll pass your note along to the developers and staff.

Developers would still need to register for an API key so that they can test that everything works.

I can confirm that getting your key would be easy if an app is built this way with an API key included. I’m also not sure why anyone would care to do so if it only grants access to data that is already public, though. It would be a big problem if the API keys allowed access to write operations or access to private data of some kind.

This design is fine for hosted applications, but not for a client app talking directly to USCF if you intend for the keys to be secret.

The rating supplement info is weirdly treated as “sorta confidential” when aggregated although each individual’s rating info is 100% public.

Mike, thank you; I’ll definitely update some workflows to use this. Do you have a list of the priority for any API 2.0 calls? I’m particularly thinking about, on the read side:

  • Retrieve a full list of directed events (or just event ids) by TD (ideally with an optional time range restriction)
  • Retrieve a full list of run events (or just event ids) by Affiliate (ideally with an optional time range restriction)
  • Retrieve all data (full event info, full section info) for a list of event IDs

On the write side, of course, submitting an event is the holy grail.

Sharing some feedback based on my first pass of testing v2.

A. Query parameters documented in the v2 OpenAPI spec that do not behave as documented

A1. Fide=false behaves identically to Fide=true

Fide=true correctly returns only members with a FIDE ID. Fide=false returns
the same filtered set instead of the complement.

for f in "" "&Fide=true" "&Fide=false"; do
  curl -s -H "X-Api-Key: $KEY" \\
    "https://ratings-api.uschess.org/api/v2/members?Fuzzy=smith&Size=50$f" \\
    | jq "\[.items\[\] | select(.fideId != null)\] | length"
done
# 12   <- no filter (baseline)
# 50   <- Fide=true   (correct)
# 50   <- Fide=false  (expected 0)

A2. UseUnofficialRatings has no effect

true and false return identical ratings, and neither matches the actual
unofficial post-event rating.

for u in true false; do
  curl -s -H "X-Api-Key: $KEY" \\
    "https://ratings-api.uschess.org/api/v2/members?Fuzzy=12743305&UseUnofficialRatings=$u" \\
    | jq -r "\\"$u -> \\(.items\[0\].ratings\[\] | select(.ratingSystem==\\"R\\") | .rating)\\""
done
# true  -> 2870
# false -> 2870

curl -s "https://ratings-api.uschess.org/api/v1/members/12743305/sections?Size=1" \\
  | jq '.items\[0\].ratingRecords\[0\] | {preRating, postRating}'
# { "preRating": 2863, "postRating": 2852 }   <- event ended 2026-08-19

2870 is the official supplement rating in both cases. The unofficial rating
(2852) is not reachable through v2 under either setting.

A3. Size on /api/v2/members is silently capped at 100

The parameter is documented, but carries no maximum. Over-large values are
clamped rather than rejected, and nothing but the echoed pageSize indicates the
requested size was not honored.

for s in 100 101 500; do
  curl -s -H "X-Api-Key: $KEY" \\
    "https://ratings-api.uschess.org/api/v2/members?Fuzzy=smith&Size=$s" \\
    | jq -r "\\"Size=$s -> items=\\(.items|length) pageSize=\\(.pageSize)\\""
done
# Size=100 -> items=100 pageSize=100
# Size=101 -> items=100 pageSize=100
# Size=500 -> items=100 pageSize=100

Adding maximum: 100 to the parameter schema would be enough.


B. Response fields declared in the spec that carry no data

Both fields are nullable: true in the spec, so always returning null is not
strictly a contract violation. We raise them because the field exists with no
reachable value, and in B2’s case the data was previously served.

B1. uscfTitle is never populated

MemberDetailDto.uscfTitle returns null for every member we checked,
including all 100 top-ranked US members. v1’s `/awards` shows these same players
holding USCF ratings titles.

curl -s -H "X-Api-Key: $KEY" \\
  https://ratings-api.uschess.org/api/v2/members/12743305 | jq '.uscfTitle'
# null        <- Fabiano Caruana, US #1

curl -s https://ratings-api.uschess.org/api/v1/members/12743305/awards \\
  | jq '\[.items\[\] | select(.category=="RatingsTitle") | .type\] | unique'
# \["NationalMaster","OriginalLifeMaster"\]

Same result for Nakamura (12641216), So (13145890), Aronian (15218444).
There is no other title field in v2, so NM/LM currently has no v2 source.

B2. gamesPlayed is missing for established ratings — a regression

MemberRatingDto.gamesPlayed is returned only when isProvisional: true. Across
600 rating objects from the top 100 members, \0 of 384* established ratings
carried a game count.

curl -s -H "X-Api-Key: $KEY" \\
  https://ratings-api.uschess.org/api/v2/members/12641216 \\
  | jq '.ratings\[\] | {ratingSystem, rating, isProvisional, gamesPlayed}'
# {"ratingSystem":"R","rating":2849,"isProvisional":false,"gamesPlayed":null}
# {"ratingSystem":"Q","rating":2724,"isProvisional":false,"gamesPlayed":null}
# {"ratingSystem":"B","rating":2911,"isProvisional":false,"gamesPlayed":null}
# {"ratingSystem":"OR","rating":2747,"isProvisional":true,"gamesPlayed":21}

We observed this data previously. Our captured response for this same member
(Nakamura) returned gamesPlayed: 1566 for OverTheBoardRegular and 180 for
OverTheBoardBlitz; the provisional OnlineRegular value of 21 is unchanged.
v1 /members/{id} now behaves the same way, so the change affects both versions.

Was this change intentional?

B3. ordinal is never populated

MemberDetailDto.ordinal is declared {"type": "integer", "format": "int32", "nullable": true} and never appears in a response, on either the list or the
detail endpoint, for any of the same 100 members. rank and stateRank are
populated on those records, so it is not obvious what `ordinal` would add.

ordinal and uscfTitle are the only two declared MemberDetailDto properties
that never appear; every other property in the schema was observed with a value
on at least one member.


C. Other Issues

C1. gms* columns in AllRatings.tsv are always empty

Uscf-YYYY-MM-AllRatings.tsv declares six game-count columns. All six are empty
in every one of the 1,283,760 rows of Uscf-2026-01-AllRatings.tsv, while
the adjacent rtg* columns are populated.

curl -s -H "X-Api-Key: $KEY" \\
  https://ratings-api.uschess.org/api/v2/rating-supplement-files/Uscf-2026-01-AllRatings.tsv \\
  | awk -F'\\t' 'NR>1{for(i=10;i<=21;i++) if($i!="") c\[i\]++}
      END{print "rtgreg",c\[10\]; print "gmsreg",c\[11\];
          print "rtgquick",c\[12\]; print "gmsquick",c\[13\]}'
# rtgreg   865378
# gmsreg   0
# rtgquick 564791
# gmsquick 0

Game counts do appear, but only encoded inside the rating value as 2194/18 for
provisional players; established players show 1789*. Combined with B2, there is
currently no way to obtain an established player’s game count from v2.

C2. AllMembers-Blitz.dbf stores blitz ratings under Q_* field names

The Blitz and Quick AllMembers DBFs declare an identical field list —
R_PLR_TYP, R_LPB_RAT, R_NRM_DAT, Q_PLR_TYP, Q_LPB_RAT, Q_NRM_DAT.
There is no B_* field. In the Blitz file the Q_* columns actually contain
blitz data:

Member Quick file `Q_LPB_RAT` Blitz file `Q_LPB_RAT` live Q live B
Caruana (12743305) 2753 2852 2736 2799
Nakamura (12641216) 2765 2959 2724 2911
So (13145890) 2695 2858 2683 2879

(DBF values are the Dec 2025 supplement, hence higher than today’s.) A consumer
parsing by field name reads blitz ratings as quick ones with no error. Renaming
the columns to B_* in the Blitz file would fix it, unless this naming is intentional for some reason.

C3. /rating-supplement-files uses different pagination parameter names

The spec documents offset/pageSize here and Offset/Size on every other
paged operation, so the server matches its contract — but the inconsistency is
easy to trip over, and the wrong spelling is silently ignored rather than
rejected.

curl -s -H "X-Api-Key: $KEY" \\
  "https://ratings-api.uschess.org/api/v2/rating-supplement-files?Size=5" \\
  | jq '.items | length'
# 50   <- Size ignored, default applied

curl -s -H "X-Api-Key: $KEY" \\
  "https://ratings-api.uschess.org/api/v2/rating-supplement-files?pageSize=5" \\
  | jq '.items | length'
# 5

C4. Minor: UsePeak changes ordering but no peak value is exposed

UsePeak=true does affect sorting, but the returned DTO contains current ratings
only, so the value being sorted on cannot be read back.

for u in true false; do
  curl -s -H "X-Api-Key: $KEY" \\
    "https://ratings-api.uschess.org/api/v2/members?SortBy=Rating&Dir=Desc&Ranked=true&UsePeak=$u&Size=5" \\
    | jq -c "\[.items\[\] | {n:.lastName, r:(.ratings\[\]|select(.ratingSystem==\\"R\\")|.rating)}\]"
done
# true : Caruana 2870, SO 2837, NAKAMURA 2849, Aronian 2795, DOMINGUEZ 2782
# false: Caruana 2870, NAKAMURA 2849, SO 2837, LE 2799,      Aronian 2795

With UsePeak=true the list is no longer ordered by any returned field.

I assume that there is some concern that bots will overload the system. (It sounds like scraping MSA pages is already a problem). If API keys might get deactivated from what is seen as abuse, then you clearly would not want to have anything hard-coded that might be “leaked”.

Yes, the MSA system would basically lock up several times a week due to what I would attribute as scraping activities based on the logs, if I had to guess it was clubs updating their local records for their members. There’s also a nearly omnipresent level of spiders indexing the site. So when you add TDs and registration programs getting ready for upcoming events to all that, it was building up a huge backlog of SQL requests that would result in requests to that server (www.uschess.org) timing out.

I don’t know if we will have the same types of problems with MUIR and I don’t currently have access to the logs to check on that. We can, of course, increase the resources to the MUIR server, but that comes with a price tag.

Thanks, Russell, I will pass these along to the developers.

The initial goal with API 2.0 was to restore the ability for pairing and registration programs to access published ratings (eg, supplements) and membership data.

Accessing current ratings is a bit more problematical, in part because unofficial ratings change frequently and any new events are likely to be out of sequence until the next rerate. (We are doing a 3 or 4 month rolling rerate every night and now a 4 year one once a week and ahead of the supplement.)

Also, official US Chess policy is that published ratings are preferred over unofficial ratings and there has been some reluctance to actively encourage and facilitate the use of unofficial ratings for events.

Accessing tournament data like crosstables via an API is likely to require upgrading to a higher tier key, and the details for the higher levels have not been fully discussed yet. There will probably need to be a signed data usage agreement and there may be an access fee to help cover the costs of providing higher bandwidth access.

At some point, the existing 1.0 APIs will either be cranked down to very low load limits or discontinued for public use.

Players, clubs and TDs should also be aware that we are working on plans to move the data on several servers, including www.uschess.org, over to an archive server in the near future (possibly in September) at which point the MSA pages will have new URLs, and will ultimately go away once we’ve replaced all their functionality, probably in early 2027.

And here’s an early heads up on some planned system down time. We are looking to move the MUIR system to a different server account (with some cost savings) and this will likely require shutting down MUIR for most of a workday and assigning new IP addresses to affected servers. Also, the membership system development team wants to use that shutdown to do some upgrades, so large parts of the uschess.org website are likely to be down. This will probably be some time in September, either during the first week or the week of the 22nd.

@nolan The API 2.0 “Try it Out” buttons don’t work, somewhat funnily because they don’t include a dummy API key or a bypass.

They work fine if you start by registering an API key and enter it then hit ‘authenticate’.

The authentication happens at the server end, there’s no way to bypass it on a demo page.

I think “Authorize”, but that makes sense; I didn’t notice that button the first time around.

This…seems wrong; the server could check if the origin was the demo page, for the most obvious option.

It’s not best practice to build bypass mechanisms in authentication procedures, and it might make it possible for non-members to use the member-only APIs.

That’s not really possible. The request comes from your browser. It’s easy to make a request that looks exactly like the one your browser makes from that page.

As I recall, it even gives the curl command to generate that query on systems that support curl. (Linux, not sure what else.)

I think everything supports curl by this point…but the curl exemplar I was looking at did not include the X-Api-Key header required. I did add that on my own command line for testing but the browser didn’t work.

Wild. I had assumed my browser was triggering a server side request, but in retrospect yeah not sure how that could possibly work without a request starting from my browser.