UTC --:--:--
Docs·API reference·POST /v1/score

Score an activity at a place in a window

The core endpoint. One call returns a 0–100 score, a verdict, a per-dimension breakdown and the raw physical readings behind it.

POSThttps://api.goable.io/v1/score

The scoring engine is deterministic: given the same inputs and provider chain, the score is reproducible, and the audit log replays the call.

Overview

Send a location and an activity; an optional time window narrows the score to a specific range. Omit it and the engine scores the current forecast instant. Get back a single suitability score with its full breakdown. The verdict is one of excellent, favorable, fair, marginal, poor, not_feasible or unsafe. A universal hard gate (lightning ≥ 0.85 or hazardous AQI) forces unsafe regardless of the rest; a profile's own feasibility gate (e.g. no rideable wind) forces not_feasible instead — impossible, not dangerous.

Authentication

Include your API key in the X-Goable-Key header. See Authentication for key handling + rotation.

Request

locationrequired
object { lat, lng }
Spot coordinates in WGS-84 decimal degrees. Latitude [-90, +90], longitude [-180, +180]. Resolved against the bathymetry catalog for marine activities and the spot catalog for regional calibration.
activityrequired
enum string
One of the 28 base profiles or a regional variant: kitesurfing, surfing, windsurfing, sailing, scuba, ski-touring, freeride, paragliding, trail-running, climbing, …
window
object { from, to }
Optional. ISO-8601 UTC instants. When omitted, the engine scores the current forecast instant instead of a range. Forecast horizon is capped at ~16 days; beyond that, use POST /v1/score/historical (climatology) or POST /v1/projections (climate).
ensemble
boolean
Optional. Requests the probabilistic ensemble (30+ NWP members blended into a distribution) instead of a single-model score. Plan-gated to Pro and Scale.
members
integer · 2–50
Optional. Ensemble member count to request, capped by plan max and provider max. Only meaningful when ensemble is true.
rider_skill_level
enum string
Optional. One of beginner, intermediate, expert. Requests skill-conditioned scoring against the difficulty atlas. Plan-gated to Pro and Scale.

Response

score
integer · 0–100
Suitability score, monotonic, deterministic. 78 in Tarifa for a kitesurfing window means: better than 78% of comparable windows for that spot.
verdict
enum string
Seven-band scale, per the activity profile's verdict_buckets (every profile in the catalog uses this same set today): excellent 86–100 · favorable 71–85 · fair 51–70 · marginal 31–50 · poor 1–30 · not_feasible< 1 from a feasibility gate (impossible, e.g. no rideable wind — not dangerous) · unsafe< 1 from a safety gate (lightning, hazardous AQI, a profile's own safety threshold).
confidence
float · 0–1
Derived from forecast horizon, provider agreement, profile maturity, and (on Pro+) ensemble spread. See Confidence detail in the response for the full discriminated union.
breakdown
object
Per-dimension contributions to the score. Keys are profile-specific (kitesurfing: wind, wave, tide, thermal_breeze, sun, gust_factor). Values sum to ≤ 1.0.
physics
object
Raw physical readings with units. The numbers the score was computed from: wind in knots, wave height in meters, tide phase, CAPE, etc.
alerts
array · 0–N
Surfaced advisories: golden-hour windows, lightning proximity, AQI hazards, rip-current risk, freezing-level crossings, breaker classification (surf-family).

Code samples

Same call, two ways. The TypeScript SDK (@goable-io/sdk) is the most ergonomic; cURL works anywhere.

// npm install @goable-io/sdk
import { GoableClient } from "@goable-io/sdk"

const goable = new GoableClient({ apiKey: process.env.GOABLE_KEY! })

const { score, verdict, breakdown } = await goable.score({
location: { lat: 36.013, lng: -5.604 },
activity: "kitesurfing",
window: { from: "2026-05-23T09:00Z", to: "2026-05-23T18:00Z" },
})

