---
title: "One series, every dimension named by the caller"
method: POST
path: "/v1/series"
tags: ["Resolution"]
---

# One series, every dimension named by the caller

`POST /v1/series`

## Request

- Base URL: `https://api.stackdata.io`
- URL: `https://api.stackdata.io/v1/series`
- Auth: one of:
  - none
  - API key in header `X-API-Key`

Structured query: the caller names the dataset, ISO3 countries, an ISO period range
and a member code for every dimension (see `/v1/datasets/{id}/structure` and
`/v1/schema/series.json`). Nothing is guessed or relaxed. Status is `ok`, `empty`
(with a reason) or `refused` (`licence_unverified`, `no_data_path`,
`not_in_serve_set`, `reader_refused`, `ambiguous_selection`). Metered like `/v1/data`:
only an uncached upstream fetch is charged; refusals, empties and 4xx/5xx are free.
A body that is not JSON is `400 bad_json`.

## Request body

- SeriesRequest: One series per request. The caller names every dimension; nothing is guessed or relaxed. Build the request from GET /v1/datasets/{id}/structure.
  - `dataset` string, required: A dataset id from the catalog, as returned by search (/v1/search, or the search_datasets MCP tool), e.g. "ilo-sdmx:DF_X".
  - `countries` string[], required: ISO 3166-1 alpha-3 codes (e.g. "JPN") or an aggregate code listed in the dataset's /structure. 1 to 25 entries.
  - `period` object, required: Inclusive ISO period range. "latest" is not accepted: name the periods you want. Annual 2020, quarterly 2020-Q1, monthly 2020-01.
    - `start` string, required: First period, ISO form: YYYY, YYYY-Qn or YYYY-MM. Must match a frequency the dataset serves.
    - `end` string, required: Last period, same form and frequency as start.
  - `select` object, required: A member code for every dimension of the dataset (other than geography and time) as listed in /structure, including frequency. Every dimension must be named; a missing or unknown dimension or code is a 400, never defaulted.

## Response `200`

An `ok`, `empty` or `refused` answer. `ok` carries observations (each with its own
provenance, including the raw period string); `empty` carries a `reason`; `refused`
carries a `reason` and, for `reader_refused` and `ambiguous_selection`, a `detail`.
An empty `observations` array is never a bare success: it is `empty` or `refused`.

- SeriesResponse
  - `status` 'ok' | 'empty' | 'refused', required
  - `request` object, required: The request as received.
  - `reason` string: On `empty`, why there is nothing. On `refused`, one of `licence_unverified`, `no_data_path`, `not_in_serve_set`, `reader_refused`, `ambiguous_selection`.
  - `detail` string: With `reader_refused` and `ambiguous_selection`; also an `empty` caused by a reader's own message.
  - `dataset` string: Present on refusals.
  - `resolved` object: How the request was bound.
    - `geo` object: Caller code to the source's own geography code.
    - `select` object
    - `freq` string
    - `window` string[]
  - `observations` SeriesObservation[], required
    - `country` string, required: The code the caller sent.
    - `period` string, required: ISO period (YYYY
    - `value` number, nullable, required
    - `provenance` object, required: The nested provenance envelope for this value: the raw string it came from, the rule that produced it, and `period_raw`, the period exactly as the source wrote it.
      - `period_raw` string
  - `missing` object[]: Requested country-periods with no observation (`{country, period}`), or `{country}` when the window is too long to enumerate. Withheld points are in `notes`, not here.
    - `country` string
    - `period` string
  - `notes` object[]: What a plain gap would hide: from the reader (`withheld`, `window_capped`, `eliminated`, `opened`, `truncated`, `skipped_periods`, `refused_coordinates`), and from our own filtering — `other_frequency` (`count`, `frequencies`: points the source returned at another frequency, left out) and `unrequested_geography` (`count`: points for geography codes the request did not name, left out). An answer empty only because of that filtering says so in `reason`.
    - `kind` string
    - `reason` string
  - `unparsed_periods` string[]: Present only when the source returned period strings that could not be normalised; those points are not in `observations`.
  - `provenance` object
    - `dataset` string
    - `source_url` string, nullable
    - `licence` object
    - `attribution` string, nullable
    - `cache_hit` boolean
  - `billing` object: Metering outcome. Only an uncached upstream fetch is charged.
    - `upstream_fetches` integer
    - `charged` boolean
    - `tier` string

## Other responses

- `400`: Invalid request. The body names the error `error` code, the `field` at fault, a `detail`, and `allowed` (the valid values, when there are at most 50).
- `402`: Payment required (x402).
- `404`: Unknown dataset (`unknown_dataset`).
- `429`: Quota exhausted.
- `502`: Upstream source failed (`upstream_failed`). Free; nothing is settled.

---

[API](https://skmtc.dev/stackdata/apis/stackdata-api.md) · [All operations](https://skmtc.dev/stackdata/apis/stackdata-api/llms.txt) · [OpenAPI document](https://skmtc.dev/stackdata/apis/stackdata-api/revisions/91358ae3f6e8?raw)
