How to get first and last frost dates for a coordinate

Call https://coords.zone/api/v1/climate with lat and lng query parameters. It returns the average last spring frost and first autumn frost for that point, both as a day-of-year number and as a readable date — for latitude 42.36, longitude −71.06 (Boston, Massachusetts) the last spring frost is Apr 5 and the first autumn frost is Nov 9, a growing season of about 217 days. No API key is required to try it.

The request

curl "https://coords.zone/api/v1/climate?lat=42.36&lng=-71.06"

The response

{
  "lat": 42.36,
  "lng": -71.06,
  "min_temp": -19.34,
  "max_temp": 23.16,
  "frost_last_doy": 95.4,
  "frost_last_date": "Apr 5",
  "frost_first_doy": 312.57,
  "frost_first_date": "Nov 9"
}
Field Type Description
frost_last_doy number Last spring frost as a day of year, 1–365. Fractional, because it is an average across years.
frost_last_date string The same date rendered as "Mon D", e.g. "Apr 5".
frost_first_doy number First autumn frost as a day of year.
frost_first_date string The same date rendered as "Mon D".
min_temp number Average annual extreme minimum temperature, °C. This is the value the USDA zone is derived from.
max_temp number Average annual extreme maximum temperature, °C.

min_temp and max_temp are truncated above for readability; the API returns full floating-point precision.

Calculating the growing season

The frost-free window is the gap between the two day-of-year values, which is why they are exposed as numbers and not only as strings:

const res = await fetch(
  "https://coords.zone/api/v1/climate?lat=42.36&lng=-71.06"
);
const { frost_last_doy, frost_first_doy } = await res.json();

const growingSeasonDays = Math.round(
  frost_first_doy - frost_last_doy
);

console.log(growingSeasonDays); // 217

In Python:

import requests

c = requests.get(
    "https://coords.zone/api/v1/climate",
    params={"lat": 42.36, "lng": -71.06},
).json()

print(round(c["frost_first_doy"] - c["frost_last_doy"]))  # 217

These are averages, not a forecast

Every value here is a 30-year climatological average, not a prediction for the current year. "Last frost Apr 5" means that in a typical year the last freezing night falls around 5 April at that location — roughly half of years will have a later one. Gardeners traditionally add a safety margin of one to two weeks before planting frost-tender crops, and this API deliberately does not pick that margin for you.

The fractional day-of-year values are a direct consequence of averaging: a frost_last_doy of 95.4 is the mean of many individual years' last-frost days, not a claim of sub-day precision. Round them before showing them to people, or use the pre-rendered frost_last_date and frost_first_date strings.

Coordinates with no frost

In climates that never reliably freeze, there is no meaningful frost date, and the frost fields come back as null rather than as an invented date. Latitude 1.35, longitude 103.82 (Singapore) returns:

{
  "lat": 1.35,
  "lng": 103.82,
  "min_temp": 23.29,
  "max_temp": 27.33,
  "frost_last_doy": null,
  "frost_last_date": null,
  "frost_first_doy": null,
  "frost_first_date": null
}

Treat null as "frost-free" rather than as an error, and guard the growing-season subtraction above accordingly.

Where the data comes from

Frost dates are derived from ERA5-Land, the highest-resolution reanalysis in ECMWF's ERA5 family, over decades of record. Coverage is global and land-based, so the same call works for any land coordinate on Earth. Treat the result as a regional average rather than a reading for one garden: elevation, slope, and urban heat can shift real frost dates by days between neighbouring sites.

Next

coords.zone