GPS cart integration hooks

OpenTeeSheet does not sell a cart GPS system, and no cart vendor is integrated yet. What it has are the two hooks a cart system, or a course's own bridge script, plugs into: round events going out, and cart reports coming in. Both run on every plan.

1. Round events (outbound webhooks)

Subscribe from the dashboard (Settings, Webhooks) or with POST /api/v1/hooks and {"events": ["round.checked_in", "round.turn", "round.ended", "round.marshal_message"]}. Every delivery is signed with X-OTS-Signature: t=<unix>,v1=<HMAC-SHA256 of "t.body">, queued, and retried with backoff. Events are picked up from the course's event log on each delivery run (about every ten minutes), so a check-in from any screen or the API fires the hook, with up to one run of delay.

EventWhen
round.checked_inThe starter checks the group in, with the cart ids it is taking. The round's clock starts here: teed_at is the later of the tee time and the check-in
round.turnAn 18-hole group checks in at the turn
round.endedThe group finishes
round.marshal_messageThe ranger flags the group slow or sends it a message: show it on the cart screen

The body carries the tee time's booking, plus a round block (cart_ids, holes, tee, checked_in_at, teed_at, turn_at, finished_at, last_hole) and, on round.marshal_message, a marshal block (action, note, behind_min, hole_estimate).

{
  "event": "round.checked_in",
  "sent_at": "2026-11-01T14:02:11.000Z",
  "booking": { "id": "6f1c…", "status": "confirmed", "start_time": "…", "players": 4 },
  "venue": { "id": "…", "name": "…", "slug": "…" },
  "bay": { "id": "…", "name": "1st tee" },
  "round": {
    "cart_ids": ["12", "14"],
    "holes": 18,
    "tee": 1,
    "checked_in_at": "2026-11-01T14:02:10.000Z",
    "teed_at": "2026-11-01T14:10:00.000Z",
    "turn_at": null,
    "finished_at": null,
    "last_hole": null
  },
  "marshal": null
}

2. Cart reports (inbound)

POST /api/v1/carts/reports with the venue owner's API key (write scope, created on your account page). Send one report or {"reports": [...]} with up to 50. Each report has a cart_id (the number the starter entered at check-in), and a hole (1-18), a position (lat + lng), or both. Optional: booking_id to match a booking directly, and reported_at (ISO 8601, never in the future; default now).

curl -X POST https://openteesheet.com/api/v1/carts/reports \
  -H "Authorization: Bearer ots_sk_…" -H "Content-Type: application/json" \
  -d '{ "reports": [ { "cart_id": "14", "hole": 6, "lat": 38.9012, "lng": -77.0301, "reported_at": "2026-11-01T16:01:00Z" } ] }'

→ { "accepted": 1, "matched": 1, "results": [ { "cart_id": "14", "booking_id": "6f1c…", "matched": true } ] }

A report is matched to the group on the course (checked in today, not finished) that took that cart. Every report is stored; a matched one moves the group's hole on the ranger view. A 400 names the bad field; 401 means no key; 403 means a read-only key or a key that is not a venue owner's.

How the ranger view uses it

With a cart report no older than 15 minutes, the ranger view shows the reported hole and how many minutes the group is past the course's target time for finishing it (the tee sheet's round target). Without one, it shows the starter's estimate from measured pace (minutes out against the target, adjusted by the group's own front nine once it checks in at the turn), and says that it is an estimate.