---
title: "Time series of catalog products at up to 25 locations."
method: POST
path: "/v1/series/batch"
tags: ["Products"]
---

# Time series of catalog products at up to 25 locations.

`POST /v1/series/batch`

## Request

- Base URL: `https://api.smarterweather.com`
- URL: `https://api.smarterweather.com/v1/series/batch`
- Auth: HTTP bearer

Multi-location form of `GET /v1/series`: the same grammar as JSON
fields, with `locations[]` of `{lat, lon, label?}` (at most 25) in
place of `lat` and `lon`. Products x locations is at most 100.
Elements come back location-major in request order, each echoing
its location's `label`. Every rule of `GET /v1/series` applies;
`points`, top-level `lat`/`lon` and `at` are not words of this
route, and an unknown field is a `400`.

**Experimental** (`x-stability: experimental`), as
`GET /v1/series`.

### Caching

`Cache-Control: private, no-store`.

### Auth and cost

Standard sw-api Bearer auth and rate limits apply. Cost class
`data`, weight 1 per location (`len(locations)`), whatever each
element's status; products, variables, hours and `limit` never
change it.

## Request body

- SeriesBatchRequest: The `GET /v1/series` grammar as JSON, with `locations[]` in place of `lat` and `lon`.
  - `locations` SeriesBatchLocation[], required
    - `lat` number, double, required
    - `lon` number, double, required
    - `label` string
  - `product` string
  - `var` string[]
  - `select` string[]
  - `units` 'si' | 'metric' | 'imperial', required
  - `hours` integer
  - `start` string, date-time
  - `end` string, date-time
  - `run` string, date-time
  - `limit` integer
  - `cursor` string

## Response `200`

One series element per (location, product).

- SeriesResponse: The `GET /v1/series` and `POST /v1/series/batch` envelope.
  - `units` 'si' | 'metric' | 'imperial', required: The requested units system.
  - `truncated` boolean, required: True when `limit` cut the series; `next_cursor` resumes them.
  - `next_cursor` string, nullable, required: Opaque cursor for the next page, null when not truncated.
  - `meta` object, required
    - `step` unknown, required
    - `sources` SeriesSource[], required: The catalog sources behind the response, with their attribution terms.
      - `id` string, required
      - `name` string, nullable, required
      - `attribution` string, required
      - `license` string, required
      - `terms_url` string, nullable, required
  - `series` SeriesElement[], required
    - `product` string, required
    - `lat` number, double, required
    - `lon` number, double, required
    - `label` string: The batch location's `label`, when it had one.
    - `status` 'ok' | 'partial' | 'unavailable' | 'out_of_domain' | 'not_public' | 'entitlement_required', required: `ok`; `unavailable` (no frames in the window); `out_of_domain` (outside the product's coverage); `not_public` (public on another door, not served on this route). `partial` and `entitlement_required` are reserved.
    - `related` string: With `out_of_domain`, a related product that covers the point.
    - `times` string[], required: Frame valid times, RFC 3339 UTC, ascending.
    - `values` object, required: Values per variable `standard_name`, aligned with `times`; null where missing.
    - `units` object, required: Unit per variable `standard_name`; null for a raw value with no stored unit.
    - `run` string, date-time: The model run behind every frame, when they share one.
    - `runs` string[]: Per-frame model runs aligned with `times`, when frames mix runs.
    - `data_age_seconds` integer, nullable, required: Age of the product's latest data (or of the pinned `run`), null when unknown.
    - `freshness_status` 'fresh' | 'stale' | 'missing' | 'null', nullable, required: The product's freshness, as on the catalog; with `run`, the pinned run's age against the product's freshness budget (the strictest of its stores). Null when unknown.
    - `meta` object, required
      - `timezone` string, nullable, required: IANA timezone of the location, null when it cannot be resolved.

## Other responses

- `400`: Bad request.
- `401`: Missing or invalid API key. Carries a `WWW-Authenticate` challenge and a `next_steps` extension. `recommended` is `device_flow`: the object is a complete no-install mint recipe.
- `403`: API key lacks the required scope. Carries a `WWW-Authenticate` challenge with `error="insufficient_scope"` and a `next_steps` extension.
- `404`: Resource not found: a location that cannot be resolved (unknown ZIP, unrecognised free-text location), no upstream data for the location, or a missing resource. On a route that checks coverage (its description says so), a location outside the 50 US states and DC returns type `https://smarterweather.com/errors/outside-coverage`; that and the location-not-found problem carry `next_steps` (`recommended = coverage`).
- `413`: Payload too large. The request body exceeded the 1 MiB cap on `POST /v1/pois/exposure`.
- `429`: Rate limit exceeded. Carries a `next_steps` extension (`recommended: upgrade`, plus `pricing` and `usage`) telling the caller how to lift the limit.
- `500`: Internal server error.
- `502`: Bad gateway. The service received an invalid response from an upstream data source. Transient; clients should retry with backoff.
- `503`: Service unavailable. An upstream dependency is unavailable or shedding load (type `service-unavailable`). Transient; retry with backoff and honor any `Retry-After` header.
- `504`: Gateway timeout. An upstream data source did not respond within the service's budget.

---

[API](https://skmtc.dev/smarterweather/apis/smarter-weather-api.md) · [All operations](https://skmtc.dev/smarterweather/apis/smarter-weather-api/llms.txt) · [OpenAPI document](https://skmtc.dev/smarterweather/apis/smarter-weather-api/revisions/520d55a696e4?raw)
