{"openapi":"3.1.0","info":{"title":"SurfMap API","version":"1.0.0","description":"Surf spot database, 7-day hourly forecasts with 3-level ratings, and tide extremes for 5,877 named surf spots. JSON over HTTPS, metric units, UTC times. Weather inputs include Open-Meteo data (CC BY 4.0).","contact":{"email":"contact@surf-map.com","url":"https://surf-map.com/api/"}},"servers":[{"url":"https://api.surf-map.com"}],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"paths":{"/v1/spots":{"get":{"operationId":"searchSpots","summary":"Search spots by name, alias, or country","description":"Case- and diacritic-insensitive search. q is required (at least 2 characters); there is no unfiltered listing. Returns a bare JSON array.","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":2},"example":"hossegor"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":10}}],"responses":{"200":{"description":"Matching spots, best match first.","headers":{"$ref":"#/components/headers/RateLimitHeaders"},"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Spot"}}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/InvalidKey"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/spots/nearby":{"get":{"operationId":"spotsNearby","summary":"Find spots near a coordinate","description":"Spots within radius km of (lat, lng), sorted by distance. Returns a bare JSON array; each result includes distanceKm.","parameters":[{"name":"lat","in":"query","required":true,"schema":{"type":"number","minimum":-90,"maximum":90}},{"name":"lng","in":"query","required":true,"schema":{"type":"number","minimum":-180,"maximum":180}},{"name":"radius","in":"query","description":"Radius in km.","schema":{"type":"number","minimum":0,"maximum":100,"default":50}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":10}}],"responses":{"200":{"description":"Nearby spots, closest first.","headers":{"$ref":"#/components/headers/RateLimitHeaders"},"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SpotWithDistance"}}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/InvalidKey"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/spots/{id}":{"get":{"operationId":"getSpot","summary":"Get one spot","parameters":[{"$ref":"#/components/parameters/SpotId"}],"responses":{"200":{"description":"The spot.","headers":{"$ref":"#/components/headers/RateLimitHeaders"},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Spot"}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/spots/{id}/forecast":{"get":{"operationId":"getForecast","summary":"Hourly forecast with 3-level ratings","description":"Hourly conditions from the current UTC hour forward, refreshed twice a day. waveDirection is the primary swell direction (true wave direction is not persisted). tide is null for tideless spots. Responds 404 with code no_forecast when the spot exists but has no forecast document yet.","parameters":[{"$ref":"#/components/parameters/SpotId"},{"$ref":"#/components/parameters/Days"}],"responses":{"200":{"description":"Forecast envelope.","headers":{"$ref":"#/components/headers/RateLimitHeaders"},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForecastResponse"}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/InvalidKey"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/spots/{id}/tides":{"get":{"operationId":"getTides","summary":"Tide extremes","description":"Stored tide extremes from now forward. Tideless spots return 200 with an empty extremes array and tideless true.","parameters":[{"$ref":"#/components/parameters/SpotId"},{"$ref":"#/components/parameters/Days"}],"responses":{"200":{"description":"Tide extremes envelope.","headers":{"$ref":"#/components/headers/RateLimitHeaders"},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TidesResponse"}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/InvalidKey"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/usage":{"get":{"operationId":"getUsage","summary":"Usage for the presented key","description":"Daily totals for the last 30 UTC days. Does not count toward the daily quota.","responses":{"200":{"description":"Usage series.","headers":{"$ref":"#/components/headers/RateLimitHeaders"},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageResponse"}}}},"401":{"$ref":"#/components/responses/InvalidKey"},"429":{"$ref":"#/components/responses/RateLimited"}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"API key (sm_live_...). Create one at https://surf-map.com/api/console/"},"BearerAuth":{"type":"http","scheme":"bearer","description":"The same API key may be sent as Authorization: Bearer sm_live_..."}},"parameters":{"SpotId":{"name":"id","in":"path","required":true,"description":"Spot slug (e.g. anchor-point) or raw 16-hex id.","schema":{"type":"string"},"example":"anchor-point"},"Days":{"name":"days","in":"query","schema":{"type":"integer","minimum":1,"maximum":7,"default":7}}},"headers":{"RateLimitHeaders":{"description":"X-RateLimit-Limit, X-RateLimit-Remaining (daily), X-RateLimit-Reset (next UTC midnight, ISO 8601). Sent on every /v1 response.","schema":{"type":"string"}}},"responses":{"InvalidRequest":{"description":"Invalid parameters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"InvalidKey":{"description":"Missing, unknown, or revoked API key (code invalid_key), or a key whose account is disabled (code account_disabled).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Unknown spot (code not_found), or no forecast yet for a known spot (code no_forecast).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Daily quota or per-minute burst exceeded. Retry-After header is set.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Spot":{"type":"object","required":["id","name","country","region","lat","lng"],"properties":{"id":{"type":"string","description":"Stable slug id, e.g. anchor-point."},"name":{"type":"string"},"country":{"type":"string","description":"First segment of the spot's country string."},"region":{"type":["string","null"],"description":"Remaining segments, or null."},"lat":{"type":"number"},"lng":{"type":"number"}}},"SpotWithDistance":{"allOf":[{"$ref":"#/components/schemas/Spot"},{"type":"object","required":["distanceKm"],"properties":{"distanceKm":{"type":"number"}}}]},"Rating":{"type":"object","description":"1-5 for each level, 5 best.","required":["beginner","intermediate","advanced"],"properties":{"beginner":{"type":"integer","minimum":1,"maximum":5},"intermediate":{"type":"integer","minimum":1,"maximum":5},"advanced":{"type":"integer","minimum":1,"maximum":5}}},"Tide":{"type":"object","required":["height","state"],"properties":{"height":{"type":"number","description":"Meters."},"state":{"type":"string","enum":["rising","falling"]}}},"ForecastHour":{"type":"object","required":["time","waveHeight","swellHeight","wavePeriod","waveDirection","windSpeed","windDirection","windGust","windDirectionType","airTemperature","waterTemperature","weatherCode","tide","rating"],"properties":{"time":{"type":"string","format":"date-time","description":"UTC, e.g. 2026-08-09T08:00:00Z."},"waveHeight":{"type":"number","description":"Combined significant wave height, meters."},"swellHeight":{"type":"number","description":"Meters."},"wavePeriod":{"type":"number","description":"Seconds."},"waveDirection":{"type":"integer","description":"Primary swell direction, degrees."},"windSpeed":{"type":"number","description":"km/h."},"windDirection":{"type":"integer","description":"Degrees."},"windGust":{"type":"number","description":"km/h."},"windDirectionType":{"type":["string","null"],"enum":["offshore","onshore","cross",null]},"airTemperature":{"type":"number","description":"Celsius."},"waterTemperature":{"type":"number","description":"Celsius."},"weatherCode":{"type":"integer","description":"WMO weather code."},"tide":{"oneOf":[{"$ref":"#/components/schemas/Tide"},{"type":"null"}]},"rating":{"$ref":"#/components/schemas/Rating"}}},"DailyWeather":{"type":"object","required":["date","sunrise","sunset","uvIndexMax"],"properties":{"date":{"type":"string","format":"date"},"sunrise":{"type":["string","null"],"format":"date-time"},"sunset":{"type":["string","null"],"format":"date-time"},"uvIndexMax":{"type":"number"}}},"ForecastResponse":{"type":"object","required":["spot","units","updatedAt","attribution","hours","daily"],"properties":{"spot":{"type":"object","required":["id","name","country"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"country":{"type":"string"}}},"units":{"type":"string","const":"metric"},"updatedAt":{"type":"string","format":"date-time"},"attribution":{"type":"string","const":"Weather inputs include Open-Meteo data (CC BY 4.0)"},"hours":{"type":"array","items":{"$ref":"#/components/schemas/ForecastHour"}},"daily":{"type":"array","items":{"$ref":"#/components/schemas/DailyWeather"}}}},"TideExtreme":{"type":"object","required":["type","time","height"],"properties":{"type":{"type":"string","enum":["high","low"]},"time":{"type":"string","format":"date-time"},"height":{"type":"number","description":"Meters."}}},"TidesResponse":{"type":"object","required":["spot","units","extremes","tideless"],"properties":{"spot":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}},"units":{"type":"string","const":"metric"},"extremes":{"type":"array","items":{"$ref":"#/components/schemas/TideExtreme"}},"tideless":{"type":"boolean"}}},"UsageDay":{"type":"object","required":["date","total","byEndpoint"],"properties":{"date":{"type":"string","format":"date"},"total":{"type":"integer"},"byEndpoint":{"type":"object","description":"Request counts keyed by spots.search, spots.nearby, spots.get, forecast, tides, mcp.<tool>.","additionalProperties":{"type":"integer"}}}},"UsageResponse":{"type":"object","required":["plan","limit","today","days"],"properties":{"plan":{"type":"string"},"limit":{"type":"integer","description":"Daily request limit."},"today":{"type":"integer"},"days":{"type":"array","items":{"$ref":"#/components/schemas/UsageDay"}}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["invalid_key","account_disabled","not_found","no_forecast","rate_limited","invalid_request","internal"]},"message":{"type":"string"}}}}}}}}