{"openapi":"3.1.0","info":{"title":"Golf Intelligence API","version":"0.1.0","description":"Tee time search, booking, and course intelligence for Ontario golf courses."},"components":{"schemas":{},"parameters":{}},"paths":{"/health":{"get":{"tags":["System"],"responses":{"200":{"description":"Service health","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ok","degraded"]},"timestamp":{"type":"string"},"db":{"type":"boolean"}},"required":["status","timestamp","db"]}}}}}}},"/admin/prune":{"post":{"tags":["System"],"responses":{"200":{"description":"Pruning results","content":{"application/json":{"schema":{"type":"object","properties":{"teeTimesPruned":{"type":"number"},"idempotencyKeysPruned":{"type":"number"},"intentsExpired":{"type":"number"}},"required":["teeTimesPruned","idempotencyKeysPruned","intentsExpired"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/admin/scan-runs":{"get":{"tags":["System"],"responses":{"200":{"description":"Last 50 scheduler cycles","content":{"application/json":{"schema":{"type":"object","properties":{"runs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"provider":{"type":"string"},"startedAt":{"type":"string"},"finishedAt":{"type":"string","nullable":true},"durationMs":{"type":"number","nullable":true},"rowsUpserted":{"type":"number"},"cbTriggers":{"type":"number"},"errorCount":{"type":"number"},"status":{"type":"string"},"notes":{"nullable":true}},"required":["id","provider","startedAt","finishedAt","durationMs","rowsUpserted","cbTriggers","errorCount","status"]}}},"required":["runs"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/courses":{"get":{"tags":["Courses"],"parameters":[{"schema":{"type":"number","nullable":true,"minimum":-90,"maximum":90},"required":false,"name":"lat","in":"query"},{"schema":{"type":"number","nullable":true,"minimum":-180,"maximum":180},"required":false,"name":"lng","in":"query"},{"schema":{"type":"number","minimum":1,"maximum":200,"default":50},"required":false,"name":"radius_km","in":"query"}],"responses":{"200":{"description":"List of courses, optionally filtered by proximity","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"lat":{"type":"number"},"lng":{"type":"number"},"bookingSystem":{"type":"string"},"bookingUrl":{"type":"string"},"integrationTier":{"type":"string"},"isGolfNorth":{"type":"boolean"},"distanceKm":{"type":"number","nullable":true},"weekdayRackRate":{"type":"number","nullable":true},"weekendRackRate":{"type":"number","nullable":true},"cartRate":{"type":"number","nullable":true},"coordConfirmed":{"type":"boolean"}},"required":["id","name","slug","lat","lng","bookingSystem","bookingUrl","integrationTier","isGolfNorth","distanceKm","weekdayRackRate","weekendRackRate","cartRate","coordConfirmed"]}}}}}}}},"/courses/{slug}":{"get":{"tags":["Courses"],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"slug","in":"path"}],"responses":{"200":{"description":"Course details","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"lat":{"type":"number"},"lng":{"type":"number"},"bookingSystem":{"type":"string"},"bookingUrl":{"type":"string"},"teeOnPortalSlug":{"type":"string","nullable":true},"isGolfNorth":{"type":"boolean"},"integrationTier":{"type":"string"},"browserAutomationTier":{"type":"string","nullable":true},"weekdayRackRate":{"type":"number","nullable":true},"weekendRackRate":{"type":"number","nullable":true},"cartRate":{"type":"number","nullable":true},"walkingPermitted":{"type":"boolean"},"successRate":{"type":"number","nullable":true},"circuitBreaker":{"type":"string"}},"required":["id","name","slug","lat","lng","bookingSystem","bookingUrl","teeOnPortalSlug","isGolfNorth","integrationTier","browserAutomationTier","weekdayRackRate","weekendRackRate","cartRate","walkingPermitted","successRate","circuitBreaker"]}}}},"404":{"description":"Course not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/courses/{id}/rating":{"get":{"tags":["Courses"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Course rating stats (min/max across all tees)","content":{"application/json":{"schema":{"type":"object","properties":{"slopeMin":{"type":"number","nullable":true},"slopeMax":{"type":"number","nullable":true},"ratingMin":{"type":"number","nullable":true},"ratingMax":{"type":"number","nullable":true},"yardageMin":{"type":"number","nullable":true},"yardageMax":{"type":"number","nullable":true}},"required":["slopeMin","slopeMax","ratingMin","ratingMax","yardageMin","yardageMax"]}}}}}}},"/intents":{"post":{"tags":["Intents"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"creatorName":{"type":"string","minLength":1,"maxLength":50,"description":"Display name of the round's organizer (the human this agent represents)."},"groupSize":{"type":"integer","minimum":1,"maximum":4,"default":4,"description":"Total number of players the round must accommodate (1–4)."},"message":{"type":"string","maxLength":200,"description":"Optional one-line note shared with the other participants on the join screen."},"lat":{"type":"number","minimum":-90,"maximum":90,"description":"Organizer's home latitude in WGS84 degrees."},"lng":{"type":"number","minimum":-180,"maximum":180,"description":"Organizer's home longitude in WGS84 degrees."},"radiusKm":{"type":"integer","minimum":1,"maximum":200,"default":50,"description":"Search radius in kilometres around (lat, lng). Default 50, max 200."},"expiresInHours":{"type":"number","minimum":1,"maximum":168,"default":24,"description":"Hours after which the intent auto-expires if it never reaches consensus. Default 24, max 168 (one week)."}},"required":["creatorName","lat","lng"]}}}},"responses":{"201":{"description":"Intent created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"joinUrl":{"type":"string"},"creatorToken":{"type":"string"}},"required":["id","joinUrl","creatorToken"]}}}}}},"get":{"tags":["Intents"],"responses":{"200":{"description":"User intents","content":{"application/json":{"schema":{"nullable":true}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/intents/{id}":{"get":{"tags":["Intents"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Intent details","content":{"application/json":{"schema":{"nullable":true}}}},"404":{"description":"Not found"}}}},"/intents/{id}/constraints":{"post":{"tags":["Intents"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"playerName":{"type":"string","minLength":1,"maxLength":50,"description":"Display name of the participant submitting these constraints."},"dates":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","format":"date","description":"YYYY-MM-DD calendar date."},"earliestTime":{"type":"string","pattern":"^\\d{2}:\\d{2}$","description":"HH:MM (24h) earliest acceptable tee time on this date."},"latestTime":{"type":"string","pattern":"^\\d{2}:\\d{2}$","description":"HH:MM (24h) latest acceptable tee time on this date."}},"required":["date","earliestTime","latestTime"]},"minItems":1,"maxItems":14,"description":"Up to 14 calendar dates with per-date HH:MM windows."},"maxPriceCents":{"type":"integer","minimum":0,"description":"Optional ceiling on per-player green-fee price, in CAD cents."},"holesPreference":{"anyOf":[{"type":"number","enum":[9]},{"type":"number","enum":[18]}],"description":"Either 9 or 18; omit if the participant has no preference."},"cartRequired":{"type":"boolean","default":false,"description":"Set true if the participant requires a power cart at the course."}},"required":["playerName","dates"]}}}},"responses":{"200":{"description":"Constraints saved"},"400":{"description":"Bad request"}}}},"/intents/{id}/resolve":{"post":{"tags":["Intents"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Resolved options"},"400":{"description":"Bad request"}}}},"/intents/{id}/vote":{"post":{"tags":["Intents"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"playerName":{"type":"string","minLength":1,"maxLength":50,"description":"Display name of the voting participant."},"optionId":{"type":"string","format":"uuid","description":"UUID of the resolved option from resolveOptions."}},"required":["playerName","optionId"]}}}},"responses":{"200":{"description":"Vote recorded"},"400":{"description":"Bad request"}}}},"/intents/{id}/notify/creator":{"post":{"tags":["Intents"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"channel":{"type":"string","enum":["email"]},"destination":{"type":"string","maxLength":320,"format":"email"}},"required":["channel","destination"]},{"type":"object","properties":{"channel":{"type":"string","enum":["sms"]},"destination":{"type":"string","pattern":"^\\+[1-9]\\d{1,14}$"}},"required":["channel","destination"]}]}}}},"responses":{"200":{"description":"Subscribed (or already subscribed — idempotent)"},"400":{"description":"Bad request"},"401":{"description":"Missing or invalid creator token"}}}},"/intents/{id}/notify/player":{"post":{"tags":["Intents"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"playerName":{"type":"string","minLength":1,"maxLength":50}},"required":["playerName"]},{"oneOf":[{"type":"object","properties":{"channel":{"type":"string","enum":["email"]},"destination":{"type":"string","maxLength":320,"format":"email"}},"required":["channel","destination"]},{"type":"object","properties":{"channel":{"type":"string","enum":["sms"]},"destination":{"type":"string","pattern":"^\\+[1-9]\\d{1,14}$"}},"required":["channel","destination"]}]}]}}}},"responses":{"200":{"description":"Subscribed (or already subscribed — idempotent)"},"400":{"description":"Bad request"},"401":{"description":"Missing or invalid player token, or no constraints on record"}}}},"/intents/{id}/options/{optionId}/verify":{"post":{"tags":["Intents"],"summary":"Verify a single option against the live platform","description":"Step 6 verify-at-handoff. Resolves the option to a per-platform verify helper and reports current availability. Result is cached in-memory for 30s so a pre-fetch on winner page render and the user click reuse the same verification rather than firing two HTTP calls. Single-replica cache assumption.","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","format":"uuid"},"required":true,"name":"optionId","in":"path"},{"schema":{"type":"string","enum":["click","prefetch"]},"required":false,"name":"reason","in":"query"}],"responses":{"200":{"description":"Verify result. `available` is the headline. `reason` carries the failure mode when not available. `alternative` is populated only when the primary is unavailable AND the same-course waterfall found a substitute within ±30 min and ≤ original price.","content":{"application/json":{"schema":{"type":"object","properties":{"optionId":{"type":"string","format":"uuid"},"available":{"type":"boolean"},"verifiedAt":{"type":"string"},"reason":{"type":"string","enum":["gone","capacity_full","out_of_window","http_error","platform_unsupported"]},"alternative":{"type":"object","properties":{"teeTimeId":{"type":"string","format":"uuid"},"courseId":{"type":"string","format":"uuid"},"date":{"type":"string"},"time":{"type":"string"},"holes":{"anyOf":[{"type":"number","enum":[9]},{"type":"number","enum":[18]}]},"bookingUrl":{"type":"string"},"priceCents":{"type":"number"},"verifiedAt":{"type":"string"}},"required":["teeTimeId","courseId","date","time","holes","bookingUrl","priceCents","verifiedAt"]}},"required":["optionId","available","verifiedAt"]}}}},"404":{"description":"Option not found or not in this intent","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/intents/{id}/re-resolve":{"post":{"tags":["Intents"],"summary":"Trigger a fresh resolve, replacing existing options","description":"Recovery path for when verify-at-handoff catches drift AND the same-course waterfall finds no alternative. Any participant with a valid X-Player-Token can trigger; concurrent triggers idempotently land on the same in-flight resolve.","parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"`status: started` — this caller flipped the state and a fresh resolveIntent is firing. `status: in_progress` — another participant already triggered re-resolve; the desired outcome (fresh options) is in flight. Both shapes trigger the same client behavior: poll the intent until status returns to `voting`.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["started","in_progress"]}},"required":["status"]}}}},"403":{"description":"X-Player-Token missing or invalid","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"404":{"description":"Intent not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"409":{"description":"Intent is in a terminal state (expired or cancelled)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"status":{"type":"string","enum":["expired","cancelled"]}},"required":["error","status"]}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/receipts/{intentId}":{"get":{"tags":["Receipts"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"intentId","in":"path"}],"responses":{"200":{"description":"Receipts for the intent","content":{"application/json":{"schema":{"nullable":true}}}}}}},"/receipts/{intentId}/{receiptId}":{"get":{"tags":["Receipts"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"intentId","in":"path"},{"schema":{"type":"string","format":"uuid"},"required":true,"name":"receiptId","in":"path"}],"responses":{"200":{"description":"Single receipt","content":{"application/json":{"schema":{"nullable":true}}}},"404":{"description":"Not found"}}}},"/unsubscribe/{token}":{"get":{"tags":["Unsubscribe"],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":512},"required":true,"name":"token","in":"path"}],"responses":{"302":{"description":"Redirect to /unsubscribed landing page on success"},"400":{"description":"Invalid or expired token"}}}},"/a2a":{"post":{"tags":["A2A"],"description":"JSON-RPC 2.0 endpoint for the A2A interface declared in /.well-known/agent-card.json. Accepts the five declared methods (createIntent, submitConstraints, resolveOptions, castVote, bookingHandoff). Always returns HTTP 200; protocol errors are encoded in the JSON-RPC error envelope.","responses":{"200":{"description":"JSON-RPC 2.0 response (success or error)","content":{"application/json":{"schema":{"nullable":true}}}}}}},"/actions/create-intent":{"post":{"tags":["Actions"],"operationId":"createGolfIntent","description":"Create an anonymous Ontario golf coordination intent after collecting group size, rough Ontario location, and date context. This does not book a tee time.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"creatorName":{"type":"string","minLength":1,"maxLength":50,"description":"Display name of the round's organizer (the human this agent represents)."},"groupSize":{"type":"integer","minimum":1,"maximum":4,"default":4,"description":"Total number of players the round must accommodate (1–4)."},"message":{"type":"string","maxLength":200,"description":"Optional one-line note shared with the other participants on the join screen."},"lat":{"type":"number","minimum":-90,"maximum":90,"description":"Organizer's home latitude in WGS84 degrees."},"lng":{"type":"number","minimum":-180,"maximum":180,"description":"Organizer's home longitude in WGS84 degrees."},"radiusKm":{"type":"integer","minimum":1,"maximum":200,"default":50,"description":"Search radius in kilometres around (lat, lng). Default 50, max 200."},"expiresInHours":{"type":"number","minimum":1,"maximum":168,"default":24,"description":"Hours after which the intent auto-expires if it never reaches consensus. Default 24, max 168 (one week)."},"province":{"type":"string","enum":["ON"],"description":"ChatGPT-facing Ontario-only guardrail."},"country":{"type":"string","enum":["CA"],"description":"ChatGPT-facing Canada-only guardrail."}},"required":["creatorName","lat","lng","province","country"]}}}},"responses":{"200":{"description":"Intent created","content":{"application/json":{"schema":{"nullable":true}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"number"},"details":{"nullable":true}},"required":["error"]}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"number"},"details":{"nullable":true}},"required":["error"]}}}}}}},"/actions/submit-constraints":{"post":{"tags":["Actions"],"operationId":"submitPlayerConstraints","description":"Submit one player's availability and preferences for an existing anonymous intent. Call once per player.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"playerName":{"type":"string","minLength":1,"maxLength":50,"description":"Display name of the participant submitting these constraints."},"dates":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","format":"date","description":"YYYY-MM-DD calendar date."},"earliestTime":{"type":"string","pattern":"^\\d{2}:\\d{2}$","description":"HH:MM (24h) earliest acceptable tee time on this date."},"latestTime":{"type":"string","pattern":"^\\d{2}:\\d{2}$","description":"HH:MM (24h) latest acceptable tee time on this date."}},"required":["date","earliestTime","latestTime"]},"minItems":1,"maxItems":14,"description":"Up to 14 calendar dates with per-date HH:MM windows."},"maxPriceCents":{"type":"integer","minimum":0,"description":"Optional ceiling on per-player green-fee price, in CAD cents."},"holesPreference":{"anyOf":[{"type":"number","enum":[9]},{"type":"number","enum":[18]}],"description":"Either 9 or 18; omit if the participant has no preference."},"cartRequired":{"type":"boolean","default":false,"description":"Set true if the participant requires a power cart at the course."},"intentId":{"type":"string","format":"uuid","description":"The intent UUID returned by createIntent."},"deviceToken":{"type":"string","description":"Per-participant device token returned from a previous submitConstraints call. Omit on first submission; include to re-identify across calls."}},"required":["playerName","dates","intentId"]}}}},"responses":{"200":{"description":"Constraints saved","content":{"application/json":{"schema":{"nullable":true}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"number"},"details":{"nullable":true}},"required":["error"]}}}},"401":{"description":"Invalid player token","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"number"},"details":{"nullable":true}},"required":["error"]}}}},"404":{"description":"Intent not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"number"},"details":{"nullable":true}},"required":["error"]}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"number"},"details":{"nullable":true}},"required":["error"]}}}}}}},"/actions/resolve-options":{"post":{"tags":["Actions"],"operationId":"resolveTeeTimeOptions","description":"Resolve ranked tee-time options only after all known players have submitted constraints. Do not invent options beyond the returned list.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"intentId":{"type":"string","format":"uuid","description":"The intent UUID to resolve into ranked options."}},"required":["intentId"]}}}},"responses":{"200":{"description":"Resolved options","content":{"application/json":{"schema":{"nullable":true}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"number"},"details":{"nullable":true}},"required":["error"]}}}},"404":{"description":"Intent not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"number"},"details":{"nullable":true}},"required":["error"]}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"number"},"details":{"nullable":true}},"required":["error"]}}}}}}},"/actions/cast-vote":{"post":{"tags":["Actions"],"operationId":"castTeeTimeVote","description":"Cast one player vote for a tee-time option returned by Limlock. Do not claim the tee time is booked after voting.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"playerName":{"type":"string","minLength":1,"maxLength":50,"description":"Display name of the voting participant."},"optionId":{"type":"string","format":"uuid","description":"UUID of the resolved option from resolveOptions."},"intentId":{"type":"string","format":"uuid","description":"The intent UUID being voted on."},"deviceToken":{"type":"string","description":"Device token returned by submitConstraints, if available; ties this vote to the original participant identity."}},"required":["playerName","optionId","intentId"]}}}},"responses":{"200":{"description":"Vote recorded","content":{"application/json":{"schema":{"nullable":true}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"number"},"details":{"nullable":true}},"required":["error"]}}}},"401":{"description":"Invalid player token","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"number"},"details":{"nullable":true}},"required":["error"]}}}},"404":{"description":"Intent not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"number"},"details":{"nullable":true}},"required":["error"]}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"number"},"details":{"nullable":true}},"required":["error"]}}}}}}},"/actions/booking-handoff":{"post":{"tags":["Actions"],"operationId":"getBookingHandoff","description":"Return current intent state and external booking URLs after a winner exists or when the user asks for booking handoff. Limlock does not process payment.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"intentId":{"type":"string","format":"uuid","description":"The intent UUID whose winning option should be handed off to the course booking surface."}},"required":["intentId"]}}}},"responses":{"200":{"description":"Intent detail and booking handoff state","content":{"application/json":{"schema":{"nullable":true}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"number"},"details":{"nullable":true}},"required":["error"]}}}},"404":{"description":"Intent not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"number"},"details":{"nullable":true}},"required":["error"]}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"number"},"details":{"nullable":true}},"required":["error"]}}}}}}},"/jobs/freshen-active-courses":{"post":{"tags":["Jobs"],"description":"Cron-driven freshener for Tee-On courses currently in any active intent's radius. Runs the existing on-demand scanner against the prioritized active set (max 100 courses, 10-min skip-recent backpressure). Triggered every 20 min from outside; gated by X-Jobs-Secret header. Returns a JSON summary; observability also lands in scan_runs.","security":[{"JobsSecret":[]}],"responses":{"200":{"description":"Cron run completed (may be partial — `dayErrors` records per-day failures)","content":{"application/json":{"schema":{"nullable":true}}}},"401":{"description":"Missing or invalid X-Jobs-Secret","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"500":{"description":"Cron run threw an unexpected exception","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"503":{"description":"JOBS_SECRET not configured on this deployment","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/jobs/probe-chronogolf-ip":{"post":{"tags":["Jobs"],"description":"Diagnostic: fetches Railway outbound IP via ifconfig.me, then makes a naked Node fetch to a known-good chronogolf marketplace teetimes URL. Returns the IP + HTTP status + response-body preview + relevant CF headers. Used to confirm whether the 2026-05-18 403 cliff is source-IP-reputation (Railway IP flagged by CF Bot Management) or request-shape.","security":[{"JobsSecret":[]}],"responses":{"200":{"description":"Probe completed","content":{"application/json":{"schema":{"nullable":true}}}},"401":{"description":"Missing or invalid X-Jobs-Secret","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"500":{"description":"Probe threw an unexpected exception","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"503":{"description":"JOBS_SECRET not configured on this deployment","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/events":{"post":{"tags":["Events"],"requestBody":{"content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"event":{"type":"string","enum":["landing.cta.clicked"]},"metadata":{"type":"object","properties":{"to":{"type":"string","maxLength":200}},"additionalProperties":false}},"required":["event"]},{"type":"object","properties":{"event":{"type":"string","enum":["new.viewed"]},"metadata":{"type":"object","properties":{"hasGeoConsent":{"type":"string","enum":["granted","denied","pending"]}},"additionalProperties":false}},"required":["event"]},{"type":"object","properties":{"event":{"type":"string","enum":["join.viewed"]},"metadata":{"type":"object","properties":{"intentId":{"type":"string","format":"uuid"}},"required":["intentId"],"additionalProperties":false}},"required":["event","metadata"]},{"type":"object","properties":{"event":{"type":"string","enum":["book.clicked"]},"metadata":{"type":"object","properties":{"courseId":{"type":"string","format":"uuid"},"bookingSystem":{"type":"string","maxLength":50},"intentId":{"type":"string","format":"uuid"}},"required":["courseId","bookingSystem"],"additionalProperties":false}},"required":["event","metadata"]}]}}}},"responses":{"204":{"description":"Event recorded"},"400":{"description":"Invalid event name or metadata"}}}},"/auth/connect/{provider}":{"get":{"tags":["Auth"],"parameters":[{"schema":{"type":"string"},"required":true,"name":"provider","in":"path"},{"schema":{"type":"string","format":"uuid"},"required":true,"name":"courseId","in":"query"}],"responses":{"302":{"description":"Redirect to OAuth consent screen"},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/auth/callback/{provider}":{"get":{"tags":["Auth"],"parameters":[{"schema":{"type":"string"},"required":true,"name":"provider","in":"path"},{"schema":{"type":"string"},"required":true,"name":"code","in":"query"},{"schema":{"type":"string"},"required":true,"name":"state","in":"query"}],"responses":{"200":{"description":"OAuth callback success","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"provider":{"type":"string"},"courseId":{"type":"string"}},"required":["success","provider","courseId"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}},"/bookings/prepare":{"post":{"tags":["Bookings"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"courseId":{"type":"string","format":"uuid"},"date":{"type":"string","format":"date"},"time":{"type":"string","pattern":"^\\d{2}:\\d{2}$"},"players":{"type":"integer","minimum":1,"maximum":4},"playerDetails":{"type":"array","items":{"type":"object","properties":{"firstName":{"type":"string","minLength":1},"lastName":{"type":"string","minLength":1},"email":{"type":"string","format":"email"},"phone":{"type":"string"}},"required":["firstName","lastName","email"]},"minItems":1,"maxItems":4},"idempotencyKey":{"type":"string","format":"uuid"}},"required":["courseId","date","time","players","playerDetails","idempotencyKey"]}}}},"responses":{"200":{"description":"Booking prepared (quote returned)","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"courseId":{"type":"string","format":"uuid"},"date":{"type":"string"},"time":{"type":"string"},"players":{"type":"number"},"playerDetails":{"type":"array","items":{"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"}},"required":["firstName","lastName","email"]}},"status":{"type":"string"},"quotedPriceCents":{"type":"number","nullable":true},"method":{"type":"string","nullable":true},"confirmationCode":{"type":"string","nullable":true},"createdAt":{"type":"string"}},"required":["id","courseId","date","time","players","playerDetails","status","quotedPriceCents","method","confirmationCode","createdAt"]}}}},"404":{"description":"Course not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"}},"required":["error","code"]}}}},"409":{"description":"Conflict (idempotency key reused, circuit open, etc.)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"}},"required":["error","code"]}}}}}}},"/bookings/{id}/confirm":{"post":{"tags":["Bookings"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Booking confirmed","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"courseId":{"type":"string","format":"uuid"},"date":{"type":"string"},"time":{"type":"string"},"players":{"type":"number"},"playerDetails":{"type":"array","items":{"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"}},"required":["firstName","lastName","email"]}},"status":{"type":"string"},"quotedPriceCents":{"type":"number","nullable":true},"method":{"type":"string","nullable":true},"confirmationCode":{"type":"string","nullable":true},"createdAt":{"type":"string"}},"required":["id","courseId","date","time","players","playerDetails","status","quotedPriceCents","method","confirmationCode","createdAt"]}}}},"404":{"description":"Booking not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"}},"required":["error","code"]}}}},"409":{"description":"Invalid booking state","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string"}},"required":["error","code"]}}}}}}},"/bookings/{id}":{"get":{"tags":["Bookings"],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Booking details","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"courseId":{"type":"string","format":"uuid"},"date":{"type":"string"},"time":{"type":"string"},"players":{"type":"number"},"playerDetails":{"type":"array","items":{"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"}},"required":["firstName","lastName","email"]}},"status":{"type":"string"},"quotedPriceCents":{"type":"number","nullable":true},"method":{"type":"string","nullable":true},"confirmationCode":{"type":"string","nullable":true},"createdAt":{"type":"string"}},"required":["id","courseId","date","time","players","playerDetails","status","quotedPriceCents","method","confirmationCode","createdAt"]}}}},"404":{"description":"Booking not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}}}}}}}}