Bolt6 golf data integration¶
Bolt6 publishes golf ball positions during play. This package is everything you need to integrate: send us the scorecard and stroke state, receive ball, pin and tee positions.
Who provides what¶
| Scoring Provider | Ball Positioning Provider | |
|---|---|---|
| Tournament and round structure | ✔ | |
| Course, holes, par | ✔ | |
| Groups, players | ✔ | |
| Strokes, and stroke state if you have it | ✔ | |
| Ball positions | ✔ | |
| Pin and tee positions | ✔ |
Distances — to the pin, travelled, to the fairway edge — you compute from the positions we publish. They are latitude/longitude; over a course, a flat local approximation is all it takes: Computing distances.
Package contents¶
| File | What it is |
|---|---|
| this page | overview, what you'll need, quickstart, checklist |
| Integration guide | every use case end to end, with request/response examples |
| Reference | normative semantics: coordinates, units, time, delivery, auth, errors |
partner-schema.gql |
the contract, as GraphQL SDL |
| Operations | ready-to-use operation documents |
Read this page, then the Integration guide. Use the Reference to settle details.
Five things worth knowing up front¶
These are easier to design around now than to retrofit later.
- Ball coordinates can be null. When we know the ball is in the left greenside bunker but have
not yet fixed its coordinates, you get
state: ZONED, a populatedsurface, andlat/lon/elevationall null. This is the normal state of every stroke before the ball is located — not an error, not rare. See Zone-only positions. - Coordinates are WGS84 latitude/longitude, with
elevationin metres. See Coordinates. - Everything is metric. Elevation, and the distances you compute from positions, are metres.
- Positions get corrected and withdrawn. There is one position per stroke; a correction arrives
as the same
strokeIdwith a higherrevision, a withdrawal withretracted: true. Both arrive on the samestrokeId, so a single handler covers them. See Corrections and retractions. - You hold the cursor. We keep no per-consumer delivery state. Persist the highest
revisionyou have processed and resume from it. See Delivery.
What you'll need¶
- A GraphQL client that supports subscriptions over the
graphql-transport-wsWebSocket subprotocol. Widely available:graphql-ws(JS/TS),gqlwithwebsockets(Python), Apollo, urql. - Credentials. We issue you a bearer JWT during onboarding.
- Your own stable ids for tournament, course, round, group, player and stroke, sent as
providerIdwhen you create each one, and used to refer to it from then on. Use whatever key space you already have. Creation is an idempotent upsert onproviderId, so retries are safe. See Ids. - Durable storage for one integer per round — the last
revisionyou processed.
Endpoint¶
wss://<tour>.hasura.bolt6.cloud/v1/graphql subscriptions
https://<tour>.hasura.bolt6.cloud/v1/graphql queries and mutations
We'll give you the exact host for your tour. One deployment per tour, so there is no tenant field on the wire.
Authenticate with Authorization: Bearer <jwt> on both. Details in
Authentication.
Quickstart¶
The fastest end-to-end check is to create a tournament and read it back — it exercises authentication, a write and a read in two calls.
curl -s https://<tour>.hasura.bolt6.cloud/v1/graphql \
-H "Authorization: Bearer $BOLT6_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"query": "mutation($input: TournamentInput!) { upsertTournament(input: $input) { accepted message id } }",
"variables": { "input": { "providerId": "T-2026-07", "name": "Sydney Invitational", "startDate": "2026-03-12", "endDate": "2026-03-15" } }
}'
{ "data": { "upsertTournament": { "accepted": true, "message": null, "id": 42 } } }
That is the pattern for the whole integration: you name everything by your own id, and refer to it by that same id from then on. Read it back:
curl -s https://<tour>.hasura.bolt6.cloud/v1/graphql \
-H "Authorization: Bearer $BOLT6_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"query": "query($id: String!) { tournaments(where: {providerId: {_eq: $id}}) { providerId name startDate endDate } }",
"variables": { "id": "T-2026-07" }
}'
{ "data": { "tournaments": [ { "providerId": "T-2026-07", "name": "Sydney Invitational", "startDate": "2026-03-12", "endDate": "2026-03-15" } ] } }
The rest of setup follows the same pattern — course, then rounds, then groups — and can go in a single request (Integration guide).
Once play starts, positions look like this:
curl -s https://<tour>.hasura.bolt6.cloud/v1/graphql \
-H "Authorization: Bearer $BOLT6_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"query": "query($round: String!, $hole: Int!) { ballPositions(where: {round: {providerId: {_eq: $round}}, hole: {_eq: $hole}}) { revision strokeId strokeProviderId player { providerId } hole strokeNum lat lon elevation surface state retracted observedAt } }",
"variables": { "round": "R-3", "hole": 7 }
}'
{
"data": {
"ballPositions": [
{
"revision": 48122,
"strokeId": 88301,
"strokeProviderId": "S-8E8E254E",
"player": { "providerId": "P-231" },
"hole": 7,
"strokeNum": 2,
"lat": -33.897422,
"lon": 151.218017,
"elevation": 30.85,
"surface": "FWY",
"state": "VERIFIED",
"retracted": false,
"observedAt": "2026-03-14T20:16:03.076+00:00"
},
{
"revision": 48125,
"strokeId": 88307,
"strokeProviderId": "S-7A1C90D2",
"player": { "providerId": "P-244" },
"hole": 7,
"strokeNum": 2,
"lat": null,
"lon": null,
"elevation": null,
"surface": "GBK",
"state": "ZONED",
"retracted": false,
"observedAt": "2026-03-14T20:16:41.5+00:00"
}
]
}
}
Note the second row: a real ball, in a greenside bunker, with no coordinates yet. Your renderer will
meet this case often. And note the ids: strokeId is ours, strokeProviderId and player.providerId
are the ones you sent in upsertStroke and upsertGroup, so it joins to your records directly.
For live delivery use the ballPositionEvents subscription rather than polling this query —
Receiving ball positions.
Integration checklist¶
Setup, once:
- [ ] Credentials verified against the quickstart above
- [ ] Your provider ids stable and unique per entity
Before play, in this order:
- [ ]
upsertTournament - [ ]
upsertCourse— holes and par - [ ]
upsertRoundfor each round - [ ]
upsertGroupfor each group, with its players
During play:
- [ ]
upsertStrokeper stroke — one call atHIT, or one per status change if you have pre-shot events - [ ]
retractStrokefor anything reported in error - [ ]
ballPositionEventssubscribed, withafterRevisionfrom durable storage - [ ]
holeReferencessubscribed — pin positions change daily
Client correctness — the parts worth exercising before going live, since they rarely surface in testing:
- [ ] Null
lat/lon/elevationrender correctly - [ ]
retracted: trueremoves the position from display - [ ] Same
strokeIdwith a higherrevisionreplaces, not duplicates - [ ] Cursor persisted after processing, so a restart resumes rather than replays
- [ ]
accepted: falseis logged and not retried unchanged - [ ] Transport errors retried with backoff
- [ ] Round numbers ordered by
num, not assumed to be 1..4
Support¶
Contract version is in the header of partner-schema.gql. Sending it along with the round's providerId and a
revision is enough for us to find the exact records you saw.