⛳ An unofficial wrapper for the GHIN api
This TypeScript library provides a convenient and easy-to-use API wrapper for accessing the Golfer Handicap Index Network (GHIN) api unofficially. It allows you to interact with GHIN data, retrieve golfer handicaps, scores, and perform various operations related to golf handicaps.
This library is a client only — it ships no GHIN code or data, and it does not bypass authentication. You must supply your own credentials.
It also implements no part of the World Handicap System — no handicap formula, no Course Rating System. Handicap indexes and course/playing handicaps are returned by the USGA's centralized computation service; this library only transports them.
Bring credentials you are authorized to use. Vendor access is granted through the USGA's Golfer Product Access (GPA) program, under a GPA and API agreement between the vendor and the USGA — that agreement, not the consumer GHIN Terms of Service, governs API use, and each account carries its own GPA entitlements. If you aren't a GPA Technology Vendor, confirm your intended use is permitted before pointing this at the API. Your compliance with USGA's terms is your responsibility.
This is a fork of n8io/ghin, originally created by Nate Clark. This fork includes additional features and improvements.
- Retrieve golfer handicap information.
- Retrieve course and playing handicaps for golfers.
- Search for golfers by name, ID, or other criteria.
- Access golfer scoring history.
- And more!
To use this library in your TypeScript project, you can install it via npm or bun:
npm install @spicygolf/ghin
# or
bun add @spicygolf/ghinHere's a quick example of how to use this library:
import { GhinClient } from '@spicygolf/ghin'
// Initialize the client. The constructor is the one part of the surface that
// still throws — a bad config is a boot-time programmer error, not a runtime
// API failure, so it raises `ConfigurationError` rather than returning a Result.
const ghin = new GhinClient({
password: process.env.GHIN_PASSWORD,
username: process.env.GHIN_USERNAME,
})
// Get a golfer's handicap. Any membership status: an inactive or lapsed member
// has a readable index and comes back here, so read `status` off the record if
// that matters to you. `undefined` means no usable row came back — an unknown
// GHIN number, or one dropped by the schema; wire `onDegraded` to tell them apart.
const ghinNumber = 1234567
const result = await ghin.handicaps.getOne(ghinNumber)
// Every method resolves to a `Result<T, GhinError>` and never rejects, so
// check the error case before touching the value.
if (result.isErr()) {
// a `GhinError` — `code`, `message`, `cause`, and `statusCode` when the
// failure came off the wire
console.error(result.error.code, result.error.statusCode, result.error.message)
} else {
// `undefined` when no golfer matches, and `handicap_index` is `null`
// for a golfer with no established index (GHIN sends `"NH"`, or `"WD"` when withdrawn, on the wire)
const golfer = result.value
console.log(`Golfer ${ghinNumber} has a handicap of ${golfer?.handicap_index}`)
}golfers.getMany fetches a list of GHIN numbers in one batched call instead of
one call per golfer — useful for a picker list, or a nightly Handicap Index
refresh, against GHIN's per-account rate limit.
const result = await ghin.golfers.getMany([1234567, 7654321])
if (result.isOk()) {
const { golfers, missing } = result.value
console.log(`${golfers.length} found, ${missing.length} unknown to GHIN`)
}
// Only golfers whose record moved since a date — a delta refresh
const changed = await ghin.golfers.getMany(storedGhinNumbers, { updated_since: '2026-08-01' })Three things it handles that a raw golfers.search leaves to you:
- GHIN pages by row, not by golfer. A golfer appears once per club
affiliation, so 100 GHIN numbers can be ~180 rows and there is no total to
page against.
getManybatches the numbers and pages each batch to exhaustion — 120 GHIN numbers is 3 HTTP calls, and 12 is one. - Multi-club golfers arrive more than once. Rows are deduplicated to one per GHIN number, preferring the golfer's home club. The handicap fields are identical across a golfer's rows; only the club differs.
- Unknown GHIN numbers vanish silently. They come back in
missingrather than simply not being there.statusdefaults to'Active', so inactive golfers land inmissingtoo — as does anyone whose only rows failed the schema, so wireonDegradedif you need to tell that apart. Pass{ status: null }to cover every status in one call — that omits the parameter, which is GHIN's "both". ('All'is not: measured againstapi-uaton 2026-09-02, golfer 2890015 returns 0 rows forstatus=Activeandstatus=All, and 3 rows forstatus=Inactive, nostatusat all, or an emptystatus=.)getOnenever filters.
golfers is ordered to match the numbers you asked for, with duplicates
collapsed. An empty list is a ValidationError, not an empty result.
Every method on GhinClient returns Promise<Result<T, GhinError>> from
neverthrow — already a dependency of
this package, so there is nothing extra to install — and webhooks.iterateUndelivered
yields one Result per envelope. None of them reject; a failure arrives as an Err
carrying a GhinError subclass (AuthenticationError, NetworkError,
RateLimitError, ValidationError, CacheError) with code, statusCode and
cause intact, plus retryAfter on RateLimitError and field on
ValidationError. new GhinClient(config) is the sole exception and
still throws ConfigurationError.
"Not found" is an Ok, not an Err. handicaps.getOne and golfers.getOne
return ok(undefined) when no golfer matches the GHIN number — "no such
golfer" is a normal answer from GHIN, not a failure — so result.isErr()
stays false and the undefined shows up in result.value. This is the one place
a reader might reasonably expect an Err; treat ok(undefined) as the not-found
signal.
- 🔑 Add client authentication
- ♻️ Add client token auto-refresh
- 💸 Add configurable cache client
- ✨ Add golfer search
- ✨ Add golfer scores fetching
- ✨ Add course handicap fetching
- 💄 Enforce code style for consistency
- ✨ Add course search
- ✨ Add course details fetching
- 🧪 Test coverage all the things
- 📘 Autogenerated documentation
We welcome contributions from the community. If you'd like to contribute to this project, please follow these steps:
- Fork the repository.
- Create a new branch for your feature or bug fix.
- Make your changes and write tests if applicable.
- Commit your changes and push them to your fork.
- Open a pull request to the main repository.
MIT License
Copyright (c) 2023 Nate Clark
Copyright (c) 2025 Brad Anderson
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.