{
	"openapi": "3.1.0",
	"info": {
		"title": "MacroRadar Public API",
		"version": "1.3.0",
		"description": "A free, keyless JSON API for US macroeconomic indicators and MacroRadar's derived model outputs (macro regime, recession probability, sentiment). Every observation is vintage-dated; every model output is walk-forward validated. Underlying US-government series are public domain; ten privately-owned series are metadata-only (see the 451 response on the series endpoints). MacroRadar model outputs are historical indicators, not investment advice, and are not a recommendation to buy, sell, or hold any security.",
		"license": {
			"name": "Free to use with attribution",
			"url": "https://www.macroradar.io/developers"
		},
		"contact": {
			"name": "MacroRadar",
			"url": "https://www.macroradar.io/developers"
		}
	},
	"servers": [
		{
			"url": "https://www.macroradar.io/api/v1",
			"description": "Production"
		}
	],
	"paths": {
		"/catalog": {
			"get": {
				"operationId": "getCatalog",
				"summary": "Full series catalog and model endpoint list. Ambiguous price series carry a price_basis object naming what the number is, who publishes it, and the near neighbour it is not — the EIA Brent spot price is not the ICE front-month future.",
				"description": "Every tracked series (id, slug, name, category, frequency, units, description, license status, chart URL) plus the list of model output endpoints.",
				"responses": {
					"200": {
						"description": "Catalog envelope",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/CatalogEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/series/{id}": {
			"get": {
				"operationId": "getSeriesObservations",
				"summary": "Observations for one series",
				"description": "Observations for one series, oldest first. Licensed series (see /catalog for the redistributable flag) return HTTP 451 with metadata only.",
				"parameters": [
					{
						"$ref": "#/components/parameters/SeriesId"
					},
					{
						"$ref": "#/components/parameters/Limit"
					},
					{
						"$ref": "#/components/parameters/Derived"
					},
					{
						"$ref": "#/components/parameters/Format"
					}
				],
				"responses": {
					"200": {
						"description": "Series observations envelope, or a CSV download when format=csv",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/SeriesObservationsEnvelope"
								}
							},
							"text/csv": {
								"schema": {
									"type": "string"
								}
							}
						}
					},
					"400": {
						"description": "The limit query parameter is not a positive integer from 1 through 50000.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"404": {
						"description": "Unknown series id or slug",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"451": {
						"$ref": "#/components/responses/LicensedSeries"
					},
					"503": {
						"description": "No data available for this series yet",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/series/{id}/latest": {
			"get": {
				"operationId": "getSeriesLatest",
				"summary": "Latest observation for one series",
				"description": "The most recent observation for one series, plus its current 10-year and 30-year percentiles and z-score. Licensed series return HTTP 451 with metadata only.",
				"parameters": [
					{
						"$ref": "#/components/parameters/SeriesId"
					}
				],
				"responses": {
					"200": {
						"description": "Latest observation envelope",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/SeriesLatestEnvelope"
								}
							}
						}
					},
					"404": {
						"description": "Unknown series id or slug",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					},
					"451": {
						"$ref": "#/components/responses/LicensedSeries"
					},
					"503": {
						"description": "No data available for this series yet",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/series/{id}/as-of": {
			"get": {
				"summary": "A series as it read on a past date",
				"description": "The vintage archive: what this series showed on a given date, rather than what it shows now. Statistics are revised, and reading a revised figure as if it were contemporaneous is one of the silent ways a macro claim goes wrong. `basis` distinguishes known_at (a real reading) from not_yet_published (the number did not exist yet — a fact about the world) and no_archive (no vintage is held — a fact about MacroRadar). The rule is at /claims#as-of-vintage.",
				"parameters": [
					{
						"name": "id",
						"in": "path",
						"required": true,
						"schema": {
							"type": "string"
						},
						"description": "Series id or url slug, e.g. CPIAUCSL or inflation-rate."
					},
					{
						"name": "as_of",
						"in": "query",
						"required": true,
						"schema": {
							"type": "string",
							"format": "date"
						},
						"description": "Read the archive as it stood on this date, e.g. 2008-09-15."
					},
					{
						"name": "period",
						"in": "query",
						"required": false,
						"schema": {
							"type": "string",
							"format": "date"
						},
						"description": "Optional. One observation period; adds its full revision history, including how many publications the source holds versus how many are held here."
					}
				],
				"responses": {
					"200": {
						"description": "The reading, or a stated reason there is none."
					},
					"400": {
						"description": "as_of missing or not a valid past date."
					},
					"404": {
						"description": "Unknown series."
					},
					"451": {
						"description": "Licensed series: vintage levels are not redistributed."
					},
					"503": {
						"description": "No vintage archive is published for this series yet."
					}
				}
			}
		},
		"/regime": {
			"get": {
				"operationId": "getRegime",
				"summary": "Current macro regime read",
				"description": "The current multi-dimensional macro regime read — a MacroRadar model output. Historical indicator, not investment advice. ?as_of and ?vintage refuse (400): the composite reading is a singleton row overwritten wholesale on every sync, with no vintage of a past recomputation held. Per-series input vintages are at /api/v1/series/{id}/as-of.",
				"responses": {
					"200": {
						"description": "Regime envelope",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Envelope"
								}
							}
						}
					},
					"503": {
						"description": "Regime summary not available yet",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/regime/history": {
			"get": {
				"operationId": "getRegimeHistory",
				"summary": "Walk-forward regime probability history",
				"description": "Each point comes from a model refit using only data available up to that date. Historical indicator, not investment advice. ?as_of and ?vintage refuse (400): each point is computed point-in-time, but the output series is recomputed wholesale and no vintage of past recomputations is stored. Per-series input vintages are at /api/v1/series/{id}/as-of.",
				"responses": {
					"200": {
						"description": "Regime history envelope",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Envelope"
								}
							}
						}
					},
					"503": {
						"description": "Regime history not available yet",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/recession": {
			"get": {
				"operationId": "getRecession",
				"summary": "6-month US recession probability",
				"description": "The current 6-month recession probability. Add ?history=true for the walk-forward history. Historical indicator, not investment advice. ?as_of and ?vintage refuse (400): the singleton reading and its history table are overwritten wholesale on every sync; neither carries a vintage of an earlier recomputation. Per-series input vintages are at /api/v1/series/{id}/as-of.",
				"parameters": [
					{
						"$ref": "#/components/parameters/History"
					}
				],
				"responses": {
					"200": {
						"description": "Recession probability envelope",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Envelope"
								}
							}
						}
					},
					"503": {
						"description": "Recession probability not available yet",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/sentiment": {
			"get": {
				"operationId": "getSentiment",
				"summary": "MacroRadar sentiment composite",
				"description": "The current 0-100 sentiment composite. Add ?history=true for the causal daily history (trailing-window percentiles, no look-ahead). Historical indicator, not investment advice. ?as_of and ?vintage refuse (400): the composite snapshot and its causal history are recomputed and overwritten wholesale on every sync; no vintage of an earlier composite is held. Per-series input vintages are at /api/v1/series/{id}/as-of.",
				"parameters": [
					{
						"$ref": "#/components/parameters/History"
					}
				],
				"responses": {
					"200": {
						"description": "Sentiment envelope",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Envelope"
								}
							}
						}
					},
					"503": {
						"description": "Sentiment index not available yet",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/whats-moving": {
			"get": {
				"summary": "Every tracked series' latest move ranked by rarity against its own history, with a checkable sentence per row. Raw levels of licensed series are withheld. ?as_of and ?vintage refuse (400): the per-observation vintage archive exists but this ranking does not read it, and the derived ranking itself has no vintaged snapshot — an as-of read of all 162 tracked series per request is an architecture this surface does not run. Per-series input vintages are at /api/v1/series/{id}/as-of.",
				"responses": {
					"200": {
						"description": "OK — { data, meta } envelope with attribution."
					}
				}
			}
		},
		"/episodes": {
			"get": {
				"summary": "Derived episode spans plus the open spans ranked by running duration. ?as_of and ?vintage refuse (400): derived spans are recomputed from the current record and overwrite the table wholesale on every sync; no archive of an earlier recomputation's span boundaries is held. Per-series input vintages are at /api/v1/series/{id}/as-of.",
				"responses": {
					"200": {
						"description": "OK — { data, meta } envelope with attribution."
					}
				},
				"parameters": [
					{
						"name": "series",
						"in": "query",
						"required": false,
						"schema": {
							"type": "string"
						},
						"description": "Series id or slug filter."
					}
				]
			}
		},
		"/event-windows": {
			"get": {
				"summary": "FOMC decision-day windows with the latest decision's moves ranked within each series' record. Raw closes of licensed series are withheld. ?as_of and ?vintage refuse (400): decision-day windows are recomputed from the current record and overwrite the table wholesale on every sync; no archive of an earlier recomputation's window values is held. Per-series input vintages are at /api/v1/series/{id}/as-of.",
				"responses": {
					"200": {
						"description": "OK — { data, meta } envelope with attribution."
					}
				},
				"parameters": [
					{
						"name": "series",
						"in": "query",
						"required": false,
						"schema": {
							"type": "string"
						},
						"description": "Series id or slug filter."
					}
				]
			}
		},
		"/calendar": {
			"get": {
				"summary": "The consolidated macro schedule and recent record from the institutions' own published calendars. Pass from/to for an explicit historical range instead of the rolling window. Every event carries four timestamps: event_date (when it happened), published_at (when the source published it), ingested_at (when this record first held it — the as-of key) and revised_at (when the record's copy last changed).",
				"responses": {
					"200": {
						"description": "OK — { data, meta } envelope with attribution."
					}
				},
				"parameters": [
					{
						"name": "days_ahead",
						"in": "query",
						"required": false,
						"schema": {
							"type": "integer",
							"maximum": 120
						},
						"description": "Days ahead (default 60)."
					},
					{
						"name": "days_back",
						"in": "query",
						"required": false,
						"schema": {
							"type": "integer",
							"maximum": 60
						},
						"description": "Days back (default 14)."
					},
					{
						"name": "from",
						"in": "query",
						"required": false,
						"schema": {
							"type": "string",
							"format": "date"
						},
						"description": "Range start, YYYY-MM-DD. Requires `to`. Max span 1100 days."
					},
					{
						"name": "to",
						"in": "query",
						"required": false,
						"schema": {
							"type": "string",
							"format": "date"
						},
						"description": "Range end, YYYY-MM-DD. Requires `from`."
					}
				]
			}
		},
		"/log": {
			"get": {
				"summary": "One day's macro record: events, episode transitions, decision-day moves ranked as of that date, and cited annotations. Also returns record_completeness: how many entries the record already held on the day itself versus picked up later, so a back-filled document is visible rather than silent. ?as_of and ?vintage refuse (400): events carry ingested_at/revised_at, but the episode and decision-window derivations this endpoint unions in do not, so the union cannot be reconstructed as of a past date. Per-series input vintages are at /api/v1/series/{id}/as-of.",
				"responses": {
					"200": {
						"description": "OK — { data, meta } envelope with attribution."
					}
				},
				"parameters": [
					{
						"name": "date",
						"in": "query",
						"required": true,
						"schema": {
							"type": "string",
							"format": "date"
						},
						"description": "ISO date between 1950-01-01 and today."
					}
				]
			}
		},
		"/claims": {
			"get": {
				"summary": "The claims ledger: every kind of quantitative sentence on the site with its rule and recompute steps.",
				"responses": {
					"200": {
						"description": "OK — { data, meta } envelope with attribution."
					}
				}
			}
		},
		"/policy-regimes": {
			"get": {
				"summary": "US sanctions, tariff and export-control programmes as spans, each dated from a cited Federal Register instrument. Durations are exact; action counts are bounded by record_starts, returned alongside them.",
				"responses": {
					"200": {
						"description": "OK — { data, meta } envelope with attribution."
					}
				}
			}
		},
		"/studies": {
			"get": {
				"summary": "Conditional history: what followed when conditions held",
				"description": "Every past window where a study's stated conditions held together, with the distribution of what followed and the sample size. Contains no probability, expected value or forecast by design — the count and the spread are the answer. The rule is at /claims#conditional-history. Pass ?study=SLUG&as_of=YYYY-MM-DD to answer as of a past date: every loaded series is truncated to observations on or before as_of before anything is evaluated, and the request refuses (naming the series) if any of the study's series is revised after publication and its vintage archive is not backfilled — the same rule POST /api/v1/studies uses. as_of has no meaning on the bare catalogue listing (no ?study=) and refuses there.",
				"parameters": [
					{
						"name": "study",
						"in": "query",
						"required": false,
						"schema": {
							"type": "string"
						},
						"description": "Study slug. Omit to list the published studies."
					},
					{
						"name": "as_of",
						"in": "query",
						"required": false,
						"schema": {
							"type": "string",
							"format": "date"
						},
						"description": "Truncate the named study's series to observations on or before this date before evaluating it. Requires ?study=; refused on the bare listing and refused, naming the series, when a study series is revised and its vintage archive is not backfilled."
					}
				],
				"responses": {
					"200": {
						"description": "The study's matched windows and distribution (as of as_of, when given), or the catalogue."
					},
					"400": {
						"description": "as_of given without ?study=, as_of not a real date, or an unsupported vintage query param."
					},
					"404": {
						"description": "Unknown study slug."
					},
					"422": {
						"description": "as_of given, but the request cannot be answered as of that date. `vintage/not-yet-served`: a study series is revised after publication and its vintage archive is not backfilled, named in the refusal. `study/thin-sample`: truncating the record to as_of leaves fewer than the thin-sample floor of completed windows to distribute — the same refusal an equivalent POST /api/v1/studies body would give."
					}
				}
			},
			"post": {
				"operationId": "postStudy",
				"summary": "Count and date conditional-history windows for a structured study",
				"description": "Enumerates every past window where the stated conditions held and distributes the measured changes after the stated horizon. Accepts one to eight conditions and observation counts up to 10,000. Raw measured-at-start and measured-at-horizon values are null when the measure reads a non-redistributable series; `recompute.licensed_series` names every licensed input that must be fetched from its publisher.",
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/StudyPostRequest"
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "Matched windows, counts, distributions, compiled steps, and recompute licensing details.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Envelope"
								}
							}
						}
					},
					"400": {
						"description": "Malformed JSON or a request shape the parser refuses. Codes: `study/bad-body`, `study/no-conditions`, `study/too-many-conditions`, `condition/bad-shape`, `condition/bad-kind`, `condition/bad-comparator`, `condition/bad-threshold`, `condition/bad-percentile`, `condition/bad-window`, `condition/bad-over`, `expr/empty`, `expr/dangling-operator`, `expr/unknown-operator`, `expr/not-addressable`, `series/bad-id`, `transform/bad-argument`, `transform/unexpected-argument`, `transform/unknown`, `range/bad-date`, `range/reversed`, `vintage/required`, `vintage/bad-date`, `study/bad-horizon`, `study/bad-dedupe`.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/PostParseErrorEnvelope"
								}
							}
						}
					},
					"422": {
						"description": "A well-formed request the compiler or conditional-history engine refuses. Codes: `arithmetic/incompatible-units`, `arithmetic/different-concepts`, `arithmetic/mixed-seasonality`, `arithmetic/currency-not-stated`, `arithmetic/index-levels-not-comparable`, `frequency/mismatch`, `series/unknown`, `series/unregistered`, `series/not-ingested`, `vintage/not-yet-served`, `vintage/ranking-not-available-as-of`, `vintage/range-after-as-of`, `transform/mom-on-unadjusted`, `transform/needs-level-or-rate`, `transform/annualize-needs-level`, `transform/index-needs-level`, `condition/threshold-on-level`, `condition/percentile-range`, `condition/percentile-window`, `condition/direction-span`, `study/thin-sample`.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/PostRefusalEnvelope"
								}
							}
						}
					}
				}
			}
		},
		"/compute": {
			"get": {
				"summary": "Compute an expression in the macro grammar",
				"description": "Returns the observations AND the compiled plan that produced them. The plan is generated from the same object as the numbers, so it cannot drift from them.\n\nMacro arithmetic has failure modes invisible in the output — percent-changing a series already quoted in percent, differencing two index levels with different bases, combining frequencies with no alignment rule. Each is refused with a named reason rather than returned as a plausible wrong number.\n\nAn as_of date is refused, not answered: the vintage archive is not backfilled, and serving revised numbers under a point-in-time address is wrong in exactly the way that question exists to avoid. Use /series/{id}/as-of instead.",
				"parameters": [
					{
						"name": "expr",
						"in": "query",
						"required": true,
						"schema": {
							"type": "string"
						},
						"description": "e.g. FEDFUNDS~minus~CPIAUCSL.yoy — a series, transforms after a dot (yoy, mom, diff_N, lag_N, annualized_N, zscore_N, percentile_N, movingAverage_N, index_YYYY-MM-DD), and plus/minus/times/over between two expressions. Needs no percent-encoding.",
						"example": "FEDFUNDS~minus~CPIAUCSL.yoy"
					},
					{
						"name": "from",
						"in": "query",
						"required": false,
						"schema": {
							"type": "string",
							"format": "date"
						}
					},
					{
						"name": "to",
						"in": "query",
						"required": false,
						"schema": {
							"type": "string",
							"format": "date"
						}
					}
				],
				"responses": {
					"200": {
						"description": "Observations with the compiled plan attached"
					},
					"400": {
						"description": "The expression could not be read; every reason is listed"
					},
					"404": {
						"description": "A named series is not in the catalog"
					},
					"422": {
						"description": "Well-formed but refused; refusals name what would have been wrong"
					},
					"451": {
						"description": "An input may be displayed but not redistributed"
					}
				}
			},
			"post": {
				"summary": "Compute a structured analysis in the macro grammar",
				"description": "Accepts the structured Analysis object used by the macro grammar. It shares compilation, execution, response envelopes, and refusals with the GET expression-address form.",
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"type": "object"
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "Observations with the compiled plan attached"
					},
					"400": {
						"description": "Malformed JSON or an Analysis shape the parser refuses"
					},
					"404": {
						"description": "A named series is not in the catalog"
					},
					"422": {
						"description": "Well-formed but refused; refusals name what would have been wrong"
					},
					"451": {
						"description": "An input may be displayed but not redistributed"
					}
				}
			}
		},
		"/breadth": {
			"get": {
				"operationId": "getCrossCountryBreadth",
				"summary": "Count cross-country tail breadth by concept",
				"description": "Counts the economies observed for each concept in the representative calendar-month bucket and enumerates which sit in their own high or low tail. Each concept includes member_readings with every covered member's exact date, computed bucket, series id, and status (high, low, middle, suppressed, or unrankable). Calendar-month boundaries are coarse: adjacent dates across a month end do not align, while dates farther apart within one month do. The rule is at /claims#cross-country-breadth. ?as_of and ?vintage refuse (400): selected series are read from the current-only indicators_observations projection, and no archive of derived per-concept breadth snapshots is held. Per-series input vintages are at /api/v1/series/{id}/as-of.",
				"responses": {
					"200": {
						"description": "Concept breadth counts and dated economy sets."
					},
					"400": {
						"description": "Vintage query parameters are not accepted."
					}
				}
			}
		},
		"/ranked": {
			"get": {
				"operationId": "getCrossAssetRank",
				"summary": "Rank current cross-asset readings within their own histories",
				"description": "Ranks each stated asset series by its distance from the midpoint of its own trailing-window percentile. Licensed raw levels are withheld while derived ranks and percentiles remain present. ?as_of and ?vintage refuse (400): the nine candidate series are read from the current-only indicators_observations projection, and no archive of derived cross-asset ranking snapshots is held. Per-series input vintages are at /api/v1/series/{id}/as-of.",
				"responses": {
					"200": {
						"description": "Dated ranks, exclusions, and the enumeration rule."
					},
					"400": {
						"description": "Vintage query parameters are not accepted."
					}
				}
			}
		},
		"/exposure": {
			"post": {
				"operationId": "postExposure",
				"summary": "Count and date conditional-history windows for a weighted basket",
				"description": "Enumerates every past window where the stated conditions held and distributes the weighted basket changes after the stated horizon. The basket accepts one to ten distinct, positive-weight members of one frequency; weights must sum to 1. Raw member start and end values are null for non-redistributable series, while the derived basket change remains present; `recompute.licensed_series` names licensed condition and basket inputs that must be fetched from their publishers.",
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/ExposurePostRequest"
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "Matched windows, counts, basket-change distribution, compiled steps, and recompute licensing details.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Envelope"
								}
							}
						}
					},
					"400": {
						"description": "Malformed JSON or a request shape the parser refuses. Codes: `exposure/bad-body`, `exposure/no-basket`, `exposure/too-many-members`, `exposure/bad-member`, `exposure/bad-series-id`, `exposure/bad-weight`, `study/no-conditions`, `study/too-many-conditions`, `condition/bad-shape`, `condition/bad-kind`, `condition/bad-comparator`, `condition/bad-threshold`, `condition/bad-percentile`, `condition/bad-window`, `condition/bad-over`, `expr/empty`, `expr/dangling-operator`, `expr/unknown-operator`, `expr/not-addressable`, `series/bad-id`, `transform/bad-argument`, `transform/unexpected-argument`, `transform/unknown`, `range/bad-date`, `range/reversed`, `vintage/required`, `vintage/bad-date`, `exposure/bad-horizon`, `exposure/bad-dedupe`.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/PostParseErrorEnvelope"
								}
							}
						}
					},
					"422": {
						"description": "A well-formed request the compiler or basket engine refuses. Codes: `arithmetic/incompatible-units`, `arithmetic/different-concepts`, `arithmetic/mixed-seasonality`, `arithmetic/currency-not-stated`, `arithmetic/index-levels-not-comparable`, `frequency/mismatch`, `series/unknown`, `series/unregistered`, `series/not-ingested`, `vintage/not-yet-served`, `vintage/ranking-not-available-as-of`, `vintage/range-after-as-of`, `transform/mom-on-unadjusted`, `transform/needs-level-or-rate`, `transform/annualize-needs-level`, `transform/index-needs-level`, `condition/threshold-on-level`, `condition/percentile-range`, `condition/percentile-window`, `condition/direction-span`, `study/thin-sample`, `basket/empty`, `basket/too-many-members`, `basket/duplicate-member`, `basket/mixed-frequency`, `basket/percent-unit-member`, `basket/non-positive-weight`, `basket/weight-sum`.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/PostRefusalEnvelope"
								}
							}
						}
					}
				}
			}
		}
	},
	"components": {
		"parameters": {
			"SeriesId": {
				"name": "id",
				"in": "path",
				"required": true,
				"description": "A FRED-style series id (e.g. CPIAUCSL) or the site's url slug (e.g. inflation-rate).",
				"schema": {
					"type": "string"
				},
				"example": "CPIAUCSL"
			},
			"Limit": {
				"name": "limit",
				"in": "query",
				"required": false,
				"description": "Cap on the number of most-recent observations returned.",
				"schema": {
					"type": "integer",
					"minimum": 1,
					"maximum": 50000,
					"default": 5000
				}
			},
			"Derived": {
				"name": "derived",
				"in": "query",
				"required": false,
				"description": "When true, adds percentile_10y, percentile_30y, and z_score to each observation.",
				"schema": {
					"type": "boolean",
					"default": false
				}
			},
			"Format": {
				"name": "format",
				"in": "query",
				"required": false,
				"description": "When set to csv, returns a CSV download with an attribution footer instead of JSON.",
				"schema": {
					"type": "string",
					"enum": ["csv"]
				}
			},
			"History": {
				"name": "history",
				"in": "query",
				"required": false,
				"description": "When true, includes the full walk-forward or causal history alongside the current reading.",
				"schema": {
					"type": "boolean",
					"default": false
				}
			}
		},
		"responses": {
			"LicensedSeries": {
				"description": "Series is not redistributable through this API; observations are metadata-only. The response includes a license_note and a chart_url pointing at the interactive chart on the site.",
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/LicensedSeriesErrorEnvelope"
						}
					}
				}
			}
		},
		"schemas": {
			"Meta": {
				"type": "object",
				"description": "Attribution and provenance metadata attached to every response.",
				"properties": {
					"source": {
						"type": "string",
						"example": "MacroRadar"
					},
					"attribution": {
						"type": "string"
					},
					"license": {
						"type": "string"
					},
					"docs": {
						"type": "string",
						"format": "uri",
						"example": "https://www.macroradar.io/developers"
					},
					"generated_at": {
						"type": "string",
						"format": "date-time"
					}
				},
				"additionalProperties": true
			},
			"Envelope": {
				"type": "object",
				"description": "The standard response envelope. `data` shape varies by endpoint.",
				"properties": {
					"data": {},
					"meta": {
						"$ref": "#/components/schemas/Meta"
					}
				},
				"required": ["data", "meta"]
			},
			"ErrorEnvelope": {
				"type": "object",
				"properties": {
					"error": {
						"type": "string"
					},
					"meta": {
						"$ref": "#/components/schemas/Meta"
					}
				},
				"required": ["error", "meta"]
			},
			"LicensedSeriesErrorEnvelope": {
				"allOf": [
					{
						"$ref": "#/components/schemas/ErrorEnvelope"
					},
					{
						"type": "object",
						"properties": {
							"meta": {
								"allOf": [
									{
										"$ref": "#/components/schemas/Meta"
									},
									{
										"type": "object",
										"properties": {
											"series_id": {
												"type": "string"
											},
											"license_note": {
												"type": "string"
											},
											"chart_url": {
												"type": "string",
												"format": "uri"
											},
											"note": {
												"type": "string"
											}
										}
									}
								]
							}
						}
					}
				]
			},
			"SeriesCatalogEntry": {
				"type": "object",
				"properties": {
					"series_id": {
						"type": "string",
						"example": "CPIAUCSL"
					},
					"slug": {
						"type": "string",
						"example": "inflation-rate"
					},
					"name": {
						"type": "string"
					},
					"category": {
						"type": "string"
					},
					"frequency": {
						"type": "string",
						"enum": ["daily", "weekly", "monthly", "quarterly"]
					},
					"units": {
						"type": "string"
					},
					"description": {
						"type": "string"
					},
					"concept": {
						"type": "string",
						"example": "consumer-price-level",
						"description": "What the series MEASURES, as distinct from what it is called and what unit it is quoted in. The compute grammar refuses to combine two levels of different concepts, so this says in advance which pairs it will accept: INDPRO and PCEPI are both \"Index 2017=100\" and their difference is not a quantity."
					},
					"geography": {
						"type": "string",
						"example": "US",
						"description": "The economy the series measures. Also resolves a unit string that names no currency: \"Millions of Domestic Currency\" is a template the country fills in, and two of them in different economies are refused."
					},
					"publisher": {
						"type": "string",
						"example": "US Bureau of Labor Statistics"
					},
					"seasonality": {
						"type": "string",
						"enum": ["adjusted", "unadjusted", "not-applicable"],
						"description": "Three states, not FRED's two. \"unadjusted\" refuses a month-over-month change; \"not-applicable\" is a market close, where mom is an ordinary question."
					},
					"observation_start": {
						"type": "string",
						"format": "date",
						"description": "The publisher's first observation, which bounds any study over the series."
					},
					"discontinued": {
						"type": ["string", "null"],
						"format": "date",
						"example": "2021-06-01",
						"description": "The publisher's LAST observation, where the publisher has stopped. null means the series is still being published. Without this an agent ranking economies on a concept cannot tell a live series from a wound-down one, and would report a 2021 reading against a 2026 one as a comparison."
					},
					"revisions": {
						"type": "string",
						"enum": ["published-once", "revised"],
						"description": "\"published-once\" means the publisher never restates a figure, which is exactly when an as-of question can be answered from the current record."
					},
					"observations_available": {
						"type": "boolean",
						"description": "Whether the serving layer actually holds observations, as distinct from whether the series is defined. The registry is the semantic layer and runs ahead of the data: /compute can reason about a series it cannot yet read."
					},
					"price_basis": {
						"type": "object",
						"description": "On ambiguous price series only: what the number IS and the near neighbour it is not. MacroRadar's Brent is the EIA spot price, not the ICE front-month future.",
						"properties": {
							"kind": {
								"type": "string"
							},
							"instrument": {
								"type": "string"
							},
							"publisher": {
								"type": "string"
							},
							"not_this": {
								"type": "string"
							},
							"quoted_as": {
								"type": "string"
							}
						}
					},
					"redistributable": {
						"type": "boolean"
					},
					"license_note": {
						"type": "string"
					},
					"chart_url": {
						"type": "string",
						"format": "uri"
					},
					"api_url": {
						"type": "string",
						"format": "uri"
					},
					"csv_url": {
						"type": "string",
						"format": "uri"
					}
				},
				"required": [
					"series_id",
					"slug",
					"name",
					"category",
					"frequency",
					"units",
					"description",
					"redistributable",
					"chart_url",
					"concept",
					"geography",
					"publisher",
					"seasonality",
					"observation_start",
					"revisions",
					"observations_available"
				]
			},
			"CatalogEnvelope": {
				"type": "object",
				"properties": {
					"data": {
						"type": "object",
						"properties": {
							"concepts": {
								"type": "array",
								"items": {
									"$ref": "#/components/schemas/ConceptCoverage"
								},
								"description": "The semantic layer, grouped and counted. Answers \"which concepts are cross-country, and for which economies\" without the caller deriving it — derived blind it is wrong three ways: a stopped series looks live, several national variants look like several economies, and a currency pair looks like a country."
							},
							"series": {
								"type": "array",
								"items": {
									"$ref": "#/components/schemas/SeriesCatalogEntry"
								}
							},
							"models": {
								"type": "array",
								"items": {
									"type": "object",
									"properties": {
										"name": {
											"type": "string"
										},
										"api_url": {
											"type": "string",
											"format": "uri"
										},
										"history_url": {
											"type": "string",
											"format": "uri"
										}
									},
									"required": ["name", "api_url"]
								}
							}
						},
						"required": ["concepts", "series", "models"]
					},
					"meta": {
						"$ref": "#/components/schemas/Meta"
					}
				},
				"required": ["data", "meta"]
			},
			"Observation": {
				"type": "object",
				"properties": {
					"date": {
						"type": "string",
						"format": "date"
					},
					"value": {
						"type": "number",
						"nullable": true
					},
					"percentile_10y": {
						"type": "number",
						"nullable": true
					},
					"percentile_30y": {
						"type": "number",
						"nullable": true
					},
					"z_score": {
						"type": "number",
						"nullable": true
					}
				},
				"required": ["date", "value"]
			},
			"SeriesObservationsEnvelope": {
				"type": "object",
				"properties": {
					"data": {
						"type": "object",
						"properties": {
							"series": {
								"$ref": "#/components/schemas/SeriesCatalogEntry"
							},
							"count": {
								"type": "integer"
							},
							"observations": {
								"type": "array",
								"items": {
									"$ref": "#/components/schemas/Observation"
								}
							}
						},
						"required": ["series", "count", "observations"]
					},
					"meta": {
						"$ref": "#/components/schemas/Meta"
					}
				},
				"required": ["data", "meta"]
			},
			"SeriesLatestEnvelope": {
				"type": "object",
				"properties": {
					"data": {
						"type": "object",
						"properties": {
							"series": {
								"$ref": "#/components/schemas/SeriesCatalogEntry"
							},
							"date": {
								"type": "string",
								"format": "date"
							},
							"value": {
								"type": "number",
								"nullable": true
							},
							"percentile_10y": {
								"type": "number",
								"nullable": true
							},
							"percentile_30y": {
								"type": "number",
								"nullable": true
							},
							"z_score": {
								"type": "number",
								"nullable": true
							}
						},
						"required": ["series", "date", "value"]
					},
					"meta": {
						"$ref": "#/components/schemas/Meta"
					}
				},
				"required": ["data", "meta"]
			},
			"ConceptCoverage": {
				"type": "object",
				"description": "Coverage for one concept: how many economies it actually reaches, and which series have stopped. Counted from the registry by apps/web/lib/concept-coverage.ts, the same rule that generates docs/coverage.md, so the endpoint and the document cannot disagree.",
				"properties": {
					"concept": {
						"type": "string",
						"example": "government-bond-yield"
					},
					"economies": {
						"type": "array",
						"items": {
							"type": "string"
						},
						"example": ["CA", "DE", "FR", "GB", "JP", "US"],
						"description": "Distinct economies with at least one LIVE series. NOT the series count — consumer-price-level has eight live series and reaches two economies, because most are United States variants. Excludes economies whose only series has stopped."
					},
					"scopes": {
						"type": "array",
						"items": {
							"type": "string"
						},
						"example": ["US/CA", "US/JP"],
						"description": "Live geographies that are not economies: `world` for prices belonging to no country, and bilateral pairs like `US/CA` for exchange rates. A separate field because a bilateral rate is not national coverage."
					},
					"series_defined": {
						"type": "integer",
						"description": "Every series carrying the concept, live or stopped."
					},
					"series_live": {
						"type": "integer",
						"description": "Series the publisher is still publishing."
					},
					"single_economy": {
						"type": "boolean",
						"description": "True when the concept reaches exactly one economy. This is the only breadth threshold in the code: one economy is a reading, not a pattern. False when it reaches none, which is a different state."
					},
					"related_concepts": {
						"type": "array",
						"items": {
							"type": "string"
						},
						"example": [
							"breakeven-inflation",
							"corporate-bond-yield",
							"credit-spread",
							"mortgage-rate",
							"real-bond-yield"
						],
						"description": "Which other concepts this one is conventionally compared against — citation, never causation: the same published release, a textbook accounting identity, a named empirical or theoretical regularity, or a named composite index. Never what the relationship means or which side moves the other. See RELATED_CONCEPTS in @macroradar/shared, derived from a cited edge list (CONCEPT_EDGES) where every pairing names its specific source. Empty when no citable convention was found; density was never the goal."
					},
					"discontinued": {
						"type": "array",
						"description": "Series the publisher has wound down, oldest last-observation first. Kept rather than deleted: the history is real, but it cannot answer what the reading is now.",
						"items": {
							"type": "object",
							"properties": {
								"series_id": {
									"type": "string"
								},
								"geography": {
									"type": "string"
								},
								"last_observation": {
									"type": "string",
									"format": "date"
								}
							}
						}
					},
					"series": {
						"type": "array",
						"description": "Every series carrying the concept, live or stopped.",
						"items": {
							"type": "object",
							"properties": {
								"series_id": {
									"type": "string"
								},
								"geography": {
									"type": "string"
								}
							}
						}
					}
				},
				"required": [
					"concept",
					"economies",
					"scopes",
					"series_defined",
					"series_live",
					"single_economy",
					"related_concepts",
					"discontinued",
					"series"
				]
			},
			"Expression": {
				"description": "A structured macro expression. Binary right operands may not themselves be binary because the URL grammar has no grouping.",
				"oneOf": [
					{
						"$ref": "#/components/schemas/SeriesExpression"
					},
					{
						"$ref": "#/components/schemas/BinaryExpression"
					}
				]
			},
			"SeriesExpression": {
				"type": "object",
				"properties": {
					"kind": {
						"const": "series"
					},
					"seriesId": {
						"type": "string",
						"pattern": "^[A-Za-z0-9_]+$"
					},
					"transforms": {
						"type": "array",
						"items": {
							"$ref": "#/components/schemas/Transform"
						}
					}
				},
				"required": ["kind", "seriesId"]
			},
			"BinaryExpression": {
				"type": "object",
				"properties": {
					"kind": {
						"const": "binary"
					},
					"op": {
						"type": "string",
						"enum": ["+", "-", "*", "/"]
					},
					"left": {
						"$ref": "#/components/schemas/Expression"
					},
					"right": {
						"$ref": "#/components/schemas/SeriesExpression"
					}
				},
				"required": ["kind", "op", "left", "right"]
			},
			"Range": {
				"type": "object",
				"properties": {
					"start": {
						"type": "string",
						"format": "date"
					},
					"end": {
						"type": "string",
						"format": "date"
					}
				},
				"required": ["start", "end"]
			},
			"Vintage": {
				"oneOf": [
					{
						"type": "object",
						"properties": {
							"mode": {
								"const": "latest"
							}
						},
						"required": ["mode"]
					},
					{
						"type": "object",
						"properties": {
							"mode": {
								"const": "as_of"
							},
							"date": {
								"type": "string",
								"format": "date"
							}
						},
						"required": ["mode", "date"]
					}
				]
			},
			"ThresholdCondition": {
				"type": "object",
				"properties": {
					"kind": {
						"const": "threshold"
					},
					"expr": {
						"$ref": "#/components/schemas/Expression"
					},
					"comparator": {
						"type": "string",
						"enum": ["above", "below"]
					},
					"value": {
						"type": "number"
					}
				},
				"required": ["kind", "expr", "comparator", "value"]
			},
			"PercentileCondition": {
				"type": "object",
				"properties": {
					"kind": {
						"const": "percentile"
					},
					"expr": {
						"$ref": "#/components/schemas/Expression"
					},
					"comparator": {
						"type": "string",
						"enum": ["above", "below"]
					},
					"percentile": {
						"type": "number",
						"minimum": 0,
						"maximum": 100
					},
					"window": {
						"type": "integer",
						"minimum": 1,
						"maximum": 10000
					}
				},
				"required": ["kind", "expr", "comparator", "percentile", "window"]
			},
			"DirectionCondition": {
				"type": "object",
				"properties": {
					"kind": {
						"const": "direction"
					},
					"expr": {
						"$ref": "#/components/schemas/Expression"
					},
					"comparator": {
						"type": "string",
						"enum": ["rising", "falling"]
					},
					"over": {
						"type": "integer",
						"minimum": 1,
						"maximum": 10000
					}
				},
				"required": ["kind", "expr", "comparator", "over"]
			},
			"StudyPostRequest": {
				"type": "object",
				"properties": {
					"conditions": {
						"type": "array",
						"minItems": 1,
						"maxItems": 8,
						"items": {
							"oneOf": [
								{
									"$ref": "#/components/schemas/ThresholdCondition"
								},
								{
									"$ref": "#/components/schemas/PercentileCondition"
								},
								{
									"$ref": "#/components/schemas/DirectionCondition"
								}
							]
						}
					},
					"measure": {
						"$ref": "#/components/schemas/Expression"
					},
					"horizonPeriods": {
						"type": "integer",
						"minimum": 1,
						"maximum": 10000
					},
					"range": {
						"$ref": "#/components/schemas/Range"
					},
					"dedupeWithin": {
						"type": "integer",
						"minimum": 0,
						"default": 0
					},
					"vintage": {
						"$ref": "#/components/schemas/Vintage"
					}
				},
				"required": ["conditions", "measure", "horizonPeriods", "range", "vintage"]
			},
			"BasketMember": {
				"type": "object",
				"properties": {
					"series_id": {
						"type": "string",
						"pattern": "^[A-Za-z0-9_]+$"
					},
					"weight": {
						"type": "number",
						"description": "A finite number. The engine refuses zero or negative weights and requires all weights to sum to 1."
					}
				},
				"required": ["series_id", "weight"]
			},
			"ExposurePostRequest": {
				"type": "object",
				"properties": {
					"basket": {
						"type": "array",
						"minItems": 1,
						"maxItems": 10,
						"items": {
							"$ref": "#/components/schemas/BasketMember"
						}
					},
					"conditions": {
						"type": "array",
						"minItems": 1,
						"maxItems": 8,
						"items": {
							"oneOf": [
								{
									"$ref": "#/components/schemas/ThresholdCondition"
								},
								{
									"$ref": "#/components/schemas/PercentileCondition"
								},
								{
									"$ref": "#/components/schemas/DirectionCondition"
								}
							]
						}
					},
					"horizonPeriods": {
						"type": "integer",
						"minimum": 1,
						"maximum": 10000
					},
					"range": {
						"$ref": "#/components/schemas/Range"
					},
					"dedupeWithin": {
						"type": "integer",
						"minimum": 0,
						"default": 0
					},
					"vintage": {
						"$ref": "#/components/schemas/Vintage"
					}
				},
				"required": ["basket", "conditions", "horizonPeriods", "range", "vintage"]
			},
			"PostProblem": {
				"type": "object",
				"properties": {
					"code": {
						"type": "string"
					},
					"input": {
						"type": "string"
					},
					"message": {
						"type": "string"
					}
				},
				"required": ["code", "message"]
			},
			"PostParseErrorEnvelope": {
				"allOf": [
					{
						"$ref": "#/components/schemas/ErrorEnvelope"
					},
					{
						"type": "object",
						"properties": {
							"errors": {
								"type": "array",
								"items": {
									"$ref": "#/components/schemas/PostProblem"
								}
							}
						},
						"required": ["errors"]
					}
				]
			},
			"PostRefusalEnvelope": {
				"allOf": [
					{
						"$ref": "#/components/schemas/ErrorEnvelope"
					},
					{
						"type": "object",
						"properties": {
							"refusals": {
								"type": "array",
								"items": {
									"$ref": "#/components/schemas/PostProblem"
								}
							}
						},
						"required": ["refusals"]
					}
				]
			},
			"Transform": {
				"description": "A transform read left to right; observation-count arguments are capped at 10,000.",
				"oneOf": [
					{
						"type": "object",
						"properties": {
							"op": {
								"type": "string",
								"enum": ["yoy", "mom"]
							}
						},
						"required": ["op"]
					},
					{
						"type": "object",
						"properties": {
							"op": {
								"type": "string",
								"enum": ["diff", "lag", "annualized"]
							},
							"periods": {
								"type": "integer",
								"minimum": 1,
								"maximum": 10000
							}
						},
						"required": ["op", "periods"]
					},
					{
						"type": "object",
						"properties": {
							"op": {
								"type": "string",
								"enum": ["zscore", "percentile", "movingAverage"]
							},
							"window": {
								"type": "integer",
								"minimum": 1,
								"maximum": 10000
							}
						},
						"required": ["op", "window"]
					},
					{
						"type": "object",
						"properties": {
							"op": {
								"const": "index"
							},
							"baseDate": {
								"type": "string",
								"format": "date"
							}
						},
						"required": ["op", "baseDate"]
					}
				]
			}
		}
	}
}
