Docs navigation
REST API
Weather MCP
Agents as developers
Quickstart
Smarter Weather covers the 50 US states and DC only. Every route that takes a point (lat and lon, zip or location) returns 404 outside-coverage for a point outside the 50 states and DC. /v1/alerts and /v1/observations return it for a bounding box that touches no US domain (CONUS, Alaska, Hawaii); a box that touches one may still include area outside the US. /v1/weather/batch reports it per item as outside_coverage, and /v1/point with points and /v1/series/batch mark such points out_of_domain, as they do a point the check could not reach in time (retry it). If the location lookup is unavailable, the check can briefly fail open. Routes keyed by a station, radar site, forecast office or alert id, and nationwide lists, are not filtered by point, so do not treat an empty result as an all-clear. See Errors.
Works the same for a person with a terminal and for a coding agent acting on their behalf: the API answers every unauthenticated or over-limit call with an RFC 9457 problem whose next_steps member says what to do next. Agents: the machine-readable version of this page is llms.txt.
Option A: three requests, no install
1. Call the API with no key
The 401 carries a WWW-Authenticate challenge and a next_steps map. recommended names the link to take first.
curl -sS -D - 'https://api.smarterweather.com/v1/weather?lat=41.66&lon=-91.53'type
"https://smarterweather.com/errors/unauthorized"title
"Unauthorized"status
401detail
"Missing Authorization header. Pass your API key as 'Authorization: Bearer sw_live_*'."instance
"/v1/weather"next_steps
{
"recommended": "device_flow",
"device_flow": {
"href": "https://developers.smarterweather.com/quickstart?utm_campaign=401&utm_medium=problem-json&utm_source=api#device-flow",
"description": "No install, no loopback port: POST device_authorization_endpoint, show the human the code, poll, then GET /developer/keys.",
"client_id": "k2h05BUoTP393zcD",
"device_authorization_endpoint": "https://clerk.smarterweather.com/oauth/device_authorization",
"token_endpoint": "https://clerk.smarterweather.com/oauth/token",
"api_keys_endpoint": "https://api.smarterweather.com/developer/keys"
},
"get_key": {
"href": "https://developers.smarterweather.com/dashboard/api-keys?utm_campaign=401&utm_medium=problem-json&utm_source=api",
"description": "Sign in (free, no card) and mint an API key. Pass it as 'Authorization: Bearer sw_live_*'."
},
"quickstart": {
"href": "…",
"description": "…"
},
"agents": {
"href": "…",
"description": "…"
},
"onboarding_mcp": {
"href": "…",
"description": "…"
},
"keyless_x402": {
"href": "…",
"description": "…"
},
"key_handling": {
"href": "…",
"description": "…"
},
"errors": {
"href": "…",
"description": "…"
}
}2. Get a key
Human present: npx -y @smarterweather/mcp-onboarding@latest login. No human: npx -y @smarterweather/mcp-onboarding@latest trial. Both write .env (mode 0600, gitignored) and print only a key prefix — never the bearer (key handling). A human can also mint from the dashboard (create a free account, no card).
3. Call it again with the key
Success is HTTP 200 with meta and attribution. Anything else is a problem document with next_steps.
# .env -> SMARTERWEATHER_API_KEY=sw_live_... (and add .env to .gitignore)
set -a; source .env; set +a
curl -sS 'https://api.smarterweather.com/v1/weather?lat=41.66&lon=-91.53' \
-H "Authorization: Bearer $SMARTERWEATHER_API_KEY"location
{
"lat": 42.3601,
"lon": -71.0589,
"name": "Boston",
"region": "MA",
"country": "US",
"timezone": "America/New_York",
"county": "Suffolk",
"zip": "02108"
}current
{
"time": "2026-04-20T15:00:00Z",
"temperature": 58.4,
"feels_like": 56.1,
"humidity": 62,
"dew_point": 45.2,
"pressure": 30.05,
"wind_speed": 11.3,
"wind_gust": 17.8,
"wind_direction": 240,
"visibility": 10,
"cloud_cover": 35,
"uv_index": 5,
"precip_rate": 0,
"precip_type": "none",
"conditions": "Partly Cloudy",
"icon": "partly-cloudy-day",
"severe": false
}hourly
[
{
"time": "2026-04-20T16:00:00Z",
"temperature": 59.1,
"feels_like": 56.8,
"humidity": 60,
"wind_speed": 12.1,
"wind_direction": 245,
"precip_probability": 5,
"precip_amount": 0,
"conditions": "Partly Cloudy",
"icon": "partly-cloudy-day"
},
{
"time": "2026-04-20T17:00:00Z",
"temperature": 59.6,
"feels_like": 57.2,
"humidity": 58,
"wind_speed": 12.4,
"wind_direction": 245,
"precip_probability": 5,
"precip_amount": 0,
"conditions": "Partly Cloudy",
"icon": "partly-cloudy-day"
}
]daily
[
{
"date": "2026-04-20",
"high": 61,
"low": 44.5,
"humidity_avg": 55,
"wind_speed_max": 14.2,
"wind_direction": 240,
"precip_probability": 10,
"precip_total": 0,
"snow_total": 0,
"conditions": "Partly Cloudy",
"icon": "partly-cloudy-day",
"day": {
"temperature": 61,
"wind_speed": 14.2,
"wind_direction": 240,
"precip_probability": 10,
"precip_total": 0,
"conditions": "Partly Cloudy",
"icon": "partly-cloudy-day",
"narrative": "Partly cloudy with a high near 61. West winds 10 to 15 mph."
},
"night": {
"temperature": 44.5,
"wind_speed": 6.8,
"wind_direction": 260,
"precip_probability": 5,
"precip_total": 0,
"conditions": "Mostly Clear",
"icon": "mostly-clear-night",
"narrative": "Mostly clear with a low around 45. Winds becoming light."
}
}
]alerts
[]outlooks
[]astro
{
"civil_dawn": "2026-04-20T09:37:00Z",
"sunrise": "2026-04-20T10:05:00Z",
"solar_noon": "2026-04-20T16:47:00Z",
"sunset": "2026-04-20T23:29:00Z",
"civil_dusk": "2026-04-20T23:57:00Z",
"day_length": 48240,
"daylight_length": 51600,
"moonrise": "2026-04-20T11:42:00Z",
"moonset": "2026-04-21T01:18:00Z",
"moon_phase": 0.15,
"moon_phase_name": "Waxing Crescent",
"moon_illumination": 0.22,
"polar": null
}units
{
"temperature": "F",
"wind_speed": "mph",
"pressure": "inHg",
"visibility": "mi",
"precip_amount": "in"
}attribution
{
"sources": [
"NOAA NBM",
"HRRR",
"RTMA",
"MRMS",
"NWS",
"JPL Ephemeris"
],
"notice": "Weather data from NOAA/NWS. Astronomical calculations from JPL ephemerides."
}meta
{
"schema_version": "2026-04-20",
"generated_at": "2026-04-20T15:00:04Z",
"freshness": [
{
"source": "rtma",
"product_id": "rtma_conus_2p5km",
"reference_time": "2026-04-20T14:45:00Z",
"latest_valid": "2026-04-20T14:45:00Z",
"age_seconds": 904
},
{
"source": "hrrr",
"product_id": "hrrr_conus_3km",
"reference_time": "2026-04-20T14:00:00Z",
"latest_valid": "2026-04-20T15:00:00Z",
"age_seconds": 3604
}
],
"warnings": []
}Device flow (agents)
Preferred: run npx -y @smarterweather/mcp-onboarding@latest login. It shows the human a verification URL and code, writes SMARTERWEATHER_API_KEY to ./.env (mode 0600, gitignored), and prints only a key prefix. login blocks until the human approves — run it in the background or with a long shell timeout and relay the printed URL. The 401 next_steps.device_flow.command field names that one-liner. Without Node, follow the raw endpoints below and write only the key field of the POST api_keys_endpoint response to .env as SMARTERWEATHER_API_KEY= — never print the key or access token.
POSTdevice_authorization_endpointwith the advertisedclient_idandscope=openid email. Readdevice_code,user_code,verification_uri,interval.- Show the human
verification_uri+user_code. Polltoken_endpointwithgrant_type=urn:ietf:params:oauth:grant-type:device_codeeveryintervalseconds. Honorauthorization_pending,slow_down, andexpired_token. POST api_keys_endpointwith the access token and{ "name": "cli <project>", "origin": { "channel": "device_flow" } }plus an agent-generatedIdempotency-KeyUUID. On403 email_not_verified, ask the human to verify email and retry. Write only the response'skeyfield to.envasSMARTERWEATHER_API_KEY=.- Discard the access token. Never print the key.
# Preferred: one command. Shows the human a URL, writes ./.env (0600, gitignored),
# prints only a key prefix.
npx -y @smarterweather/mcp-onboarding@latest login
# Raw recipe (no Node): reference only, not a runnable script; step 2 needs
# your own poll loop. Stop if .env already has SMARTERWEATHER_API_KEY.
# 1. Ask Clerk for a device code (client_id is public; it is in the 401 body).
curl -sS -X POST 'https://clerk.smarterweather.com/oauth/device_authorization' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=k2h05BUoTP393zcD' \
--data-urlencode 'scope=openid email'
# 2. Show the human verification_uri + user_code. Poll until they approve.
# grant_type=urn:ietf:params:oauth:grant-type:device_code
# handle authorization_pending / slow_down / expired_token.
# 3. Create a key and write ONLY its key field to .env. Nothing reaches the terminal.
# (umask 077; curl -sS -X POST 'https://api.smarterweather.com/developer/keys' \
# -H "Authorization: Bearer $ACCESS_TOKEN" \
# -H "Idempotency-Key: $(uuidgen)" \
# -H 'Content-Type: application/json' \
# -d '{"name":"cli my-project","origin":{"channel":"device_flow"}}' \
# | jq -er '"SMARTERWEATHER_API_KEY=" + .key' >> .env)
# grep -qx '.env' .gitignore 2>/dev/null || echo .env >> .gitignore
# 4. Discard the access token (unset ACCESS_TOKEN).Option B: give your agent weather tools
If your client speaks MCP, paste this prompt. Your agent connects to the onboarding MCP anonymously, signs you up, mints a key after OAuth consent, and configures the weather MCP in your client. sign_up is anonymous; create_api_key and configure_mcp run after consent. Agents with a wallet can instead pay per call via x402.
Install the Smarter Weather onboarding skill. Run `npx skills add https://developers.smarterweather.com --skill smarterweather-onboarding` and select your agent. You can read the skill directly at https://developers.smarterweather.com/.well-known/skills/smarterweather-onboarding/SKILL.md (raw: https://raw.githubusercontent.com/smarterweather/developer/main/plugins/smarterweather-onboarding/skills/smarterweather-onboarding/SKILL.md). Then use the onboarding skill to sign up, mint an API key, and configure the weather MCP.
Option C: from the dashboard, in your language
Same key, same endpoint, with a place name instead of coordinates.
# first: export SMARTERWEATHER_API_KEY=sw_live_... (or: set -a; source .env; set +a)
curl "https://api.smarterweather.com/v1/weather?location=Boston,%20MA&units=imperial" \
-H "Authorization: Bearer $SMARTERWEATHER_API_KEY"// save as sample.mjs; run: node --env-file=.env sample.mjs (Node 20.6+, ESM top-level await)
const res = await fetch(
"https://api.smarterweather.com/v1/weather?location=Boston,%20MA&units=imperial",
{ headers: { Authorization: `Bearer ${process.env.SMARTERWEATHER_API_KEY}` } },
);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const data = await res.json();
console.log(JSON.stringify(data, null, 2));import os, sys, requests
r = requests.get(
"https://api.smarterweather.com/v1/weather",
params={"location":"Boston, MA","units":"imperial"},
headers={"Authorization": f"Bearer {os.environ['SMARTERWEATHER_API_KEY']}"},
)
if not r.ok:
sys.exit(f"{r.status_code} {r.text}")
data = r.json()
print(data)package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("GET", "https://api.smarterweather.com/v1/weather?location=Boston,%20MA&units=imperial", nil)
if err != nil {
log.Fatal(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("SMARTERWEATHER_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
log.Fatal(err)
}
if res.StatusCode < 200 || res.StatusCode > 299 {
log.Fatalf("%s %s", res.Status, body)
}
fmt.Println(string(body))
}location
{
"lat": 42.3601,
"lon": -71.0589,
"name": "Boston",
"region": "MA",
"country": "US",
"timezone": "America/New_York",
"county": "Suffolk",
"zip": "02108"
}current
{
"time": "2026-04-20T15:00:00Z",
"temperature": 58.4,
"feels_like": 56.1,
"humidity": 62,
"dew_point": 45.2,
"pressure": 30.05,
"wind_speed": 11.3,
"wind_gust": 17.8,
"wind_direction": 240,
"visibility": 10,
"cloud_cover": 35,
"uv_index": 5,
"precip_rate": 0,
"precip_type": "none",
"conditions": "Partly Cloudy",
"icon": "partly-cloudy-day",
"severe": false
}hourly
[
{
"time": "2026-04-20T16:00:00Z",
"temperature": 59.1,
"feels_like": 56.8,
"humidity": 60,
"wind_speed": 12.1,
"wind_direction": 245,
"precip_probability": 5,
"precip_amount": 0,
"conditions": "Partly Cloudy",
"icon": "partly-cloudy-day"
},
{
"time": "2026-04-20T17:00:00Z",
"temperature": 59.6,
"feels_like": 57.2,
"humidity": 58,
"wind_speed": 12.4,
"wind_direction": 245,
"precip_probability": 5,
"precip_amount": 0,
"conditions": "Partly Cloudy",
"icon": "partly-cloudy-day"
}
]daily
[
{
"date": "2026-04-20",
"high": 61,
"low": 44.5,
"humidity_avg": 55,
"wind_speed_max": 14.2,
"wind_direction": 240,
"precip_probability": 10,
"precip_total": 0,
"snow_total": 0,
"conditions": "Partly Cloudy",
"icon": "partly-cloudy-day",
"day": {
"temperature": 61,
"wind_speed": 14.2,
"wind_direction": 240,
"precip_probability": 10,
"precip_total": 0,
"conditions": "Partly Cloudy",
"icon": "partly-cloudy-day",
"narrative": "Partly cloudy with a high near 61. West winds 10 to 15 mph."
},
"night": {
"temperature": 44.5,
"wind_speed": 6.8,
"wind_direction": 260,
"precip_probability": 5,
"precip_total": 0,
"conditions": "Mostly Clear",
"icon": "mostly-clear-night",
"narrative": "Mostly clear with a low around 45. Winds becoming light."
}
}
]alerts
[]outlooks
[]astro
{
"civil_dawn": "2026-04-20T09:37:00Z",
"sunrise": "2026-04-20T10:05:00Z",
"solar_noon": "2026-04-20T16:47:00Z",
"sunset": "2026-04-20T23:29:00Z",
"civil_dusk": "2026-04-20T23:57:00Z",
"day_length": 48240,
"daylight_length": 51600,
"moonrise": "2026-04-20T11:42:00Z",
"moonset": "2026-04-21T01:18:00Z",
"moon_phase": 0.15,
"moon_phase_name": "Waxing Crescent",
"moon_illumination": 0.22,
"polar": null
}units
{
"temperature": "F",
"wind_speed": "mph",
"pressure": "inHg",
"visibility": "mi",
"precip_amount": "in"
}attribution
{
"sources": [
"NOAA NBM",
"HRRR",
"RTMA",
"MRMS",
"NWS",
"JPL Ephemeris"
],
"notice": "Weather data from NOAA/NWS. Astronomical calculations from JPL ephemerides."
}meta
{
"schema_version": "2026-04-20",
"generated_at": "2026-04-20T15:00:04Z",
"freshness": [
{
"source": "rtma",
"product_id": "rtma_conus_2p5km",
"reference_time": "2026-04-20T14:45:00Z",
"latest_valid": "2026-04-20T14:45:00Z",
"age_seconds": 904
},
{
"source": "hrrr",
"product_id": "hrrr_conus_3km",
"reference_time": "2026-04-20T14:00:00Z",
"latest_valid": "2026-04-20T15:00:00Z",
"age_seconds": 3604
}
],
"warnings": []
}Three recipes
Schedule check
Move outdoor work when daily precipitation probability crosses a threshold.
// save as schedule.mjs; run: node --env-file=.env schedule.mjs (Node 20.6+)
const res = await fetch(
"https://api.smarterweather.com/v1/weather?location=Boston,%20MA&include=daily",
{ headers: { Authorization: `Bearer ${process.env.SMARTERWEATHER_API_KEY}` } },
);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const weather = await res.json();
const today = weather.daily.find((d) => d.relative_day === 0);
if ((today?.precip_probability ?? 0) >= 40) {
console.log("Move outdoor work inside");
}Alert routing
Forward the alerts array to a webhook.
// save as alerts.mjs; run: node --env-file=.env alerts.mjs (Node 20.6+, WEBHOOK_URL set)
const res = await fetch(
"https://api.smarterweather.com/v1/weather?location=Boston,%20MA&include=alerts",
{ headers: { Authorization: `Bearer ${process.env.SMARTERWEATHER_API_KEY}` } },
);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const weather = await res.json();
for (const alert of weather.alerts ?? []) {
await fetch(process.env.WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(alert),
});
}Map overlay
Draw active NWS alert polygons in MapLibre. Your server holds the key (the API is not called from browsers). It draws the 4 most severe alerts in view and reports returned (capped at 50) and truncated. A new view costs 1 search call plus 1 call per alert not fetched in the last minute (up to 5); trial keys allow 10 calls a minute. Set PAGE_ORIGIN to your page's origin.
// proxy.mjs holds the key; the browser calls it. Run: node --env-file=.env proxy.mjs
// Set PAGE_ORIGIN to your page's origin (default http://localhost:3000).
import { createServer } from "node:http";
const PAGE_ORIGIN = process.env.PAGE_ORIGIN ?? "http://localhost:3000";
const TTL_MS = 60_000; // matches the API's Cache-Control: max-age=60
const cache = new Map(); // key -> { at, promise }; failures are not kept
function cached(key, load) {
const hit = cache.get(key);
if (hit && Date.now() - hit.at < TTL_MS) return hit.promise;
if (cache.size > 500) cache.clear();
const promise = load();
cache.set(key, { at: Date.now(), promise });
promise.catch(() => cache.get(key)?.promise === promise && cache.delete(key));
return promise;
}
async function api(path) {
const res = await fetch("https://api.smarterweather.com" + path, { headers: { Authorization: `Bearer ${process.env.SMARTERWEATHER_API_KEY}` } });
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
return res.json();
}
createServer(async (req, res) => {
const q = new URL(req.url, "http://localhost").searchParams;
// Snap the box outward to 0.5° so nearby views share a cache entry.
const snap = (k, round) => round(Number(q.get(k)) * 2) / 2;
const box = new URLSearchParams({
min_lon: snap("min_lon", Math.floor),
min_lat: snap("min_lat", Math.floor),
max_lon: snap("max_lon", Math.ceil),
max_lat: snap("max_lat", Math.ceil),
}).toString();
try {
// Search, then geometry (only on /v1/alerts/{id}) for the 4 most severe
// alerts. Details are cached per id, so re-pans reuse them.
const { alerts, truncated } = await cached(box, () => api(`/v1/alerts?${box}`));
const details = await Promise.allSettled(
alerts.slice(0, 4).map((a) =>
cached(a.id, () => api(`/v1/alerts/${encodeURIComponent(a.id)}`)),
),
);
const features = details
.filter((d) => d.status === "fulfilled" && d.value.geometry)
.map(({ value: d }) => ({
type: "Feature",
geometry: d.geometry,
properties: { event: d.event, severity: d.severity },
}));
res.writeHead(200, {
"Content-Type": "application/geo+json",
"Access-Control-Allow-Origin": PAGE_ORIGIN,
});
// returned: alerts the search returned (capped at 50; truncated = more exist).
res.end(JSON.stringify({ type: "FeatureCollection", features, returned: alerts.length, truncated }));
} catch (err) {
res.writeHead(502, { "Access-Control-Allow-Origin": PAGE_ORIGIN });
res.end(String(err));
}
}).listen(8787, "127.0.0.1");// In the page (maplibre-gl loaded, <div id="map">), with proxy.mjs on :8787
// and its PAGE_ORIGIN set to this page's origin.
const map = new maplibregl.Map({
container: "map",
style: "https://demotiles.maplibre.org/style.json",
center: [-87.63, 41.88],
zoom: 5,
});
function alertsUrl() {
// Undo world-copy wrapping, then clamp: the API rejects lon outside ±180.
const b = map.getBounds();
const shift = Math.round(b.getCenter().lng / 360) * 360;
const clamp = (v, max) => Math.min(max, Math.max(-max, v));
const box = new URLSearchParams({
min_lon: clamp(b.getWest() - shift, 180),
min_lat: clamp(b.getSouth(), 90),
max_lon: clamp(b.getEast() - shift, 180),
max_lat: clamp(b.getNorth(), 90),
});
return "http://127.0.0.1:8787/?" + box;
}
map.on("load", () => {
map.addSource("alerts", { type: "geojson", data: alertsUrl() });
map.addLayer({
id: "alerts",
type: "fill",
source: "alerts",
paint: { "fill-color": "#e5484d", "fill-opacity": 0.35 },
});
let timer;
map.on("moveend", () => {
clearTimeout(timer);
timer = setTimeout(() => map.getSource("alerts").setData(alertsUrl()), 3000);
});
});Next steps
- REST API — per-resource references
- Weather endpoint — the unified response
- MCP setup — Claude Desktop, Claude Code, Cursor, or VS Code
- Error model — every problem type and its
next_steps