Skip to content
Docs navigation

Quickstart

Three requests from zero to a live forecast call. Nothing to install.

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
401
detail
"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.

  1. POST device_authorization_endpoint with the advertised client_id and scope=openid email. Read device_code, user_code, verification_uri, interval.
  2. Show the human verification_uri + user_code. Poll token_endpoint with grant_type=urn:ietf:params:oauth:grant-type:device_code every interval seconds. Honor authorization_pending, slow_down, and expired_token.
  3. POST api_keys_endpoint with the access token and { "name": "cli <project>", "origin": { "channel": "device_flow" } } plus an agent-generated Idempotency-Key UUID. On 403 email_not_verified, ask the human to verify email and retry. Write only the response's key field to .env as SMARTERWEATHER_API_KEY=.
  4. 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