if (verdict === "favorable") {
openBookingSlot(score)
}

Try it

Pick an activity, a spot and the option flags. Runs against the live engine (public demo tenant, 60 calls / hour / IP); falls back to a deterministic sample.

Try it · liveready
request
activity
spot
expected response

Display-only — the real /v1/score request has no option to omit fields; this just trims what's shown here.

{
"score": 76,
"verdict": "favorable",
"confidence": 0.75,
"breakdown": {
"wind": {
"contribution": 0.323,
"score": 77,
"hasData": true
},
"wave": {
"contribution": 0.137,
"score": 76,
"hasData": true
},
"tide": {
"contribution": 0.085,
"score": 85,
"hasData": true
},
"thermal_breeze": {
"contribution": 0.094,
"score": 78,
"hasData": true
},
"sun": {
"contribution": 0.062,
"score": 78,
"hasData": true
},
"gust_factor": {
"contribution": 0.077,
"score": 77,
"hasData": true
}
},
"physics": {
"wind": {
"value": 28,
"unit": "knots"
},
"wave": {
"value": 2.4,
"unit": "meters"
},
"period": {
"value": 6.8,
"unit": "seconds"
},
"tide": {
"phase": "falling"
}
},
"alerts": [],
"provider_chain": [
"stormglass",
"openmeteo-marine"
],
"eco": {
"metarValidation": {
"sourceType": "in_situ_observation",
"stationId": "LE81",
"sourceDistanceKm": 3.2,
"observedAt": "2026-05-23T08:42:13.952Z",
"wind": {
"validated": true,
"observedMs": 8.8,
"forecastMs": 11.1,
"overridden": false
}
},
"cmemsWaveValidation": {
"sourceType": "satellite_observation",
"validated": true,
"observedHeightM": 1.87,
"forecastHeightM": 0.73,
"overridden": false,
"sourceDistanceKm": 15.9
},
"sstValidation": {
"sourceType": "satellite_observation",
"validated": true,
"observedC": 14.7,
"forecastC": 20.3,
"overridden": true,
"sourceDistanceKm": 17
},
"tideObserved": {
"sourceType": "in_situ_observation",
"observedLevelM": 0.11,
"datum": "station_zero",
"stationId": "ioc-cadiz",
"sourceDistanceKm": 23.1,
"staleMinutes": 21,
"trend": "falling",
"rateMPerH": -0.13,
"rangePosition": 0.05
},
"buoyValidation": {
"sourceType": "in_situ_observation",
"stationId": "6200025",
"sourceDistanceKm": 20.9,
"observedAt": "2026-05-23T08:00:00.000Z",
"wave": {
"validated": true,
"observedM": 0.69,
"forecastM": 1.4,
"overridden": true
},
"sst": {
"validated": true,
"observedC": 18,
"forecastC": 24,
"overridden": false
},
"wind": {
"validated": true,
"observedMs": 7,
"forecastMs": 13.1,
"overridden": false
},
"observedWavePeriodS": 8.2,
"observedWaveDirectionDeg": 32
}
},
"computed_at": "2026-05-23T09:00:00.000Z"
}

Errors

422 · VALIDATION_ERROR
Zod validation
Payload failed schema validation. The issues field lists each failing path and reason.
401 · UNAUTHORIZED
missing / bad key
API key absent, malformed, or hash mismatch. Rotate from the tenant portal.
402 · PAYMENT_REQUIRED
plan gate
Endpoint requires a higher plan than the tenant currently holds. Ensemble + rider_skill_level gated to Pro and Scale.
404 · ACTIVITY_NOT_FOUND
unknown activity
No profile matches the requested activity slug at this location.
429 · RATE_LIMITED
safety cap
Per-tenant daily safety cap exceeded (DDoS guard, NOT the billing line; monthly quota + overage live in Stripe). Free 50/day · Starter 5k/day · Pro 50k/day · Scale ∞. Resets at 00:00 UTC.