{"openapi":"3.1.0","jsonSchemaDialect":"https://json-schema.org/draft/2020-12/schema","info":{"title":"HELPERG Data — Weather and Economics API","version":"1.1.0","summary":"Provider-independent weather readings and economic release data, with freshness, provenance and licensing attached to every response.","description":"The weather and economics surfaces of the HELPERG Data Infrastructure layer.\n\n**What every response carries.** Data never travels without its freshness (`meta.cached`, `meta.cacheAgeSeconds`, `meta.stale`, `meta.degraded`, and on the economics surface `meta.lastIngestedAt` and `meta.lastSuccessfulIngestionAt`), its provenance (which source, which adapter at which version, what was transformed, and whether HELPERG derived the value) and its licensing obligation (`meta.attributionRecords`). A consumer that renders the data and ignores the attribution is in breach of the source licence, but it cannot claim it was never told.\n\n**What this deployment is.** The cache is in-process and the rate-limit counter is per instance and per route name. There is no fleet-wide cache and no fleet-wide rate limit here, and none is claimed: the published allowance is what one warm instance will let a caller do, and two requests landing on two instances are counted separately.\n\n**Durable storage is a per-deployment fact, and it decides half this document.** The economics calendar, the observation history and the ingestion routes read persisted state; the weather routes and the economics registry routes do not. Whether the storage-backed half can answer HERE is published at the root of this document as `x-helperg-durable-storage`, derived from the repository this instance actually constructed, and per operation as `x-helperg-requires-durable-storage`. Read them before integrating. At the time of writing, the Preview environment has a durable store bound and Production does not: Production continues to fail those routes closed with 503, exactly as it did before the store existed. `/api/v1/capabilities` answers the same question for a running instance.\n\n**Why a storage-backed route fails closed instead of returning an empty list.** An empty economics corpus is indistinguishable from a real one that happens to have no matching rows. A consumer would cache \"there are no releases\" as a fact about the world, and nothing in the response would contradict it. So an absent corpus is a 503 and a genuinely empty result is a 200 — the distinction is the point.\n\n**Missing means null, always.** Every optional measurement is a value or null, never absent and never zero. `scheduledAt: null` means the source published a date and no time; `value: null` means a reference period the source has scheduled and not yet filled. An explicitly null field is information; an absent one is indistinguishable from a serialisation bug, and a fabricated zero is worse than both.\n\n**No forecasts, no consensus.** HELPERG holds no licensed survey of forecasters, so no consensus, expected or prior figure is published anywhere in this API. Nothing here is a market-impact claim: `importance` is HELPERG editorial methodology and says so on every response.\n\n**Errors.** Branch on `error.code`. Codes are part of the v1 contract; messages are not, and are deliberately non-specific — no stack, no provider body, no URL with credentials in it, and no environment value ever reaches a response. Each operation publishes exactly the codes it can produce as `x-helperg-error-codes`.","contact":{"name":"HELPERG LLC","url":"https://helperg.com/"}},"servers":[{"url":"https://helperg.com","description":"Production. Preview deployments serve the same surface under their own generated hostname and hold DIFFERENT credentials; a production key is not valid against a preview and vice versa. They can also differ in whether durable storage is bound — see `x-helperg-durable-storage`."}],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"tags":[{"name":"weather","description":"Readings sourced from MET Norway's Locationforecast 2.0, remapped onto a provider-independent model. Nothing in the response shape is borrowed from the provider payload, so a second provider can be added without any consumer rewriting its rendering."},{"name":"economics","description":"Scheduled statistical releases and observed values, from BLS, Eurostat and the ECB, normalised onto HELPERG's own closed vocabularies — none of which is any provider's. The registry routes (indicators, series) are PUBLIC definitions compiled into the build; the calendar and observation routes are ECOSYSTEM_INTERNAL and read persisted state. No user request ever reaches a statistical agency."},{"name":"admin","description":"OPERATOR-ONLY ingestion triggers. They are described here so that the surface is reviewable and so that the 404-not-401 behaviour is documented rather than discovered, NOT because a consumer should call them: no ecosystem credential opens one. Their presence in this document says that the SOFTWARE has these routes; it says nothing about whether the deployment serving it has an admin credential provisioned, and a 404 from one is indistinguishable from an unregistered path by design."}],"paths":{"/api/v1/weather/current":{"summary":"Instantaneous conditions at a point","parameters":[{"name":"lat","in":"query","required":true,"description":"Latitude in WGS 84 degrees. Rounded to four decimal places before use. A non-numeric or out-of-range value returns `WEATHER_LOCATION_INVALID`, which is deliberately a different code from `VALIDATION_FAILED` so a consumer can tell \"you sent nonsense\" apart from \"the provider is down\".","schema":{"type":"number","minimum":-90,"maximum":90},"example":50.0755},{"name":"lon","in":"query","required":true,"description":"Longitude in WGS 84 degrees. Rounded to four decimal places before use.","schema":{"type":"number","minimum":-180,"maximum":180},"example":14.4378},{"name":"altitude","in":"query","required":false,"description":"Metres above mean sea level, rounded to a whole number. Omit it and the provider infers an altitude for the point. Outside -500 to 9000 returns `WEATHER_LOCATION_INVALID`.","schema":{"type":"number","minimum":-500,"maximum":9000},"example":245},{"name":"units","in":"query","required":false,"description":"NOT VALIDATED. The route performs one exact, case-sensitive comparison against the string `imperial`; every other value — `metric`, `IMPERIAL`, `Imperial`, `us`, an empty value, or the parameter being absent — yields the metric variant with a 200. An unrecognised value is never an error. See the `CurrentWeatherImperial` schema for what `imperial` actually changes: it is additive, not a conversion.","schema":{"type":"string"},"example":"imperial","x-helperg-validated":false,"x-helperg-imperial-trigger":"imperial"}],"get":{"operationId":"getWeatherCurrent","summary":"Instantaneous conditions at a point","description":"Current conditions for one WGS 84 coordinate, normalised into the canonical HELPERG weather model.\n\nThe response is served from an in-process cache of the provider payload, refreshed no more often than the provider's own `Expires` header permits — one provider request per location per TTL, never one per call. When the provider fails, the last good payload is served with `meta.degraded` true and its TRUE age in `meta.cacheAgeSeconds`. Nothing is estimated and no field is back-filled; if no cached payload exists, the request fails rather than inventing one.\n\nThis route is ECOSYSTEM_INTERNAL rather than public even though the underlying data is openly licensed. An unauthenticated endpoint keyed on caller-supplied coordinates can be cache-busted by varying the fourth decimal place, turning every request into an upstream call; the provider documents a ceiling of 20 requests per second per application, and breaching it throttles every HELPERG consumer at once.","tags":["weather"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"200":{"description":"Current conditions. `data.units` announces which variant is enclosed; `data.current.location` reflects the coordinates and altitude the PROVIDER resolved for the point, which may carry an altitude even when the request supplied none.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WeatherCurrentResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"502":{"$ref":"#/components/responses/Error502"},"503":{"$ref":"#/components/responses/Error503"},"504":{"$ref":"#/components/responses/Error504"}},"x-helperg-access":"ECOSYSTEM_INTERNAL","x-helperg-required-scope":"weather:read","x-helperg-route-name":"v1.weather.current","x-helperg-requires-durable-storage":false,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED","WEATHER_LOCATION_INVALID","SOURCE_DISABLED","UPSTREAM_UNAVAILABLE","UPSTREAM_INVALID_PAYLOAD","UPSTREAM_PAYLOAD_TOO_LARGE","UPSTREAM_TIMEOUT"]},"head":{"operationId":"getWeatherCurrentHead","summary":"Instantaneous conditions at a point (headers only)","description":"Current conditions for one WGS 84 coordinate, normalised into the canonical HELPERG weather model.\n\nThe response is served from an in-process cache of the provider payload, refreshed no more often than the provider's own `Expires` header permits — one provider request per location per TTL, never one per call. When the provider fails, the last good payload is served with `meta.degraded` true and its TRUE age in `meta.cacheAgeSeconds`. Nothing is estimated and no field is back-filled; if no cached payload exists, the request fails rather than inventing one.\n\nThis route is ECOSYSTEM_INTERNAL rather than public even though the underlying data is openly licensed. An unauthenticated endpoint keyed on caller-supplied coordinates can be cache-busted by varying the fourth decimal place, turning every request into an upstream call; the provider documents a ceiling of 20 requests per second per application, and breaching it throttles every HELPERG consumer at once.","tags":["weather"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"200":{"description":"Current conditions. `data.units` announces which variant is enclosed; `data.current.location` reflects the coordinates and altitude the PROVIDER resolved for the point, which may carry an altitude even when the request supplied none. HEAD returns the status, the headers and NO BODY; it is answered wherever GET is, because most uptime monitors send HEAD by default and a route that refused it would report itself unsupported to the tooling most likely to poll it.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"502":{"$ref":"#/components/responses/Error502"},"503":{"$ref":"#/components/responses/Error503"},"504":{"$ref":"#/components/responses/Error504"}},"x-helperg-access":"ECOSYSTEM_INTERNAL","x-helperg-required-scope":"weather:read","x-helperg-route-name":"v1.weather.current","x-helperg-requires-durable-storage":false,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED","WEATHER_LOCATION_INVALID","SOURCE_DISABLED","UPSTREAM_UNAVAILABLE","UPSTREAM_INVALID_PAYLOAD","UPSTREAM_PAYLOAD_TOO_LARGE","UPSTREAM_TIMEOUT"]}},"/api/v1/weather/forecast":{"summary":"Hourly and daily forecast for a point","parameters":[{"name":"lat","in":"query","required":true,"description":"Latitude in WGS 84 degrees. Rounded to four decimal places before use. A non-numeric or out-of-range value returns `WEATHER_LOCATION_INVALID`, which is deliberately a different code from `VALIDATION_FAILED` so a consumer can tell \"you sent nonsense\" apart from \"the provider is down\".","schema":{"type":"number","minimum":-90,"maximum":90},"example":50.0755},{"name":"lon","in":"query","required":true,"description":"Longitude in WGS 84 degrees. Rounded to four decimal places before use.","schema":{"type":"number","minimum":-180,"maximum":180},"example":14.4378},{"name":"altitude","in":"query","required":false,"description":"Metres above mean sea level, rounded to a whole number. Omit it and the provider infers an altitude for the point. Outside -500 to 9000 returns `WEATHER_LOCATION_INVALID`.","schema":{"type":"number","minimum":-500,"maximum":9000},"example":245},{"name":"tz","in":"query","required":false,"description":"IANA timezone identifier deciding where day boundaries fall, and therefore which forecast lands on which calendar day. Defaults to `UTC` rather than to anything derived from the request: guessing a caller's zone from an IP or a header is exactly how a forecast ends up filed under the wrong day. An unknown zone returns `VALIDATION_FAILED`; it is never quietly defaulted.","schema":{"type":"string","default":"UTC"},"example":"Europe/Prague"},{"name":"limit","in":"query","required":false,"description":"Number of calendar days to return in `data.daily`. Defaults to 9. A value above 10 is CLAMPED to 10, not rejected — `limit=30` returns 200 with at most ten days. A value below 1, or one that does not begin with an integer, returns `VALIDATION_FAILED`. The value is parsed with a base-ten integer parse, so `3.7` is read as 3. It truncates `data.daily` only; `data.hourly` is unaffected. Fewer days than requested is normal — the provider supplies what it supplies, and `meta.dayCount` states what arrived.","schema":{"type":"integer","minimum":1,"default":9},"example":5,"x-helperg-clamped-maximum":10}],"get":{"operationId":"getWeatherForecast","summary":"Hourly and daily forecast for a point","description":"A forecast for one WGS 84 coordinate, as hourly entries and as calendar days in the requested timezone.\n\nDaily records are assembled by HELPERG from sub-daily provider values — the provider publishes no day record — so each carries `derivation: \"HELPERG_DERIVED\"` and `AGGREGATION` in its `transformation` list. Overlapping provider windows are never double-counted, and the first and last day of the range are normally partial.\n\nCAUTION: the route's own source header advertises a `days=` parameter. The code reads `limit`. `days` is accepted by the query string and ignored, with no error — see `x-helperg-ignored-parameters` on this operation.\n\nThe same cache entry backs this route and the current-conditions route, so asking for both at one location costs one provider request. Timezone is a presentation parameter and is not part of the cache key.","tags":["weather"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"200":{"description":"A forecast. `data.location` is the ROUNDED REQUESTED point, which differs from the current-conditions route, where the location comes from the provider payload.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WeatherForecastResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"502":{"$ref":"#/components/responses/Error502"},"503":{"$ref":"#/components/responses/Error503"},"504":{"$ref":"#/components/responses/Error504"}},"x-helperg-access":"ECOSYSTEM_INTERNAL","x-helperg-required-scope":"weather:read","x-helperg-route-name":"v1.weather.forecast","x-helperg-requires-durable-storage":false,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED","WEATHER_LOCATION_INVALID","SOURCE_DISABLED","UPSTREAM_UNAVAILABLE","UPSTREAM_INVALID_PAYLOAD","UPSTREAM_PAYLOAD_TOO_LARGE","UPSTREAM_TIMEOUT"],"x-helperg-ignored-parameters":["days"]},"head":{"operationId":"getWeatherForecastHead","summary":"Hourly and daily forecast for a point (headers only)","description":"A forecast for one WGS 84 coordinate, as hourly entries and as calendar days in the requested timezone.\n\nDaily records are assembled by HELPERG from sub-daily provider values — the provider publishes no day record — so each carries `derivation: \"HELPERG_DERIVED\"` and `AGGREGATION` in its `transformation` list. Overlapping provider windows are never double-counted, and the first and last day of the range are normally partial.\n\nCAUTION: the route's own source header advertises a `days=` parameter. The code reads `limit`. `days` is accepted by the query string and ignored, with no error — see `x-helperg-ignored-parameters` on this operation.\n\nThe same cache entry backs this route and the current-conditions route, so asking for both at one location costs one provider request. Timezone is a presentation parameter and is not part of the cache key.","tags":["weather"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"200":{"description":"A forecast. `data.location` is the ROUNDED REQUESTED point, which differs from the current-conditions route, where the location comes from the provider payload. HEAD returns the status, the headers and NO BODY; it is answered wherever GET is, because most uptime monitors send HEAD by default and a route that refused it would report itself unsupported to the tooling most likely to poll it.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"502":{"$ref":"#/components/responses/Error502"},"503":{"$ref":"#/components/responses/Error503"},"504":{"$ref":"#/components/responses/Error504"}},"x-helperg-access":"ECOSYSTEM_INTERNAL","x-helperg-required-scope":"weather:read","x-helperg-route-name":"v1.weather.forecast","x-helperg-requires-durable-storage":false,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED","WEATHER_LOCATION_INVALID","SOURCE_DISABLED","UPSTREAM_UNAVAILABLE","UPSTREAM_INVALID_PAYLOAD","UPSTREAM_PAYLOAD_TOO_LARGE","UPSTREAM_TIMEOUT"],"x-helperg-ignored-parameters":["days"]}},"/api/v1/economics/events":{"summary":"Scheduled statistical releases, as a flat list","parameters":[{"name":"tz","in":"query","required":false,"description":"IANA timezone identifier. It does two things, and they are easy to confuse. First, it is the zone a bare `YYYY-MM-DD` in `from` or `to` is interpreted in — `from=2026-01-15&tz=Asia/Tokyo` means the start of the 15th in Tokyo. Second, it is the zone the local projection is computed in. Defaults to `UTC`, never to anything derived from the caller's IP or headers. An unknown zone returns `VALIDATION_FAILED` and is never quietly defaulted.","schema":{"type":"string","default":"UTC"},"example":"Europe/Prague"},{"name":"from","in":"query","required":false,"description":"Start of the window. Accepts a bare `YYYY-MM-DD`, interpreted as midnight IN `tz`, or an instant carrying an explicit `Z` or `+hh:mm` offset. A timestamp with NO zone information is REFUSED with `ECONOMICS_RANGE_INVALID` rather than resolved against the server clock, which is the classic calendar-shift bug. Anything unparsable returns the same code. BE AWARE OF WHAT IT IS COMPARED WITH: the parsed instant is reduced to its UTC calendar date and matched against each event's `scheduledDate`, which is a date in the SOURCE'S zone. With a non-UTC `tz` the boundary can therefore land one day either side of the date you wrote.","schema":{"type":"string"},"example":"2026-01-01"},{"name":"to","in":"query","required":false,"description":"End of the window, same accepted forms as `from`. A bare `YYYY-MM-DD` is expanded to the START of the FOLLOWING day in `tz`, so the window covers the whole day you named — but because the comparison is then made on that instant's UTC calendar date, `to=2026-01-31` also admits events dated 2026-02-01. Send an explicit instant if you need the boundary exact. A range whose start is not before its end, or one longer than 400 days, returns `ECONOMICS_RANGE_INVALID`.","schema":{"type":"string"},"example":"2026-01-31"},{"name":"country","in":"query","required":false,"description":"Comma-separated ISO 3166-1 alpha-2 codes. Matched case-insensitively. At most 25 values; more returns `VALIDATION_FAILED`. An event filed under a REGION rather than a country never matches this filter — use `region` for those.","schema":{"type":"string"},"example":"US,CZ","x-helperg-max-items":25},{"name":"region","in":"query","required":false,"description":"Comma-separated HELPERG region codes. Matched case-insensitively and VALIDATED against the closed vocabulary: an unknown value returns `VALIDATION_FAILED` rather than silently matching nothing. At most 10 values.","schema":{"type":"string","enum":["EA","EU27_2020","G7","G20","OECD","WORLD"]},"example":"EA","x-helperg-max-items":10},{"name":"currency","in":"query","required":false,"description":"Comma-separated ISO 4217 codes. Matched case-insensitively. NOT validated against a vocabulary — an unregistered code simply matches nothing and returns an empty page. At most 10 values.","schema":{"type":"string"},"example":"EUR","x-helperg-max-items":10,"x-helperg-validated":false},{"name":"importance","in":"query","required":false,"description":"Comma-separated HELPERG importance levels. Matched case-insensitively and VALIDATED: an unknown value returns `VALIDATION_FAILED`. At most 4 values. Importance is editorial methodology, not a measurement of market impact; `event.importanceBasis` records how each level was arrived at.","schema":{"type":"string","enum":["HIGH","MEDIUM","LOW","UNCLASSIFIED"]},"example":"HIGH,MEDIUM","x-helperg-max-items":4},{"name":"release","in":"query","required":false,"description":"Comma-separated release-definition slugs, matched EXACTLY and case-SENSITIVELY — unlike `country`, `region` and `currency`, which are upper-cased first. At most 25 values.","schema":{"type":"string"},"x-helperg-max-items":25},{"name":"source","in":"query","required":false,"description":"Comma-separated registered source slugs (`bls-release-schedule`, `eurostat-release-calendar`, `ecb-meetings-calendar`, `ecb-statistics-calendar`), matched EXACTLY and case-SENSITIVELY. An event reported by more than one source matches any of them, because a second source ATTACHES a reference to the canonical event rather than creating a copy. At most 10 values.","schema":{"type":"string"},"example":"eurostat-release-calendar,ecb-statistics-calendar","x-helperg-max-items":10},{"name":"limit","in":"query","required":false,"description":"Page size. Defaults to 100. A value above 500 is CLAMPED to 500, not rejected — `limit=5000` returns 200 with at most five hundred records. A value below 1, or one that does not begin with an integer, returns `VALIDATION_FAILED`. Parsed with a base-ten integer parse, so `3.7` is read as 3.","schema":{"type":"integer","minimum":1,"default":100},"example":50,"x-helperg-clamped-maximum":500},{"name":"cursor","in":"query","required":false,"description":"OPAQUE page token, taken verbatim from `meta.nextCursor`. Do not construct, parse or store one: it encodes an offset into the FILTERED result set, so it is meaningful only alongside the identical filter arguments, and reusing it with different filters pages through a different set without saying so. A token that does not decode returns `VALIDATION_FAILED`. A token that decodes past the end of the results returns an empty page with a null `nextCursor`, not an error. Paging is NOT a snapshot: an ingestion run can rebuild the corpus between two pages.","schema":{"type":"string"}},{"name":"timezone","in":"query","required":false,"description":"A SWITCH, NOT A ZONE. Sending this parameter with any non-empty value turns on the local projection, adding `localDate`, `localTime` and `localTimeZone` to every entry. Its VALUE IS NEVER READ: the zone comes from `tz` alone, so `?timezone=Asia/Tokyo` with no `tz` localises into UTC and reports `localTimeZone: \"UTC\"`. Send `tz` instead, and send `timezone` only if you want the localised shape while leaving the zone at its default.","schema":{"type":"string"},"x-helperg-value-ignored":true}],"get":{"operationId":"getEconomicEvents","summary":"Scheduled statistical releases, as a flat list","description":"Canonical economic events ordered by scheduled date, each carrying the authoritative source `status` alongside the `derivedState` the clock supports. `/api/v1/economics/calendar` returns the same records grouped by local day.\n\nTWO FIELDS THAT MUST NOT BE CONFLATED. `event.status` is what the SOURCE says and never changes because time passed; `derivedState` is what HELPERG concludes from the clock, is computed per request and is never stored. They are given deliberately different vocabularies so a response, a log line or a database row cannot mix them up.\n\nDATE-ONLY RELEASES. `event.scheduledAt` is null whenever the source published only a date or no time at all. That is a real state, not missing data, and no time is manufactured for it: midnight would move the release into the previous day for every caller west of the source.\n\nLOCALISATION IS OPT-IN AND ADDITIVE. Send `tz` (or `timezone`) and each entry gains `localDate`, `localTime` and `localTimeZone` while keeping its canonical `scheduledDate` and `scheduledAt`. Send neither and those three fields are absent entirely — see the `EconomicsEventsEntry` schema.\n\nNo user request ever reaches BLS, Eurostat or the ECB. Reads are served from persisted state, because a calendar request that fetched live would inherit six agencies' latency, rate limits and outages, and would make HELPERG's availability a function of theirs.\n\nSTORAGE. This route reads persisted state and cannot answer without it. Where no durable store is configured, every call returns 503 `SERVICE_UNAVAILABLE`; where one is configured but no ingestion has ever completed, the same code is returned rather than an empty 200, because an empty calendar is a statement about the world and \"nothing has been ingested here\" is a statement about this deployment. An index that EXISTS and matches nothing is still an honest empty 200. Read `x-helperg-durable-storage` at the root of this document for which of those applies to the deployment serving it.","tags":["economics"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"200":{"description":"Matching events, earliest first. An index that exists and matches nothing returns an empty `entries` with a 200; an index that has never been published returns 503 instead, so an empty list here always means \"no matching releases\", never \"nothing has been ingested\".","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EconomicsEventsResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"ECOSYSTEM_INTERNAL","x-helperg-required-scope":"economics:read","x-helperg-route-name":"v1.economics.events","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED","ECONOMICS_RANGE_INVALID","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"]},"head":{"operationId":"getEconomicEventsHead","summary":"Scheduled statistical releases, as a flat list (headers only)","description":"Canonical economic events ordered by scheduled date, each carrying the authoritative source `status` alongside the `derivedState` the clock supports. `/api/v1/economics/calendar` returns the same records grouped by local day.\n\nTWO FIELDS THAT MUST NOT BE CONFLATED. `event.status` is what the SOURCE says and never changes because time passed; `derivedState` is what HELPERG concludes from the clock, is computed per request and is never stored. They are given deliberately different vocabularies so a response, a log line or a database row cannot mix them up.\n\nDATE-ONLY RELEASES. `event.scheduledAt` is null whenever the source published only a date or no time at all. That is a real state, not missing data, and no time is manufactured for it: midnight would move the release into the previous day for every caller west of the source.\n\nLOCALISATION IS OPT-IN AND ADDITIVE. Send `tz` (or `timezone`) and each entry gains `localDate`, `localTime` and `localTimeZone` while keeping its canonical `scheduledDate` and `scheduledAt`. Send neither and those three fields are absent entirely — see the `EconomicsEventsEntry` schema.\n\nNo user request ever reaches BLS, Eurostat or the ECB. Reads are served from persisted state, because a calendar request that fetched live would inherit six agencies' latency, rate limits and outages, and would make HELPERG's availability a function of theirs.\n\nSTORAGE. This route reads persisted state and cannot answer without it. Where no durable store is configured, every call returns 503 `SERVICE_UNAVAILABLE`; where one is configured but no ingestion has ever completed, the same code is returned rather than an empty 200, because an empty calendar is a statement about the world and \"nothing has been ingested here\" is a statement about this deployment. An index that EXISTS and matches nothing is still an honest empty 200. Read `x-helperg-durable-storage` at the root of this document for which of those applies to the deployment serving it.","tags":["economics"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"200":{"description":"Matching events, earliest first. An index that exists and matches nothing returns an empty `entries` with a 200; an index that has never been published returns 503 instead, so an empty list here always means \"no matching releases\", never \"nothing has been ingested\". HEAD returns the status, the headers and NO BODY; it is answered wherever GET is, because most uptime monitors send HEAD by default and a route that refused it would report itself unsupported to the tooling most likely to poll it.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"ECOSYSTEM_INTERNAL","x-helperg-required-scope":"economics:read","x-helperg-route-name":"v1.economics.events","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED","ECONOMICS_RANGE_INVALID","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"]}},"/api/v1/economics/calendar":{"summary":"Scheduled statistical releases, grouped by local day","parameters":[{"name":"tz","in":"query","required":false,"description":"IANA timezone identifier. It does two things, and they are easy to confuse. First, it is the zone a bare `YYYY-MM-DD` in `from` or `to` is interpreted in — `from=2026-01-15&tz=Asia/Tokyo` means the start of the 15th in Tokyo. Second, it is the zone the local projection is computed in. Defaults to `UTC`, never to anything derived from the caller's IP or headers. An unknown zone returns `VALIDATION_FAILED` and is never quietly defaulted.","schema":{"type":"string","default":"UTC"},"example":"Europe/Prague"},{"name":"from","in":"query","required":false,"description":"Start of the window. Accepts a bare `YYYY-MM-DD`, interpreted as midnight IN `tz`, or an instant carrying an explicit `Z` or `+hh:mm` offset. A timestamp with NO zone information is REFUSED with `ECONOMICS_RANGE_INVALID` rather than resolved against the server clock, which is the classic calendar-shift bug. Anything unparsable returns the same code. BE AWARE OF WHAT IT IS COMPARED WITH: the parsed instant is reduced to its UTC calendar date and matched against each event's `scheduledDate`, which is a date in the SOURCE'S zone. With a non-UTC `tz` the boundary can therefore land one day either side of the date you wrote.","schema":{"type":"string"},"example":"2026-01-01"},{"name":"to","in":"query","required":false,"description":"End of the window, same accepted forms as `from`. A bare `YYYY-MM-DD` is expanded to the START of the FOLLOWING day in `tz`, so the window covers the whole day you named — but because the comparison is then made on that instant's UTC calendar date, `to=2026-01-31` also admits events dated 2026-02-01. Send an explicit instant if you need the boundary exact. A range whose start is not before its end, or one longer than 400 days, returns `ECONOMICS_RANGE_INVALID`.","schema":{"type":"string"},"example":"2026-01-31"},{"name":"country","in":"query","required":false,"description":"Comma-separated ISO 3166-1 alpha-2 codes. Matched case-insensitively. At most 25 values; more returns `VALIDATION_FAILED`. An event filed under a REGION rather than a country never matches this filter — use `region` for those.","schema":{"type":"string"},"example":"US,CZ","x-helperg-max-items":25},{"name":"region","in":"query","required":false,"description":"Comma-separated HELPERG region codes. Matched case-insensitively and VALIDATED against the closed vocabulary: an unknown value returns `VALIDATION_FAILED` rather than silently matching nothing. At most 10 values.","schema":{"type":"string","enum":["EA","EU27_2020","G7","G20","OECD","WORLD"]},"example":"EA","x-helperg-max-items":10},{"name":"currency","in":"query","required":false,"description":"Comma-separated ISO 4217 codes. Matched case-insensitively. NOT validated against a vocabulary — an unregistered code simply matches nothing and returns an empty page. At most 10 values.","schema":{"type":"string"},"example":"EUR","x-helperg-max-items":10,"x-helperg-validated":false},{"name":"importance","in":"query","required":false,"description":"Comma-separated HELPERG importance levels. Matched case-insensitively and VALIDATED: an unknown value returns `VALIDATION_FAILED`. At most 4 values. Importance is editorial methodology, not a measurement of market impact; `event.importanceBasis` records how each level was arrived at.","schema":{"type":"string","enum":["HIGH","MEDIUM","LOW","UNCLASSIFIED"]},"example":"HIGH,MEDIUM","x-helperg-max-items":4},{"name":"release","in":"query","required":false,"description":"Comma-separated release-definition slugs, matched EXACTLY and case-SENSITIVELY — unlike `country`, `region` and `currency`, which are upper-cased first. At most 25 values.","schema":{"type":"string"},"x-helperg-max-items":25},{"name":"source","in":"query","required":false,"description":"Comma-separated registered source slugs (`bls-release-schedule`, `eurostat-release-calendar`, `ecb-meetings-calendar`, `ecb-statistics-calendar`), matched EXACTLY and case-SENSITIVELY. An event reported by more than one source matches any of them, because a second source ATTACHES a reference to the canonical event rather than creating a copy. At most 10 values.","schema":{"type":"string"},"example":"eurostat-release-calendar,ecb-statistics-calendar","x-helperg-max-items":10},{"name":"limit","in":"query","required":false,"description":"Page size. Defaults to 100. A value above 500 is CLAMPED to 500, not rejected — `limit=5000` returns 200 with at most five hundred records. A value below 1, or one that does not begin with an integer, returns `VALIDATION_FAILED`. Parsed with a base-ten integer parse, so `3.7` is read as 3.","schema":{"type":"integer","minimum":1,"default":100},"example":50,"x-helperg-clamped-maximum":500},{"name":"cursor","in":"query","required":false,"description":"OPAQUE page token, taken verbatim from `meta.nextCursor`. Do not construct, parse or store one: it encodes an offset into the FILTERED result set, so it is meaningful only alongside the identical filter arguments, and reusing it with different filters pages through a different set without saying so. A token that does not decode returns `VALIDATION_FAILED`. A token that decodes past the end of the results returns an empty page with a null `nextCursor`, not an error. Paging is NOT a snapshot: an ingestion run can rebuild the corpus between two pages.","schema":{"type":"string"}}],"get":{"operationId":"getEconomicCalendar","summary":"Scheduled statistical releases, grouped by local day","description":"The same records as `/api/v1/economics/events`, grouped into local calendar days and always localised — this route does not need `timezone` to switch the shape on, and ignores it if sent.\n\nGROUPING. A day bucket is keyed on the entry's `localDate` where it has one, and on `event.scheduledDate` where it does not, which is the only date a date-only release has. Days with no matching event are absent rather than present and empty.\n\nPAGING APPLIES TO EVENTS, NOT DAYS. `limit` truncates the underlying event page before grouping, so the last day in a page can be partial and its remainder arrives at the head of the next page. Group across pages rather than treating one page's last day as complete.\n\nNo user request ever reaches BLS, Eurostat or the ECB. Reads are served from persisted state, because a calendar request that fetched live would inherit six agencies' latency, rate limits and outages, and would make HELPERG's availability a function of theirs.\n\nSTORAGE. This route reads persisted state and cannot answer without it. Where no durable store is configured, every call returns 503 `SERVICE_UNAVAILABLE`; where one is configured but no ingestion has ever completed, the same code is returned rather than an empty 200, because an empty calendar is a statement about the world and \"nothing has been ingested here\" is a statement about this deployment. An index that EXISTS and matches nothing is still an honest empty 200. Read `x-helperg-durable-storage` at the root of this document for which of those applies to the deployment serving it.","tags":["economics"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"200":{"description":"Matching events grouped by day, ascending. An empty `days` with a 200 means no release matched; it never means the corpus is missing, which is a 503.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EconomicsCalendarResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"ECOSYSTEM_INTERNAL","x-helperg-required-scope":"economics:read","x-helperg-route-name":"v1.economics.calendar","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED","ECONOMICS_RANGE_INVALID","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"],"x-helperg-ignored-parameters":["timezone"]},"head":{"operationId":"getEconomicCalendarHead","summary":"Scheduled statistical releases, grouped by local day (headers only)","description":"The same records as `/api/v1/economics/events`, grouped into local calendar days and always localised — this route does not need `timezone` to switch the shape on, and ignores it if sent.\n\nGROUPING. A day bucket is keyed on the entry's `localDate` where it has one, and on `event.scheduledDate` where it does not, which is the only date a date-only release has. Days with no matching event are absent rather than present and empty.\n\nPAGING APPLIES TO EVENTS, NOT DAYS. `limit` truncates the underlying event page before grouping, so the last day in a page can be partial and its remainder arrives at the head of the next page. Group across pages rather than treating one page's last day as complete.\n\nNo user request ever reaches BLS, Eurostat or the ECB. Reads are served from persisted state, because a calendar request that fetched live would inherit six agencies' latency, rate limits and outages, and would make HELPERG's availability a function of theirs.\n\nSTORAGE. This route reads persisted state and cannot answer without it. Where no durable store is configured, every call returns 503 `SERVICE_UNAVAILABLE`; where one is configured but no ingestion has ever completed, the same code is returned rather than an empty 200, because an empty calendar is a statement about the world and \"nothing has been ingested here\" is a statement about this deployment. An index that EXISTS and matches nothing is still an honest empty 200. Read `x-helperg-durable-storage` at the root of this document for which of those applies to the deployment serving it.","tags":["economics"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"200":{"description":"Matching events grouped by day, ascending. An empty `days` with a 200 means no release matched; it never means the corpus is missing, which is a 503. HEAD returns the status, the headers and NO BODY; it is answered wherever GET is, because most uptime monitors send HEAD by default and a route that refused it would report itself unsupported to the tooling most likely to poll it.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"ECOSYSTEM_INTERNAL","x-helperg-required-scope":"economics:read","x-helperg-route-name":"v1.economics.calendar","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED","ECONOMICS_RANGE_INVALID","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"],"x-helperg-ignored-parameters":["timezone"]}},"/api/v1/economics/indicators":{"summary":"The indicator family registry","parameters":[{"name":"category","in":"query","required":false,"description":"Restrict the listing to one top-level category. Matched case-insensitively and VALIDATED against the closed vocabulary: an unknown value returns `VALIDATION_FAILED` rather than an empty list, because an empty list would read as \"no such indicators exist\".","schema":{"type":"string","enum":["PRICES","LABOUR","GROWTH","PRODUCTION","CONSUMPTION","TRADE","HOUSING","BUSINESS_ACTIVITY","MONETARY_POLICY","MONEY_AND_CREDIT","PUBLIC_FINANCE","EXTERNAL_SECTOR","CONFIDENCE","WAGES_AND_COSTS","FINANCIAL_STABILITY","OTHER"]},"example":"PRICES"}],"get":{"operationId":"listEconomicIndicators","summary":"The indicator family registry","description":"Broad economic CONCEPTS, not measurable series. \"CPI\" is a family; \"United States CPI, all items, year-on-year\" is a series, and a family has no country, no frequency, no unit and no value.\n\nCPI and HICP are deliberately SEPARATE families rather than one concept with two flavours: HICP uses a harmonised European methodology with different scope, notably owner-occupied housing, so filing them together would invite exactly the comparison the methodologies do not support. `relatedFamilySlugs` records the kinship without asserting equivalence.\n\nPUBLIC, and needs no durable storage: the registry is compiled into the build, so this route answers identically on a deployment whose store is unavailable. It carries no HELPERG operational detail — no feed URLs, no host allowlists, no ingestion state — which is why a credential is not required to read the vocabulary the products must speak.","tags":["economics"],"security":[],"responses":{"200":{"description":"Every active family, plus the complete category vocabulary. There is no paging: the registry is compiled in and bounded.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"ETag":{"$ref":"#/components/headers/ETag"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EconomicsIndicatorsResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"PUBLIC","x-helperg-required-scope":null,"x-helperg-route-name":"v1.economics.indicators","x-helperg-requires-durable-storage":false,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED"],"x-helperg-public-cache-seconds":3600},"head":{"operationId":"listEconomicIndicatorsHead","summary":"The indicator family registry (headers only)","description":"Broad economic CONCEPTS, not measurable series. \"CPI\" is a family; \"United States CPI, all items, year-on-year\" is a series, and a family has no country, no frequency, no unit and no value.\n\nCPI and HICP are deliberately SEPARATE families rather than one concept with two flavours: HICP uses a harmonised European methodology with different scope, notably owner-occupied housing, so filing them together would invite exactly the comparison the methodologies do not support. `relatedFamilySlugs` records the kinship without asserting equivalence.\n\nPUBLIC, and needs no durable storage: the registry is compiled into the build, so this route answers identically on a deployment whose store is unavailable. It carries no HELPERG operational detail — no feed URLs, no host allowlists, no ingestion state — which is why a credential is not required to read the vocabulary the products must speak.","tags":["economics"],"security":[],"responses":{"200":{"description":"Every active family, plus the complete category vocabulary. There is no paging: the registry is compiled in and bounded. HEAD returns the status, the headers and NO BODY; it is answered wherever GET is, because most uptime monitors send HEAD by default and a route that refused it would report itself unsupported to the tooling most likely to poll it.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"ETag":{"$ref":"#/components/headers/ETag"}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"PUBLIC","x-helperg-required-scope":null,"x-helperg-route-name":"v1.economics.indicators","x-helperg-requires-durable-storage":false,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED"],"x-helperg-public-cache-seconds":3600}},"/api/v1/economics/indicators/{slug}":{"summary":"One indicator family and the series under it","parameters":[{"name":"slug","in":"path","required":true,"description":"Indicator family identifier. The route matches `^[a-z0-9-]+$` and nothing else; anything else returns 404 rather than falling through to the collection listing.","schema":{"type":"string","pattern":"^[a-z0-9-]+$"},"example":"consumer-price-index"}],"get":{"operationId":"getEconomicIndicator","summary":"One indicator family and the series under it","description":"One concept, with every active series registered under it. The series list is the link that stops a reader treating the family as measurable.\n\nA slug that does not match `^[a-z0-9-]+$` is a 404 rather than a fall-through to the listing. That fall-through was a real defect: `/indicators/NO-SUCH-SLUG` answered 200 with the entire collection and, being public and cacheable, sat in the CDN for an hour while a consumer expecting one object received an array with no error signal.","tags":["economics"],"security":[],"responses":{"200":{"description":"The family and its active series.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"ETag":{"$ref":"#/components/headers/ETag"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EconomicsIndicatorDetailResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"PUBLIC","x-helperg-required-scope":null,"x-helperg-route-name":"v1.economics.indicators","x-helperg-requires-durable-storage":false,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","NOT_FOUND"],"x-helperg-ignored-parameters":["category"],"x-helperg-public-cache-seconds":3600},"head":{"operationId":"getEconomicIndicatorHead","summary":"One indicator family and the series under it (headers only)","description":"One concept, with every active series registered under it. The series list is the link that stops a reader treating the family as measurable.\n\nA slug that does not match `^[a-z0-9-]+$` is a 404 rather than a fall-through to the listing. That fall-through was a real defect: `/indicators/NO-SUCH-SLUG` answered 200 with the entire collection and, being public and cacheable, sat in the CDN for an hour while a consumer expecting one object received an array with no error signal.","tags":["economics"],"security":[],"responses":{"200":{"description":"The family and its active series. HEAD returns the status, the headers and NO BODY; it is answered wherever GET is, because most uptime monitors send HEAD by default and a route that refused it would report itself unsupported to the tooling most likely to poll it.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"ETag":{"$ref":"#/components/headers/ETag"}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"PUBLIC","x-helperg-required-scope":null,"x-helperg-route-name":"v1.economics.indicators","x-helperg-requires-durable-storage":false,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","NOT_FOUND"],"x-helperg-ignored-parameters":["category"],"x-helperg-public-cache-seconds":3600}},"/api/v1/economics/series":{"summary":"The series registry","parameters":[{"name":"indicator","in":"query","required":false,"description":"Restrict the listing to series under one indicator family, given as the family SLUG. Matched exactly and case-sensitively; an unknown slug yields an empty list rather than an error, because a family filter that matches nothing is a legitimate answer.","schema":{"type":"string"},"example":"consumer-price-index"},{"name":"country","in":"query","required":false,"description":"One ISO 3166-1 alpha-2 code, upper-cased before matching. A SINGLE value, not a list — unlike the calendar routes' `country`, a comma-separated value here matches nothing. Series filed under a region rather than a country never match; use `region`.","schema":{"type":"string"},"example":"US"},{"name":"region","in":"query","required":false,"description":"One HELPERG region code, upper-cased before matching and VALIDATED against the closed vocabulary: an unknown value returns `VALIDATION_FAILED`. A single value, not a list.","schema":{"type":"string","enum":["EA","EU27_2020","G7","G20","OECD","WORLD"]},"example":"EA"},{"name":"limit","in":"query","required":false,"description":"Page size. Defaults to 100. A value above 500 is CLAMPED to 500, not rejected — `limit=5000` returns 200 with at most five hundred records. A value below 1, or one that does not begin with an integer, returns `VALIDATION_FAILED`. Parsed with a base-ten integer parse, so `3.7` is read as 3.","schema":{"type":"integer","minimum":1,"default":100},"example":50,"x-helperg-clamped-maximum":500}],"get":{"operationId":"listEconomicSeries","summary":"The series registry","description":"Definitions of specific measurable series. Every identifying dimension is published — transformation, seasonal adjustment, unit, scale, frequency, geography, scope — because those are exactly what a consumer needs in order NOT to compare two series that merely look alike.\n\nDEFINITIONS ONLY. No values, no ingestion state, no provider feed URLs. Observed values are ECOSYSTEM_INTERNAL and live on `/api/v1/economics/series/{slug}/observations`.\n\nInactive series are omitted from this listing but stay resolvable by slug, so a stored reference to a retired series does not become a 404.\n\nTHERE IS NO CURSOR HERE. `limit` truncates and `totalMatching` states what was truncated, but this route offers no paging token — the registry is compiled in and small. A caller needing everything should raise `limit`, not look for a `nextCursor`.","tags":["economics"],"security":[],"responses":{"200":{"description":"Matching active series, in registry order, truncated to `limit`. `data.totalMatching` and `meta.totalMatching` are the same number.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"ETag":{"$ref":"#/components/headers/ETag"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EconomicsSeriesListResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"PUBLIC","x-helperg-required-scope":null,"x-helperg-route-name":"v1.economics.series","x-helperg-requires-durable-storage":false,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED"],"x-helperg-public-cache-seconds":3600},"head":{"operationId":"listEconomicSeriesHead","summary":"The series registry (headers only)","description":"Definitions of specific measurable series. Every identifying dimension is published — transformation, seasonal adjustment, unit, scale, frequency, geography, scope — because those are exactly what a consumer needs in order NOT to compare two series that merely look alike.\n\nDEFINITIONS ONLY. No values, no ingestion state, no provider feed URLs. Observed values are ECOSYSTEM_INTERNAL and live on `/api/v1/economics/series/{slug}/observations`.\n\nInactive series are omitted from this listing but stay resolvable by slug, so a stored reference to a retired series does not become a 404.\n\nTHERE IS NO CURSOR HERE. `limit` truncates and `totalMatching` states what was truncated, but this route offers no paging token — the registry is compiled in and small. A caller needing everything should raise `limit`, not look for a `nextCursor`.","tags":["economics"],"security":[],"responses":{"200":{"description":"Matching active series, in registry order, truncated to `limit`. `data.totalMatching` and `meta.totalMatching` are the same number. HEAD returns the status, the headers and NO BODY; it is answered wherever GET is, because most uptime monitors send HEAD by default and a route that refused it would report itself unsupported to the tooling most likely to poll it.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"ETag":{"$ref":"#/components/headers/ETag"}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"PUBLIC","x-helperg-required-scope":null,"x-helperg-route-name":"v1.economics.series","x-helperg-requires-durable-storage":false,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED"],"x-helperg-public-cache-seconds":3600}},"/api/v1/economics/series/{slug}":{"summary":"One series definition, with its family and its sources","parameters":[{"name":"slug","in":"path","required":true,"description":"Series identifier. The route matches `^[a-z0-9-]+$` and nothing else: an upper-case letter, an underscore or a percent-encoded byte returns 404 rather than falling through to the collection listing. That fall-through was a real defect — a mistyped slug answered 200 with the ENTIRE collection and, being public and cacheable, sat in the CDN for an hour while a consumer expecting one object received an array with no error signal.","schema":{"type":"string","pattern":"^[a-z0-9-]+$"},"example":"us-cpi-headline-yoy"}],"get":{"operationId":"getEconomicSeries","summary":"One series definition, with its family and its sources","description":"The full definition of one series, the concept it measures, and which registered sources supply it.\n\nThe providers' own dataset identifiers are withheld from `sources`: they are HELPERG integration detail rather than part of the canonical contract, and publishing them would invite consumers to bypass the canonical model and read the provider directly.\n\nA slug that does not match `^[a-z0-9-]+$` is a 404 rather than a fall-through to the listing, for the same reason as on the indicator route.","tags":["economics"],"security":[],"responses":{"200":{"description":"The series, its family, and its source mappings.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"ETag":{"$ref":"#/components/headers/ETag"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EconomicsSeriesDetailResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"PUBLIC","x-helperg-required-scope":null,"x-helperg-route-name":"v1.economics.series","x-helperg-requires-durable-storage":false,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","NOT_FOUND"],"x-helperg-ignored-parameters":["indicator","country","region","limit"],"x-helperg-public-cache-seconds":3600},"head":{"operationId":"getEconomicSeriesHead","summary":"One series definition, with its family and its sources (headers only)","description":"The full definition of one series, the concept it measures, and which registered sources supply it.\n\nThe providers' own dataset identifiers are withheld from `sources`: they are HELPERG integration detail rather than part of the canonical contract, and publishing them would invite consumers to bypass the canonical model and read the provider directly.\n\nA slug that does not match `^[a-z0-9-]+$` is a 404 rather than a fall-through to the listing, for the same reason as on the indicator route.","tags":["economics"],"security":[],"responses":{"200":{"description":"The series, its family, and its source mappings. HEAD returns the status, the headers and NO BODY; it is answered wherever GET is, because most uptime monitors send HEAD by default and a route that refused it would report itself unsupported to the tooling most likely to poll it.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"ETag":{"$ref":"#/components/headers/ETag"}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"PUBLIC","x-helperg-required-scope":null,"x-helperg-route-name":"v1.economics.series","x-helperg-requires-durable-storage":false,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","NOT_FOUND"],"x-helperg-ignored-parameters":["indicator","country","region","limit"],"x-helperg-public-cache-seconds":3600}},"/api/v1/economics/series/{slug}/observations":{"summary":"Observed values for one series, with revision history","parameters":[{"name":"slug","in":"path","required":true,"description":"Series identifier. The route matches `^[a-z0-9-]+$` and nothing else: an upper-case letter, an underscore or a percent-encoded byte returns 404 rather than falling through to the collection listing. That fall-through was a real defect — a mistyped slug answered 200 with the ENTIRE collection and, being public and cacheable, sat in the CDN for an hour while a consumer expecting one object received an array with no error signal.","schema":{"type":"string","pattern":"^[a-z0-9-]+$"},"example":"us-cpi-headline-yoy"},{"name":"period","in":"query","required":false,"description":"Switches the response to the FULL VINTAGE HISTORY of this one reference period — every version ever published, oldest first. That is the query a correction notice or a backtest needs. NOT VALIDATED against the period vocabulary: an unrecognised code is echoed back with an empty `versions` array and a 200, because \"no vintages exist under that code\" is a true answer and a 404 would claim the series does not exist. An empty value is treated as absent and returns the ordinary listing.","schema":{"type":"string"},"example":"2026-08","x-helperg-validated":false},{"name":"from","in":"query","required":false,"description":"Lower bound on the REFERENCE-PERIOD CODE, not a date. It is compared as a STRING against codes such as `2026-08` or `2026-Q2`, so it must be written in the same form as the codes of the series you are querying — `from=2026-03` for a monthly series, `from=2026-Q1` for a quarterly one. It is NOT parsed as a date and is NOT validated, so `ECONOMICS_RANGE_INVALID` is unreachable here and an inverted or nonsensical bound simply yields an empty page.","schema":{"type":"string"},"example":"2026-01","x-helperg-validated":false},{"name":"to","in":"query","required":false,"description":"Upper bound on the reference-period code, INCLUSIVE, compared as a string exactly as `from` is. Not parsed as a date and not validated.","schema":{"type":"string"},"example":"2026-12","x-helperg-validated":false},{"name":"limit","in":"query","required":false,"description":"Page size. Defaults to 100. A value above 500 is CLAMPED to 500, not rejected — `limit=5000` returns 200 with at most five hundred records. A value below 1, or one that does not begin with an integer, returns `VALIDATION_FAILED`. Parsed with a base-ten integer parse, so `3.7` is read as 3.","schema":{"type":"integer","minimum":1,"default":100},"example":50,"x-helperg-clamped-maximum":500},{"name":"cursor","in":"query","required":false,"description":"OPAQUE page token, taken verbatim from `meta.nextCursor`. Do not construct, parse or store one: it encodes an offset into the FILTERED result set, so it is meaningful only alongside the identical filter arguments, and reusing it with different filters pages through a different set without saying so. A token that does not decode returns `VALIDATION_FAILED`. A token that decodes past the end of the results returns an empty page with a null `nextCursor`, not an error. Paging is NOT a snapshot: an ingestion run can rebuild the corpus between two pages.","schema":{"type":"string"}}],"get":{"operationId":"getSeriesObservations","summary":"Observed values for one series, with revision history","description":"The observation history of one canonical series: one entry per reference period, carrying the CURRENT value and how many times it has been revised. Add `?period={code}` for the full vintage history of a single period.\n\nVALUES ARE DECIMAL STRINGS. `\"3.1\"`, not `3.1`. A JSON number reintroduces the binary floating-point error the storage model exists to avoid, and a consumer comparing two vintages would then see revisions that never happened. A null value is a period the source has scheduled and not yet published; it is never zero.\n\nAN OBSERVATION HAS NO VALUE OF ITS OWN. The value lives on a version, and there can be many: US GDP is published three times for one quarter, and euro-area figures are revised for years. Versions are appended, never overwritten, which is the only reason \"what did we believe in October?\" is answerable.\n\nRANGE FILTERING IS STRING COMPARISON ON PERIOD CODES, NOT DATE ARITHMETIC. `from` and `to` are compared as strings against codes like `2026-08`; they are not parsed, not validated, and cannot produce `ECONOMICS_RANGE_INVALID`.\n\nNo user request ever reaches BLS, Eurostat or the ECB. Reads are served from persisted state, because a calendar request that fetched live would inherit six agencies' latency, rate limits and outages, and would make HELPERG's availability a function of theirs.\n\nSTORAGE. This route reads persisted state and cannot answer without it. Where no durable store is configured, every call returns 503 `SERVICE_UNAVAILABLE`; where one is configured but no ingestion has ever completed, the same code is returned rather than an empty 200, because an empty calendar is a statement about the world and \"nothing has been ingested here\" is a statement about this deployment. An index that EXISTS and matches nothing is still an honest empty 200. Read `x-helperg-durable-storage` at the root of this document for which of those applies to the deployment serving it.\n\nThe series DEFINITION is served from the compiled registry and needs no store, which is why an unregistered slug is a 404 while a registered slug with no ingested observations is a 503 rather than an empty 200.","tags":["economics"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"200":{"description":"Either the period listing or, when `period` was sent, the vintage history for that one period. The two shapes are distinguished by whether `data` carries `observations` or `versions`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EconomicsObservationsEnvelope"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"ECOSYSTEM_INTERNAL","x-helperg-required-scope":"economics:read","x-helperg-route-name":"v1.economics.observations","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED","NOT_FOUND","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"]},"head":{"operationId":"getSeriesObservationsHead","summary":"Observed values for one series, with revision history (headers only)","description":"The observation history of one canonical series: one entry per reference period, carrying the CURRENT value and how many times it has been revised. Add `?period={code}` for the full vintage history of a single period.\n\nVALUES ARE DECIMAL STRINGS. `\"3.1\"`, not `3.1`. A JSON number reintroduces the binary floating-point error the storage model exists to avoid, and a consumer comparing two vintages would then see revisions that never happened. A null value is a period the source has scheduled and not yet published; it is never zero.\n\nAN OBSERVATION HAS NO VALUE OF ITS OWN. The value lives on a version, and there can be many: US GDP is published three times for one quarter, and euro-area figures are revised for years. Versions are appended, never overwritten, which is the only reason \"what did we believe in October?\" is answerable.\n\nRANGE FILTERING IS STRING COMPARISON ON PERIOD CODES, NOT DATE ARITHMETIC. `from` and `to` are compared as strings against codes like `2026-08`; they are not parsed, not validated, and cannot produce `ECONOMICS_RANGE_INVALID`.\n\nNo user request ever reaches BLS, Eurostat or the ECB. Reads are served from persisted state, because a calendar request that fetched live would inherit six agencies' latency, rate limits and outages, and would make HELPERG's availability a function of theirs.\n\nSTORAGE. This route reads persisted state and cannot answer without it. Where no durable store is configured, every call returns 503 `SERVICE_UNAVAILABLE`; where one is configured but no ingestion has ever completed, the same code is returned rather than an empty 200, because an empty calendar is a statement about the world and \"nothing has been ingested here\" is a statement about this deployment. An index that EXISTS and matches nothing is still an honest empty 200. Read `x-helperg-durable-storage` at the root of this document for which of those applies to the deployment serving it.\n\nThe series DEFINITION is served from the compiled registry and needs no store, which is why an unregistered slug is a 404 while a registered slug with no ingested observations is a 503 rather than an empty 200.","tags":["economics"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"200":{"description":"Either the period listing or, when `period` was sent, the vintage history for that one period. The two shapes are distinguished by whether `data` carries `observations` or `versions`. HEAD returns the status, the headers and NO BODY; it is answered wherever GET is, because most uptime monitors send HEAD by default and a route that refused it would report itself unsupported to the tooling most likely to poll it.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"ECOSYSTEM_INTERNAL","x-helperg-required-scope":"economics:read","x-helperg-route-name":"v1.economics.observations","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED","NOT_FOUND","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"]}},"/api/v1/news/events":{"summary":"News events and the official economic publications behind them","parameters":[{"name":"tz","in":"query","required":false,"schema":{"type":"string"},"description":"IANA zone in which bare dates in `from` and `to` are read. Default UTC."},{"name":"from","in":"query","required":false,"schema":{"type":"string"},"description":"Inclusive start of the publication window, `YYYY-MM-DD` (UTC) or an ISO instant. Defaults to 366 days before now, or 366 days before `to`."},{"name":"to","in":"query","required":false,"schema":{"type":"string"},"description":"Inclusive end, `YYYY-MM-DD` (to the end of that day, UTC) or an ISO instant. Defaults to now. The window may not exceed 366 days."},{"name":"confidence","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated P1 confidence states. At most 7.","x-helperg-max-items":7},{"name":"topics","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated P1 event topics. At most 25.","x-helperg-max-items":25},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500,"default":50},"description":"Page size for both halves of the response. Default 50, clamped to 500."},{"name":"source","in":"query","required":false,"schema":{"type":"string"},"description":"Registered news source slugs, exact. At most 10.","x-helperg-max-items":10},{"name":"releaseFamily","in":"query","required":false,"schema":{"type":"string"},"description":"Release-definition slugs, exact. At most 10.","x-helperg-max-items":10},{"name":"series","in":"query","required":false,"schema":{"type":"string"},"description":"Series slugs; matches publications linked to any. At most 10.","x-helperg-max-items":10},{"name":"calendarEvent","in":"query","required":false,"schema":{"type":"string"},"description":"Calendar event ids; matches publications linked to any. At most 10.","x-helperg-max-items":10},{"name":"state","in":"query","required":false,"schema":{"type":"string"},"description":"Publication states. At most 5.","x-helperg-max-items":5},{"name":"linkStatus","in":"query","required":false,"schema":{"type":"string"},"description":"Link statuses. At most 4.","x-helperg-max-items":4}],"get":{"operationId":"getNewsEvents","summary":"News events and the official economic publications behind them","description":"The consumer news surface. `data.events` is the P1 Event/SourceItem model; `data.officialPublications` is the official economic news corpus (BLS, Eurostat, ECB) in full — provenance, link evidence, versions — filtered by the same window and the P2F filters. Access is `ECOSYSTEM_INTERNAL` with `news:read`; no consumer credential holds that scope in this phase.\n\nWhere no official-news feed snapshot has been published on this deployment, `meta.officialFeedStatus` is `unpublished` and `data.officialPublications` is empty rather than the whole response failing.\n\nNo user request ever reaches BLS, Eurostat or the ECB. Reads are served from persisted state, because a calendar request that fetched live would inherit six agencies' latency, rate limits and outages, and would make HELPERG's availability a function of theirs.\n\nSTORAGE. This route reads persisted state and cannot answer without it. Where no durable store is configured, every call returns 503 `SERVICE_UNAVAILABLE`; where one is configured but no ingestion has ever completed, the same code is returned rather than an empty 200, because an empty calendar is a statement about the world and \"nothing has been ingested here\" is a statement about this deployment. An index that EXISTS and matches nothing is still an honest empty 200. Read `x-helperg-durable-storage` at the root of this document for which of those applies to the deployment serving it.","tags":["news"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"200":{"description":"Events and official publications matching the window, newest first.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewsEventsResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"ECOSYSTEM_INTERNAL","x-helperg-required-scope":"news:read","x-helperg-route-name":"v1.news.events","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED","ECONOMICS_RANGE_INVALID","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"]},"head":{"operationId":"getNewsEventsHead","summary":"News events and the official economic publications behind them (headers only)","description":"The consumer news surface. `data.events` is the P1 Event/SourceItem model; `data.officialPublications` is the official economic news corpus (BLS, Eurostat, ECB) in full — provenance, link evidence, versions — filtered by the same window and the P2F filters. Access is `ECOSYSTEM_INTERNAL` with `news:read`; no consumer credential holds that scope in this phase.\n\nWhere no official-news feed snapshot has been published on this deployment, `meta.officialFeedStatus` is `unpublished` and `data.officialPublications` is empty rather than the whole response failing.\n\nNo user request ever reaches BLS, Eurostat or the ECB. Reads are served from persisted state, because a calendar request that fetched live would inherit six agencies' latency, rate limits and outages, and would make HELPERG's availability a function of theirs.\n\nSTORAGE. This route reads persisted state and cannot answer without it. Where no durable store is configured, every call returns 503 `SERVICE_UNAVAILABLE`; where one is configured but no ingestion has ever completed, the same code is returned rather than an empty 200, because an empty calendar is a statement about the world and \"nothing has been ingested here\" is a statement about this deployment. An index that EXISTS and matches nothing is still an honest empty 200. Read `x-helperg-durable-storage` at the root of this document for which of those applies to the deployment serving it.","tags":["news"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"200":{"description":"Events and official publications matching the window, newest first. HEAD returns the status, the headers and NO BODY; it is answered wherever GET is, because most uptime monitors send HEAD by default and a route that refused it would report itself unsupported to the tooling most likely to poll it.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"ECOSYSTEM_INTERNAL","x-helperg-required-scope":"news:read","x-helperg-route-name":"v1.news.events","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED","ECONOMICS_RANGE_INVALID","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"]}},"/api/v1/admin/ingest/economics":{"summary":"OPERATOR ONLY — ingest the BLS release schedule","parameters":[{"name":"force","in":"query","required":false,"description":"Set to exactly `true` to skip the conditional request and re-read the provider document even when it has not changed. NOT VALIDATED and case-sensitive: `TRUE`, `1` and `yes` all mean false. Forcing does not make the run non-idempotent — an unchanged record is still detected and not rewritten — it only spends an upstream fetch.","schema":{"type":"string"},"example":"true","x-helperg-validated":false},{"name":"x-helperg-ingest-trigger","in":"header","required":false,"description":"Declares what kind of run this is, and is echoed back in `data.run.trigger`. VALIDATED and case-sensitive: exactly `SCHEDULE` or exactly `MANUAL`, or omitted. Anything else is a `400 VALIDATION_FAILED` rather than a fallback to the default — a scheduler sending `schedule` would otherwise have every run recorded as manual, and an audit trail that is confidently wrong is worse than one that errors. OMITTED MEANS `MANUAL`, which is what every caller predating this header was already doing. It is a trusted declaration, not a fact the route can check: an HTTPS request from a CI runner and one from an operator terminal are identical on the wire. That is safe only because this surface is unreachable without the admin credential.","schema":{"type":"string","enum":["SCHEDULE","MANUAL"]},"example":"SCHEDULE","x-helperg-validated":true}],"post":{"operationId":"runReleaseScheduleIngestion","summary":"OPERATOR ONLY — ingest the BLS release schedule","description":"Fetches the BLS news-release schedule and reconciles it into canonical events. OPERATOR ONLY: this is not part of the consumer contract and no ecosystem credential opens it.\n\nIT IS A 404, NOT A 401, WHEN NO ADMIN CREDENTIAL IS CONFIGURED. The pipeline treats an unprovisioned admin surface as absent, because a 401 confirms a route exists and a 404 does not. A 401 here therefore means the deployment HAS an admin credential and you did not present it; a 404 means either that the deployment has none or that you are not looking at an admin route at all. Do not read a 404 as proof that this endpoint is unimplemented.\n\nREAD `data.run.state`, NOT THE HTTP STATUS. Every terminal state except `FAILED` answers 202, `SKIPPED` included — and `SKIPPED` covers both \"the provider reported no change\" and \"another run holds the lease\". A `FAILED` run answers 500 in the ORDINARY `{data, meta}` envelope, NOT an error envelope, with the safe reason in `data.run.error`. That is also why an upstream timeout or an unusable provider payload never reaches this route's caller as an `UPSTREAM_*` error code.\n\nIDEMPOTENT. Re-running ingests nothing new when the source has not changed; a release that MOVED is updated in place and an immutable schedule version recording where it was before is appended first, so a failure between the two leaves evidence rather than a silent change.\n\nSCHEDULING LIVES IN GITHUB ACTIONS, NOT ON THE PLATFORM. There is no Vercel Cron and none is enabled; a workflow calls this endpoint over HTTPS so the trigger is reviewable in the same pull request as the code it triggers. Concurrent runs are refused by a storage-backed lease rather than merely discouraged.","tags":["admin"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"202":{"description":"The run was executed and did not fail. Inspect `data.run.state` and `data.run.counters` before recording this as a successful ingestion.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminIngestionResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"ADMIN_INTERNAL","x-helperg-required-scope":null,"x-helperg-route-name":"v1.admin.economics","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","NOT_FOUND","VALIDATION_FAILED","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"],"x-helperg-ignored-parameters":["apply"]}},"/api/v1/admin/ingest/eurostat":{"summary":"OPERATOR ONLY — ingest Eurostat observations","parameters":[{"name":"x-helperg-ingest-trigger","in":"header","required":false,"description":"Declares what kind of run this is, and is echoed back in `data.run.trigger`. VALIDATED and case-sensitive: exactly `SCHEDULE` or exactly `MANUAL`, or omitted. Anything else is a `400 VALIDATION_FAILED` rather than a fallback to the default — a scheduler sending `schedule` would otherwise have every run recorded as manual, and an audit trail that is confidently wrong is worse than one that errors. OMITTED MEANS `MANUAL`, which is what every caller predating this header was already doing. It is a trusted declaration, not a fact the route can check: an HTTPS request from a CI runner and one from an operator terminal are identical on the wire. That is safe only because this surface is unreachable without the admin credential.","schema":{"type":"string","enum":["SCHEDULE","MANUAL"]},"example":"SCHEDULE","x-helperg-validated":true}],"post":{"operationId":"runEurostatObservationIngestion","summary":"OPERATOR ONLY — ingest Eurostat observations","description":"Walks every active Eurostat series mapping and reconciles the observed values, appending a vintage wherever the value or its publication status changed. OPERATOR ONLY.\n\nSCHEDULED DAILY, AND STILL TRIGGERABLE BY HAND. A GitHub Actions cron calls this at 12:17 UTC; there is no Vercel Cron and none is enabled. Its production duration was measured before it was scheduled — roughly 53 seconds on a first load and 39 on an unchanged replay, against a 300-second request ceiling — because scheduling work whose runtime is unknown is how a job discovers a platform timeout in production.\n\nONE FAILING SERIES DOES NOT ABORT THE OTHERS: it is counted in `rejected` and explained in `rejections`, and the run continues.\n\nThe 404-not-401 rule and the read-`data.run.state`-not-the-status rule are identical to the release-schedule ingestion; see that operation.","tags":["admin"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"202":{"description":"The run was executed and did not fail. `counters.updated` counts revisions, not rewrites: an identical re-publication is `unchanged`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminIngestionResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"ADMIN_INTERNAL","x-helperg-required-scope":null,"x-helperg-route-name":"v1.admin.economics","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","NOT_FOUND","VALIDATION_FAILED","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"],"x-helperg-ignored-parameters":["force","apply"]}},"/api/v1/admin/ingest/ecb":{"summary":"OPERATOR ONLY — ingest ECB observations","parameters":[{"name":"x-helperg-ingest-trigger","in":"header","required":false,"description":"Declares what kind of run this is, and is echoed back in `data.run.trigger`. VALIDATED and case-sensitive: exactly `SCHEDULE` or exactly `MANUAL`, or omitted. Anything else is a `400 VALIDATION_FAILED` rather than a fallback to the default — a scheduler sending `schedule` would otherwise have every run recorded as manual, and an audit trail that is confidently wrong is worse than one that errors. OMITTED MEANS `MANUAL`, which is what every caller predating this header was already doing. It is a trusted declaration, not a fact the route can check: an HTTPS request from a CI runner and one from an operator terminal are identical on the wire. That is safe only because this surface is unreachable without the admin credential.","schema":{"type":"string","enum":["SCHEDULE","MANUAL"]},"example":"SCHEDULE","x-helperg-validated":true}],"post":{"operationId":"runEcbObservationIngestion","summary":"OPERATOR ONLY — ingest ECB observations","description":"Walks every active ECB series mapping and reconciles the observed values. OPERATOR ONLY, scheduled daily at 18:41 UTC by the same GitHub Actions workflow, and identical in every operational respect to the Eurostat ingestion — including the 404-not-401 rule and the requirement to read `data.run.state` rather than the HTTP status.","tags":["admin"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"202":{"description":"The run was executed and did not fail.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminIngestionResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"ADMIN_INTERNAL","x-helperg-required-scope":null,"x-helperg-route-name":"v1.admin.economics","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","NOT_FOUND","VALIDATION_FAILED","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"],"x-helperg-ignored-parameters":["force","apply"]}},"/api/v1/admin/ingest/eurostat-calendar":{"summary":"OPERATOR ONLY — ingest the Eurostat release calendar","parameters":[{"name":"x-helperg-ingest-trigger","in":"header","required":false,"description":"Declares what kind of run this is, and is echoed back in `data.run.trigger`. VALIDATED and case-sensitive: exactly `SCHEDULE` or exactly `MANUAL`, or omitted. Anything else is a `400 VALIDATION_FAILED` rather than a fallback to the default — a scheduler sending `schedule` would otherwise have every run recorded as manual, and an audit trail that is confidently wrong is worse than one that errors. OMITTED MEANS `MANUAL`, which is what every caller predating this header was already doing. It is a trusted declaration, not a fact the route can check: an HTTPS request from a CI runner and one from an operator terminal are identical on the wire. That is safe only because this surface is unreachable without the admin credential.","schema":{"type":"string","enum":["SCHEDULE","MANUAL"]},"example":"SCHEDULE","x-helperg-validated":true}],"post":{"operationId":"runEurostatCalendarIngestion","summary":"OPERATOR ONLY — ingest the Eurostat release calendar","description":"Fetches Eurostat's official euro-indicator release calendar (an iCalendar export) and reconciles it into canonical events. OPERATOR ONLY: not part of the consumer contract.\n\nIT IS A 404, NOT A 401, WHEN NO ADMIN CREDENTIAL IS CONFIGURED, and READ `data.run.state`, NOT THE HTTP STATUS — both exactly as on the release-schedule ingestion; see that operation.\n\nWHAT THE SOURCE STATES. Every entry is a DATE with no time and no reference period, so every event from it is `DATE_ONLY` with a null `scheduledAt` and an `UNKNOWN` period. Nothing is manufactured to fill either gap. The feed regenerates its identifiers on every fetch, so identity is (title, date); a release that moves appears as one event vanishing and another appearing, and nothing is deleted.\n\nNOT SCHEDULED IN PRODUCTION. Dispatchable by hand; a cron is a separate, later decision.","tags":["admin"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"202":{"description":"The run was executed and did not fail. An unchanged calendar reports every event `unchanged`, which is the idempotency proof.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminIngestionResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"ADMIN_INTERNAL","x-helperg-required-scope":null,"x-helperg-route-name":"v1.admin.economics","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","NOT_FOUND","VALIDATION_FAILED","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"],"x-helperg-ignored-parameters":["force","apply"]}},"/api/v1/admin/ingest/ecb-meetings":{"summary":"OPERATOR ONLY — ingest the ECB meetings calendar","parameters":[{"name":"x-helperg-ingest-trigger","in":"header","required":false,"description":"Declares what kind of run this is, and is echoed back in `data.run.trigger`. VALIDATED and case-sensitive: exactly `SCHEDULE` or exactly `MANUAL`, or omitted. Anything else is a `400 VALIDATION_FAILED` rather than a fallback to the default — a scheduler sending `schedule` would otherwise have every run recorded as manual, and an audit trail that is confidently wrong is worse than one that errors. OMITTED MEANS `MANUAL`, which is what every caller predating this header was already doing. It is a trusted declaration, not a fact the route can check: an HTTPS request from a CI runner and one from an operator terminal are identical on the wire. That is safe only because this surface is unreachable without the admin credential.","schema":{"type":"string","enum":["SCHEDULE","MANUAL"]},"example":"SCHEDULE","x-helperg-validated":true}],"post":{"operationId":"runEcbMeetingsCalendarIngestion","summary":"OPERATOR ONLY — ingest the ECB meetings calendar","description":"Reads the ECB's official schedule of Governing Council and General Council meetings — an HTML page, because the ECB publishes no structured feed for it — and reconciles monetary policy meetings, the decisions published on their last day, and the press conferences that follow them into canonical events. OPERATOR ONLY.\n\nIT IS A 404, NOT A 401, WHEN NO ADMIN CREDENTIAL IS CONFIGURED, and READ `data.run.state`, NOT THE HTTP STATUS.\n\nThe page states no times, so every event is `DATE_ONLY`. Non-monetary-policy and General Council meetings are recognised and deliberately produce no event; the run summary counts them. A page whose markup has changed fails the run rather than producing a wrong calendar.\n\nNOT SCHEDULED IN PRODUCTION. Dispatchable by hand; a cron is a separate, later decision.","tags":["admin"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"202":{"description":"The run was executed and did not fail.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminIngestionResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"ADMIN_INTERNAL","x-helperg-required-scope":null,"x-helperg-route-name":"v1.admin.economics","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","NOT_FOUND","VALIDATION_FAILED","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"],"x-helperg-ignored-parameters":["force","apply"]}},"/api/v1/admin/ingest/ecb-statistics":{"summary":"OPERATOR ONLY — ingest the ECB statistical release calendar","parameters":[{"name":"x-helperg-ingest-trigger","in":"header","required":false,"description":"Declares what kind of run this is, and is echoed back in `data.run.trigger`. VALIDATED and case-sensitive: exactly `SCHEDULE` or exactly `MANUAL`, or omitted. Anything else is a `400 VALIDATION_FAILED` rather than a fallback to the default — a scheduler sending `schedule` would otherwise have every run recorded as manual, and an audit trail that is confidently wrong is worse than one that errors. OMITTED MEANS `MANUAL`, which is what every caller predating this header was already doing. It is a trusted declaration, not a fact the route can check: an HTTPS request from a CI runner and one from an operator terminal are identical on the wire. That is safe only because this surface is unreachable without the admin credential.","schema":{"type":"string","enum":["SCHEDULE","MANUAL"]},"example":"SCHEDULE","x-helperg-validated":true}],"post":{"operationId":"runEcbStatisticsCalendarIngestion","summary":"OPERATOR ONLY — ingest the ECB statistical release calendar","description":"Reads the ECB's official statistical release calendar — an HTML page, because the ECB publishes no structured feed for it — and reconciles each entry into a canonical event carrying the exact Frankfurt time the page states (converted to UTC with real DST rules), the dataset code, the source-stated reference period, and `TENTATIVE` status where the page says so. OPERATOR ONLY.\n\nIT IS A 404, NOT A 401, WHEN NO ADMIN CREDENTIAL IS CONFIGURED, and READ `data.run.state`, NOT THE HTTP STATUS.\n\nIdentity is (release, reference period, date), because the ECB can list one period twice as two real publications. An unrecognised title, an unparsable period, or a title reappearing over a different dataset is quarantined, never guessed.\n\nNOT SCHEDULED IN PRODUCTION. Dispatchable by hand; a cron is a separate, later decision.","tags":["admin"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"202":{"description":"The run was executed and did not fail.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminIngestionResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"ADMIN_INTERNAL","x-helperg-required-scope":null,"x-helperg-route-name":"v1.admin.economics","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","NOT_FOUND","VALIDATION_FAILED","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"],"x-helperg-ignored-parameters":["force","apply"]}},"/api/v1/admin/economics/migrate":{"summary":"OPERATOR ONLY — migrate v1 events to the v2 model","parameters":[{"name":"apply","in":"query","required":false,"description":"DRY BY DEFAULT. Nothing is written unless this is exactly `true`, so probing what the migration would do cannot accidentally do it. NOT VALIDATED and case-sensitive: any other value, including `TRUE` and `1`, leaves the operation a preview. The response echoes what happened in `data.applied`.","schema":{"type":"string"},"example":"true","x-helperg-validated":false}],"post":{"operationId":"runLegacyEventMigration","summary":"OPERATOR ONLY — migrate v1 events to the v2 model","description":"Detects stored events written against the v1 event model and rewrites them into the canonical v2 model. OPERATOR ONLY.\n\nIT IS A 404, NOT A 401, WHEN NO ADMIN CREDENTIAL IS CONFIGURED, exactly as on the three ingestion operations: an unprovisioned admin surface is treated as absent, because a 401 confirms a route exists and a 404 does not.\n\nDRY BY DEFAULT: nothing is written unless `apply=true`, so this can be run to find out what it would do. A v1 record is recognised structurally rather than by a stored version flag, because the records most in need of migrating are the ones written before anybody thought to add a flag.\n\nIDEMPOTENT: re-running finds nothing left to migrate. A record whose title has no registered release mapping is quarantined rather than guessed into a series.\n\nUnlike the three ingestion operations, this one always answers 202 on completion — it runs no job and therefore has no `FAILED` run to report through a 500. A 500 here is an ordinary error envelope.","tags":["admin"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"202":{"description":"The migration completed. `data.applied` echoes whether anything was actually written.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminMigrationResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"ADMIN_INTERNAL","x-helperg-required-scope":null,"x-helperg-route-name":"v1.admin.economics","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","NOT_FOUND","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"],"x-helperg-ignored-parameters":["force","x-helperg-ingest-trigger"]}},"/api/v1/admin/ingest/news/bls":{"summary":"OPERATOR ONLY — ingest the BLS official news release feeds","parameters":[{"name":"force","in":"query","required":false,"description":"Set to exactly `true` to skip the conditional request and re-read the provider document even when it has not changed. NOT VALIDATED and case-sensitive: `TRUE`, `1` and `yes` all mean false. Forcing does not make the run non-idempotent — an unchanged record is still detected and not rewritten — it only spends an upstream fetch.","schema":{"type":"string"},"example":"true","x-helperg-validated":false},{"name":"x-helperg-ingest-trigger","in":"header","required":false,"description":"Declares what kind of run this is, and is echoed back in `data.run.trigger`. VALIDATED and case-sensitive: exactly `SCHEDULE` or exactly `MANUAL`, or omitted. Anything else is a `400 VALIDATION_FAILED` rather than a fallback to the default — a scheduler sending `schedule` would otherwise have every run recorded as manual, and an audit trail that is confidently wrong is worse than one that errors. OMITTED MEANS `MANUAL`, which is what every caller predating this header was already doing. It is a trusted declaration, not a fact the route can check: an HTTPS request from a CI runner and one from an operator terminal are identical on the wire. That is safe only because this surface is unreachable without the admin credential.","schema":{"type":"string","enum":["SCHEDULE","MANUAL"]},"example":"SCHEDULE","x-helperg-validated":true}],"post":{"operationId":"runBlsOfficialNewsIngestion","summary":"OPERATOR ONLY — ingest the BLS official news release feeds","description":"Reads the eight BLS per-program Atom feeds (CPI, Employment Situation, PPI, ECI, JOLTS, Real Earnings, Import/Export Price Indexes, Productivity and Costs), one entry per official news release, and reconciles each into an official economic news record: title, archive-page link, the public-domain two-sentence summary as the excerpt, curated release family, series through the release registry, and a calendar event when the BLS schedule holds one on the same US Eastern date. OPERATOR ONLY.\n\nIT IS A 404, NOT A 401, WHEN NO ADMIN CREDENTIAL IS CONFIGURED, and READ `data.run.state`, NOT THE HTTP STATUS: a PARTIAL run answers 202 and did not republish the public feed.\n\nCONDITIONAL: the feeds honour If-Modified-Since, so a run with every feed unchanged is SKIPPED. `force=true` ignores the checkpoint. A malformed entry is rejected (PARTIAL); one unreadable program feed is a rejection; all eight unreadable is FAILED.\n\nNOT SCHEDULED IN PRODUCTION. Dispatchable by hand; a cron is a separate, later decision.","tags":["admin"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"202":{"description":"The run was executed and did not fail.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminIngestionResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"ADMIN_INTERNAL","x-helperg-required-scope":null,"x-helperg-route-name":"v1.admin.news","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","NOT_FOUND","VALIDATION_FAILED","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"],"x-helperg-ignored-parameters":["apply"]}},"/api/v1/admin/ingest/news/eurostat":{"summary":"OPERATOR ONLY — ingest the Eurostat news releases (Euro indicators)","parameters":[{"name":"x-helperg-ingest-trigger","in":"header","required":false,"description":"Declares what kind of run this is, and is echoed back in `data.run.trigger`. VALIDATED and case-sensitive: exactly `SCHEDULE` or exactly `MANUAL`, or omitted. Anything else is a `400 VALIDATION_FAILED` rather than a fallback to the default — a scheduler sending `schedule` would otherwise have every run recorded as manual, and an audit trail that is confidently wrong is worse than one that errors. OMITTED MEANS `MANUAL`, which is what every caller predating this header was already doing. It is a trusted declaration, not a fact the route can check: an HTTPS request from a CI runner and one from an operator terminal are identical on the wire. That is safe only because this surface is unreachable without the admin credential.","schema":{"type":"string","enum":["SCHEDULE","MANUAL"]},"example":"SCHEDULE","x-helperg-validated":true}],"post":{"operationId":"runEurostatOfficialNewsIngestion","summary":"OPERATOR ONLY — ingest the Eurostat news releases (Euro indicators)","description":"Reads one page of one hundred entries of Eurostat's documented Atom collection of news releases and reconciles each into an official economic news record: title, product-code identity, the CC BY 4.0 summary as the excerpt, a release family from an exact title pattern or none, series through the release registry, a calendar event on the same Luxembourg date, and an observation where a mapped series holds the stated reference period. OPERATOR ONLY.\n\nIT IS A 404, NOT A 401, WHEN NO ADMIN CREDENTIAL IS CONFIGURED, and READ `data.run.state`, NOT THE HTTP STATUS: a PARTIAL run answers 202 and did not republish the public feed.\n\nUnconditional: the collection honours no validator, so the page is read in full every run. An empty page is FAILED, not an empty result.\n\nNOT SCHEDULED IN PRODUCTION. Dispatchable by hand; a cron is a separate, later decision.","tags":["admin"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"202":{"description":"The run was executed and did not fail.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminIngestionResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"ADMIN_INTERNAL","x-helperg-required-scope":null,"x-helperg-route-name":"v1.admin.news","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","NOT_FOUND","VALIDATION_FAILED","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"],"x-helperg-ignored-parameters":["force","apply"]}},"/api/v1/admin/ingest/news/ecb":{"summary":"OPERATOR ONLY — ingest the ECB press and statistical release feeds","parameters":[{"name":"x-helperg-ingest-trigger","in":"header","required":false,"description":"Declares what kind of run this is, and is echoed back in `data.run.trigger`. VALIDATED and case-sensitive: exactly `SCHEDULE` or exactly `MANUAL`, or omitted. Anything else is a `400 VALIDATION_FAILED` rather than a fallback to the default — a scheduler sending `schedule` would otherwise have every run recorded as manual, and an audit trail that is confidently wrong is worse than one that errors. OMITTED MEANS `MANUAL`, which is what every caller predating this header was already doing. It is a trusted declaration, not a fact the route can check: an HTTPS request from a CI runner and one from an operator terminal are identical on the wire. That is safe only because this surface is unreachable without the admin credential.","schema":{"type":"string","enum":["SCHEDULE","MANUAL"]},"example":"SCHEDULE","x-helperg-validated":true}],"post":{"operationId":"runEcbOfficialNewsIngestion","summary":"OPERATOR ONLY — ingest the ECB press and statistical release feeds","description":"Reads the ECB's press RSS feed (press releases, monetary policy decisions, monetary policy statements; speeches and interviews are ignored by URL) and its statistical press feed (statistical releases and monetary policy meeting accounts) and reconciles each item into an official economic news record: title, publication slug identity, canonical link, no excerpt, publication type and release family from the URL path, and a calendar event on the same Frankfurt date or the meeting dates an account names. OPERATOR ONLY.\n\nIT IS A 404, NOT A 401, WHEN NO ADMIN CREDENTIAL IS CONFIGURED, and READ `data.run.state`, NOT THE HTTP STATUS: a PARTIAL run answers 202 and did not republish the public feed.\n\nUnconditional: the feeds honour no validator and are read in full. One unreadable feed is a rejection (PARTIAL); both unreadable is FAILED.\n\nNOT SCHEDULED IN PRODUCTION. Dispatchable by hand; a cron is a separate, later decision.","tags":["admin"],"security":[{"helpergApiKeyBearer":[]},{"helpergApiKeyHeader":[]}],"responses":{"202":{"description":"The run was executed and did not fail.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminIngestionResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"ADMIN_INTERNAL","x-helperg-required-scope":null,"x-helperg-route-name":"v1.admin.news","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","NOT_FOUND","VALIDATION_FAILED","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"],"x-helperg-ignored-parameters":["force","apply"]}},"/api/public/v1/economics/calendar":{"summary":"Public economic calendar: upcoming and recent official releases, grouped by day","parameters":[{"name":"tz","in":"query","required":false,"description":"IANA timezone identifier. It does two things, and they are easy to confuse. First, it is the zone a bare `YYYY-MM-DD` in `from` or `to` is interpreted in — `from=2026-01-15&tz=Asia/Tokyo` means the start of the 15th in Tokyo. Second, it is the zone the local projection is computed in. Defaults to `UTC`, never to anything derived from the caller's IP or headers. An unknown zone returns `VALIDATION_FAILED` and is never quietly defaulted.","schema":{"type":"string","default":"UTC"},"example":"Europe/Prague"},{"name":"from","in":"query","required":false,"schema":{"type":"string"},"description":"Inclusive start, `YYYY-MM-DD` in `tz` or an ISO instant. Defaults to seven days before now when neither bound is sent, or to `to` minus the window ceiling when only `to` is sent."},{"name":"to","in":"query","required":false,"schema":{"type":"string"},"description":"Inclusive end, `YYYY-MM-DD` in `tz` (to the end of that day) or an ISO instant. Defaults to thirty-one days after now, or after `from`. The window may not exceed 93 days."},{"name":"country","in":"query","required":false,"description":"Comma-separated ISO 3166-1 alpha-2 codes. Matched case-insensitively. At most 25 values; more returns `VALIDATION_FAILED`. An event filed under a REGION rather than a country never matches this filter — use `region` for those.","schema":{"type":"string"},"example":"US,CZ","x-helperg-max-items":25},{"name":"region","in":"query","required":false,"description":"Comma-separated HELPERG region codes. Matched case-insensitively and VALIDATED against the closed vocabulary: an unknown value returns `VALIDATION_FAILED` rather than silently matching nothing. At most 10 values.","schema":{"type":"string","enum":["EA","EU27_2020","G7","G20","OECD","WORLD"]},"example":"EA","x-helperg-max-items":10},{"name":"currency","in":"query","required":false,"description":"Comma-separated ISO 4217 codes. Matched case-insensitively. NOT validated against a vocabulary — an unregistered code simply matches nothing and returns an empty page. At most 10 values.","schema":{"type":"string"},"example":"EUR","x-helperg-max-items":10,"x-helperg-validated":false},{"name":"importance","in":"query","required":false,"description":"Comma-separated HELPERG importance levels. Matched case-insensitively and VALIDATED: an unknown value returns `VALIDATION_FAILED`. At most 4 values. Importance is editorial methodology, not a measurement of market impact; `event.importanceBasis` records how each level was arrived at.","schema":{"type":"string","enum":["HIGH","MEDIUM","LOW","UNCLASSIFIED"]},"example":"HIGH,MEDIUM","x-helperg-max-items":4},{"name":"release","in":"query","required":false,"description":"Comma-separated release-definition slugs, matched EXACTLY and case-SENSITIVELY — unlike `country`, `region` and `currency`, which are upper-cased first. At most 25 values.","schema":{"type":"string"},"x-helperg-max-items":25},{"name":"source","in":"query","required":false,"description":"Comma-separated registered source slugs (`bls-release-schedule`, `eurostat-release-calendar`, `ecb-meetings-calendar`, `ecb-statistics-calendar`), matched EXACTLY and case-SENSITIVELY. An event reported by more than one source matches any of them, because a second source ATTACHES a reference to the canonical event rather than creating a copy. At most 10 values.","schema":{"type":"string"},"example":"eurostat-release-calendar,ecb-statistics-calendar","x-helperg-max-items":10},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Page size. Default 50, clamped to 200."},{"name":"cursor","in":"query","required":false,"description":"OPAQUE page token, taken verbatim from `meta.nextCursor`. Do not construct, parse or store one: it encodes an offset into the FILTERED result set, so it is meaningful only alongside the identical filter arguments, and reusing it with different filters pages through a different set without saying so. A token that does not decode returns `VALIDATION_FAILED`. A token that decodes past the end of the results returns an empty page with a null `nextCursor`, not an error. Paging is NOT a snapshot: an ingestion run can rebuild the corpus between two pages.","schema":{"type":"string"}}],"get":{"operationId":"getPublicEconomicCalendar","summary":"Public economic calendar: upcoming and recent official releases, grouped by day","description":"The anonymous view of the economic calendar. Same corpus and same service as `/api/v1/economics/calendar`, a narrower window, a smaller page, and a body built from `PublicCalendarEvent` rather than the canonical record.\n\nWINDOW DEFAULTS. Send neither `from` nor `to` and the window is seven days back to thirty-one days ahead. Send one and the other is derived. The window can never exceed `data.window.maxDays`; a wider request is `ECONOMICS_RANGE_INVALID`.\n\nTWO FIELDS THAT MUST NOT BE CONFLATED. `status` is what the SOURCE says and never changes because time passed; `derivedState` is what the clock supports, computed per request. A release whose scheduled time has passed and whose publication is unconfirmed is `PAST_DUE_AWAITING_CONFIRMATION`, never `RELEASED`.\n\nSTALENESS IS STATED. `meta.dataStatus` says whether the corpus is fresh, stale or unavailable; a shared cache cannot hide it.\n\nFILTER VALUES are identifier-shaped tokens (`^[A-Za-z0-9_-]{1,64}$`), at most ten per list; `region` and `importance` are closed enumerations. Unknown parameters are ignored.\n\nNo user request ever reaches BLS, Eurostat or the ECB. Reads are served from persisted state, because a calendar request that fetched live would inherit six agencies' latency, rate limits and outages, and would make HELPERG's availability a function of theirs.\n\nSTORAGE. This route reads persisted state and cannot answer without it. Where no durable store is configured, every call returns 503 `SERVICE_UNAVAILABLE`; where one is configured but no ingestion has ever completed, the same code is returned rather than an empty 200, because an empty calendar is a statement about the world and \"nothing has been ingested here\" is a statement about this deployment. An index that EXISTS and matches nothing is still an honest empty 200. Read `x-helperg-durable-storage` at the root of this document for which of those applies to the deployment serving it.","tags":["economics"],"security":[],"responses":{"200":{"description":"Matching events grouped by day, earliest first. An index that exists and matches nothing returns empty `days` with a 200; an index that has never been published returns 503, so an empty list always means \"no matching releases\".","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"ETag":{"$ref":"#/components/headers/ETag"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicCalendarResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"PUBLIC","x-helperg-required-scope":null,"x-helperg-route-name":"public.economics.calendar","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED","ECONOMICS_RANGE_INVALID","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"],"x-helperg-public-cache-seconds":300},"head":{"operationId":"getPublicEconomicCalendarHead","summary":"Public economic calendar: upcoming and recent official releases, grouped by day (headers only)","description":"The anonymous view of the economic calendar. Same corpus and same service as `/api/v1/economics/calendar`, a narrower window, a smaller page, and a body built from `PublicCalendarEvent` rather than the canonical record.\n\nWINDOW DEFAULTS. Send neither `from` nor `to` and the window is seven days back to thirty-one days ahead. Send one and the other is derived. The window can never exceed `data.window.maxDays`; a wider request is `ECONOMICS_RANGE_INVALID`.\n\nTWO FIELDS THAT MUST NOT BE CONFLATED. `status` is what the SOURCE says and never changes because time passed; `derivedState` is what the clock supports, computed per request. A release whose scheduled time has passed and whose publication is unconfirmed is `PAST_DUE_AWAITING_CONFIRMATION`, never `RELEASED`.\n\nSTALENESS IS STATED. `meta.dataStatus` says whether the corpus is fresh, stale or unavailable; a shared cache cannot hide it.\n\nFILTER VALUES are identifier-shaped tokens (`^[A-Za-z0-9_-]{1,64}$`), at most ten per list; `region` and `importance` are closed enumerations. Unknown parameters are ignored.\n\nNo user request ever reaches BLS, Eurostat or the ECB. Reads are served from persisted state, because a calendar request that fetched live would inherit six agencies' latency, rate limits and outages, and would make HELPERG's availability a function of theirs.\n\nSTORAGE. This route reads persisted state and cannot answer without it. Where no durable store is configured, every call returns 503 `SERVICE_UNAVAILABLE`; where one is configured but no ingestion has ever completed, the same code is returned rather than an empty 200, because an empty calendar is a statement about the world and \"nothing has been ingested here\" is a statement about this deployment. An index that EXISTS and matches nothing is still an honest empty 200. Read `x-helperg-durable-storage` at the root of this document for which of those applies to the deployment serving it.","tags":["economics"],"security":[],"responses":{"200":{"description":"Matching events grouped by day, earliest first. An index that exists and matches nothing returns empty `days` with a 200; an index that has never been published returns 503, so an empty list always means \"no matching releases\". HEAD returns the status, the headers and NO BODY; it is answered wherever GET is, because most uptime monitors send HEAD by default and a route that refused it would report itself unsupported to the tooling most likely to poll it.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"ETag":{"$ref":"#/components/headers/ETag"}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"PUBLIC","x-helperg-required-scope":null,"x-helperg-route-name":"public.economics.calendar","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED","ECONOMICS_RANGE_INVALID","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"],"x-helperg-public-cache-seconds":300}},"/api/public/v1/economics/releases":{"summary":"Public recent releases: the newest published values per series, merged","parameters":[{"name":"series","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated series slugs to restrict the listing to. At most ten."},{"name":"country","in":"query","required":false,"description":"One ISO 3166-1 alpha-2 code, upper-cased before matching. A SINGLE value, not a list — unlike the calendar routes' `country`, a comma-separated value here matches nothing. Series filed under a region rather than a country never match; use `region`.","schema":{"type":"string"},"example":"US"},{"name":"region","in":"query","required":false,"description":"One HELPERG region code, upper-cased before matching and VALIDATED against the closed vocabulary: an unknown value returns `VALIDATION_FAILED`. A single value, not a list.","schema":{"type":"string","enum":["EA","EU27_2020","G7","G20","OECD","WORLD"]},"example":"EA"},{"name":"perSeries","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":5,"default":1},"description":"How many newest vintages per series to merge. Default 1, clamped to 5."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Page size over the merged listing. Default 20, clamped to 100."},{"name":"cursor","in":"query","required":false,"description":"OPAQUE page token, taken verbatim from `meta.nextCursor`. Do not construct, parse or store one: it encodes an offset into the FILTERED result set, so it is meaningful only alongside the identical filter arguments, and reusing it with different filters pages through a different set without saying so. A token that does not decode returns `VALIDATION_FAILED`. A token that decodes past the end of the results returns an empty page with a null `nextCursor`, not an error. Paging is NOT a snapshot: an ingestion run can rebuild the corpus between two pages.","schema":{"type":"string"}}],"get":{"operationId":"getPublicEconomicReleases","summary":"Public recent releases: the newest published values per series, merged","description":"For every registered series with observations on this deployment, the newest `perSeries` vintages, merged and ordered newest first by `publishedAt`, else `sourceUpdatedAt`, else `ingestedAt`; ties by series slug then period. An actual value here is a verified ingested observation and nothing else: the calendar is not consulted, and a scheduled event with no observation is not a release.\n\nA series whose observations have never been ingested here is listed in `meta.unavailableSeries` rather than omitted, so \"no data yet on this deployment\" is distinguishable from \"no recent release\". No forecast, consensus, surprise or market-impact figure exists in this response.\n\nNo user request ever reaches BLS, Eurostat or the ECB. Reads are served from persisted state, because a calendar request that fetched live would inherit six agencies' latency, rate limits and outages, and would make HELPERG's availability a function of theirs.\n\nSTORAGE. This route reads persisted state and cannot answer without it. Where no durable store is configured, every call returns 503 `SERVICE_UNAVAILABLE`; where one is configured but no ingestion has ever completed, the same code is returned rather than an empty 200, because an empty calendar is a statement about the world and \"nothing has been ingested here\" is a statement about this deployment. An index that EXISTS and matches nothing is still an honest empty 200. Read `x-helperg-durable-storage` at the root of this document for which of those applies to the deployment serving it.","tags":["economics"],"security":[],"responses":{"200":{"description":"The newest releases, newest first. `meta.totalMatching` counts the merged vintages before paging.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"ETag":{"$ref":"#/components/headers/ETag"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicReleasesResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"PUBLIC","x-helperg-required-scope":null,"x-helperg-route-name":"public.economics.releases","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"],"x-helperg-public-cache-seconds":300},"head":{"operationId":"getPublicEconomicReleasesHead","summary":"Public recent releases: the newest published values per series, merged (headers only)","description":"For every registered series with observations on this deployment, the newest `perSeries` vintages, merged and ordered newest first by `publishedAt`, else `sourceUpdatedAt`, else `ingestedAt`; ties by series slug then period. An actual value here is a verified ingested observation and nothing else: the calendar is not consulted, and a scheduled event with no observation is not a release.\n\nA series whose observations have never been ingested here is listed in `meta.unavailableSeries` rather than omitted, so \"no data yet on this deployment\" is distinguishable from \"no recent release\". No forecast, consensus, surprise or market-impact figure exists in this response.\n\nNo user request ever reaches BLS, Eurostat or the ECB. Reads are served from persisted state, because a calendar request that fetched live would inherit six agencies' latency, rate limits and outages, and would make HELPERG's availability a function of theirs.\n\nSTORAGE. This route reads persisted state and cannot answer without it. Where no durable store is configured, every call returns 503 `SERVICE_UNAVAILABLE`; where one is configured but no ingestion has ever completed, the same code is returned rather than an empty 200, because an empty calendar is a statement about the world and \"nothing has been ingested here\" is a statement about this deployment. An index that EXISTS and matches nothing is still an honest empty 200. Read `x-helperg-durable-storage` at the root of this document for which of those applies to the deployment serving it.","tags":["economics"],"security":[],"responses":{"200":{"description":"The newest releases, newest first. `meta.totalMatching` counts the merged vintages before paging. HEAD returns the status, the headers and NO BODY; it is answered wherever GET is, because most uptime monitors send HEAD by default and a route that refused it would report itself unsupported to the tooling most likely to poll it.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"ETag":{"$ref":"#/components/headers/ETag"}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"PUBLIC","x-helperg-required-scope":null,"x-helperg-route-name":"public.economics.releases","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"],"x-helperg-public-cache-seconds":300}},"/api/public/v1/economics/observations":{"summary":"Public observations of one series, oldest period first","parameters":[{"name":"series","in":"query","required":true,"schema":{"type":"string"},"description":"The series slug. Unknown is 404."},{"name":"from","in":"query","required":false,"description":"Lower bound on the REFERENCE-PERIOD CODE, not a date. It is compared as a STRING against codes such as `2026-08` or `2026-Q2`, so it must be written in the same form as the codes of the series you are querying — `from=2026-03` for a monthly series, `from=2026-Q1` for a quarterly one. It is NOT parsed as a date and is NOT validated, so `ECONOMICS_RANGE_INVALID` is unreachable here and an inverted or nonsensical bound simply yields an empty page.","schema":{"type":"string"},"example":"2026-01","x-helperg-validated":false},{"name":"to","in":"query","required":false,"description":"Upper bound on the reference-period code, INCLUSIVE, compared as a string exactly as `from` is. Not parsed as a date and not validated.","schema":{"type":"string"},"example":"2026-12","x-helperg-validated":false},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":365,"default":100},"description":"Page size. Default 100, clamped to 365."},{"name":"cursor","in":"query","required":false,"description":"OPAQUE page token, taken verbatim from `meta.nextCursor`. Do not construct, parse or store one: it encodes an offset into the FILTERED result set, so it is meaningful only alongside the identical filter arguments, and reusing it with different filters pages through a different set without saying so. A token that does not decode returns `VALIDATION_FAILED`. A token that decodes past the end of the results returns an empty page with a null `nextCursor`, not an error. Paging is NOT a snapshot: an ingestion run can rebuild the corpus between two pages.","schema":{"type":"string"}}],"get":{"operationId":"getPublicEconomicObservations","summary":"Public observations of one series, oldest period first","description":"The published vintages of one registered series, with the revision facts each carries. `series` is required and is a query parameter rather than a path segment so the public namespace has no parameterised paths.\n\nA registered series with no observations on this deployment is a 503, not an empty list: an empty list would be a statement about the world rather than about this deployment. Values are decimal STRINGS, exactly as published; null is a period the source has scheduled and not yet filled.\n\nNo user request ever reaches BLS, Eurostat or the ECB. Reads are served from persisted state, because a calendar request that fetched live would inherit six agencies' latency, rate limits and outages, and would make HELPERG's availability a function of theirs.\n\nSTORAGE. This route reads persisted state and cannot answer without it. Where no durable store is configured, every call returns 503 `SERVICE_UNAVAILABLE`; where one is configured but no ingestion has ever completed, the same code is returned rather than an empty 200, because an empty calendar is a statement about the world and \"nothing has been ingested here\" is a statement about this deployment. An index that EXISTS and matches nothing is still an honest empty 200. Read `x-helperg-durable-storage` at the root of this document for which of those applies to the deployment serving it.","tags":["economics"],"security":[],"responses":{"200":{"description":"A page of vintages, period ascending.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"ETag":{"$ref":"#/components/headers/ETag"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicObservationsResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"PUBLIC","x-helperg-required-scope":null,"x-helperg-route-name":"public.economics.observations","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED","NOT_FOUND","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"],"x-helperg-public-cache-seconds":600},"head":{"operationId":"getPublicEconomicObservationsHead","summary":"Public observations of one series, oldest period first (headers only)","description":"The published vintages of one registered series, with the revision facts each carries. `series` is required and is a query parameter rather than a path segment so the public namespace has no parameterised paths.\n\nA registered series with no observations on this deployment is a 503, not an empty list: an empty list would be a statement about the world rather than about this deployment. Values are decimal STRINGS, exactly as published; null is a period the source has scheduled and not yet filled.\n\nNo user request ever reaches BLS, Eurostat or the ECB. Reads are served from persisted state, because a calendar request that fetched live would inherit six agencies' latency, rate limits and outages, and would make HELPERG's availability a function of theirs.\n\nSTORAGE. This route reads persisted state and cannot answer without it. Where no durable store is configured, every call returns 503 `SERVICE_UNAVAILABLE`; where one is configured but no ingestion has ever completed, the same code is returned rather than an empty 200, because an empty calendar is a statement about the world and \"nothing has been ingested here\" is a statement about this deployment. An index that EXISTS and matches nothing is still an honest empty 200. Read `x-helperg-durable-storage` at the root of this document for which of those applies to the deployment serving it.","tags":["economics"],"security":[],"responses":{"200":{"description":"A page of vintages, period ascending. HEAD returns the status, the headers and NO BODY; it is answered wherever GET is, because most uptime monitors send HEAD by default and a route that refused it would report itself unsupported to the tooling most likely to poll it.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"ETag":{"$ref":"#/components/headers/ETag"}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"404":{"$ref":"#/components/responses/Error404"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"PUBLIC","x-helperg-required-scope":null,"x-helperg-route-name":"public.economics.observations","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED","NOT_FOUND","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"],"x-helperg-public-cache-seconds":600}},"/api/public/v1/weather/current":{"summary":"Public current weather at a point","parameters":[{"name":"lat","in":"query","required":true,"description":"Latitude in WGS 84 degrees. Rounded to four decimal places before use. A non-numeric or out-of-range value returns `WEATHER_LOCATION_INVALID`, which is deliberately a different code from `VALIDATION_FAILED` so a consumer can tell \"you sent nonsense\" apart from \"the provider is down\".","schema":{"type":"number","minimum":-90,"maximum":90},"example":50.0755},{"name":"lon","in":"query","required":true,"description":"Longitude in WGS 84 degrees. Rounded to four decimal places before use.","schema":{"type":"number","minimum":-180,"maximum":180},"example":14.4378},{"name":"altitude","in":"query","required":false,"description":"Metres above mean sea level, rounded to a whole number. Omit it and the provider infers an altitude for the point. Outside -500 to 9000 returns `WEATHER_LOCATION_INVALID`.","schema":{"type":"number","minimum":-500,"maximum":9000},"example":245},{"name":"units","in":"query","required":false,"description":"NOT VALIDATED. The route performs one exact, case-sensitive comparison against the string `imperial`; every other value — `metric`, `IMPERIAL`, `Imperial`, `us`, an empty value, or the parameter being absent — yields the metric variant with a 200. An unrecognised value is never an error. See the `CurrentWeatherImperial` schema for what `imperial` actually changes: it is additive, not a conversion.","schema":{"type":"string"},"example":"imperial","x-helperg-validated":false,"x-helperg-imperial-trigger":"imperial"}],"get":{"operationId":"getPublicCurrentWeather","summary":"Public current weather at a point","description":"Current conditions from the same MET Norway-backed service as `/api/v1/weather/current`, with two public constraints: `lat` and `lon` are rounded to two decimals (about 1.1 km) before they become a cache key, and the body is `PublicWeatherCurrent`, which carries the readings, the condition, the source and the licence but not the provider's raw symbol or fetch provenance. The point in the body is the point that was served. Weather data from MET Norway is licensed CC BY 4.0 and the attribution in the body must be displayed wherever the readings appear.","tags":["weather"],"security":[],"responses":{"200":{"description":"Current conditions, with freshness in `meta`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"ETag":{"$ref":"#/components/headers/ETag"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicWeatherCurrentResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"502":{"$ref":"#/components/responses/Error502"},"503":{"$ref":"#/components/responses/Error503"},"504":{"$ref":"#/components/responses/Error504"}},"x-helperg-access":"PUBLIC","x-helperg-required-scope":null,"x-helperg-route-name":"public.weather.current","x-helperg-requires-durable-storage":false,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","WEATHER_LOCATION_INVALID","UPSTREAM_UNAVAILABLE","UPSTREAM_INVALID_PAYLOAD","UPSTREAM_PAYLOAD_TOO_LARGE","UPSTREAM_TIMEOUT"],"x-helperg-public-cache-seconds":600},"head":{"operationId":"getPublicCurrentWeatherHead","summary":"Public current weather at a point (headers only)","description":"Current conditions from the same MET Norway-backed service as `/api/v1/weather/current`, with two public constraints: `lat` and `lon` are rounded to two decimals (about 1.1 km) before they become a cache key, and the body is `PublicWeatherCurrent`, which carries the readings, the condition, the source and the licence but not the provider's raw symbol or fetch provenance. The point in the body is the point that was served. Weather data from MET Norway is licensed CC BY 4.0 and the attribution in the body must be displayed wherever the readings appear.","tags":["weather"],"security":[],"responses":{"200":{"description":"Current conditions, with freshness in `meta`. HEAD returns the status, the headers and NO BODY; it is answered wherever GET is, because most uptime monitors send HEAD by default and a route that refused it would report itself unsupported to the tooling most likely to poll it.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"ETag":{"$ref":"#/components/headers/ETag"}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"502":{"$ref":"#/components/responses/Error502"},"503":{"$ref":"#/components/responses/Error503"},"504":{"$ref":"#/components/responses/Error504"}},"x-helperg-access":"PUBLIC","x-helperg-required-scope":null,"x-helperg-route-name":"public.weather.current","x-helperg-requires-durable-storage":false,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","WEATHER_LOCATION_INVALID","UPSTREAM_UNAVAILABLE","UPSTREAM_INVALID_PAYLOAD","UPSTREAM_PAYLOAD_TOO_LARGE","UPSTREAM_TIMEOUT"],"x-helperg-public-cache-seconds":600}},"/api/public/v1/news/events":{"summary":"Public official economic news feed: BLS, Eurostat and ECB publications, newest first","parameters":[{"name":"from","in":"query","required":false,"schema":{"type":"string"},"description":"Inclusive start of the publication window, `YYYY-MM-DD` (UTC) or an ISO instant. Defaults to 366 days before now, or 366 days before `to`."},{"name":"to","in":"query","required":false,"schema":{"type":"string"},"description":"Inclusive end, `YYYY-MM-DD` (to the end of that day, UTC) or an ISO instant. Defaults to now. The window may not exceed 366 days."},{"name":"source","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated registered source slugs: `bls-news-releases`, `eurostat-news-releases`, `ecb-press-releases`. Exact, case-sensitive. At most 10.","x-helperg-max-items":10,"example":"eurostat-news-releases"},{"name":"releaseFamily","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated release-definition slugs, exact. At most 10.","x-helperg-max-items":10,"example":"eurostat-euro-indicators-hicp"},{"name":"country","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated ISO 3166-1 alpha-2 codes, upper-cased. At most 10.","x-helperg-max-items":10,"example":"US"},{"name":"jurisdiction","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated HELPERG region codes or country codes (`EA`, `EU27_2020`, `US`), upper-cased. At most 10.","x-helperg-max-items":10,"example":"EA"},{"name":"currency","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated ISO 4217 codes, upper-cased. At most 10.","x-helperg-max-items":10,"example":"EUR"},{"name":"topic","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated HELPERG topics (`inflation`, `labour-market`, `monetary-policy`, …). At most 10.","x-helperg-max-items":10,"example":"inflation"},{"name":"series","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated series slugs; matches publications linked to any of them. At most 10.","x-helperg-max-items":10,"example":"ea-hicp-yoy"},{"name":"calendarEvent","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated calendar event ids; matches publications linked to any of them. At most 10.","x-helperg-max-items":10},{"name":"state","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated publication states: PUBLISHED, UPDATED, CORRECTED, WITHDRAWN, UNKNOWN. At most 10.","x-helperg-max-items":10,"example":"PUBLISHED"},{"name":"linkStatus","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated link statuses: LINKED, PARTIALLY_LINKED, UNLINKED, AMBIGUOUS. At most 10.","x-helperg-max-items":10},{"name":"type","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated publication types: NEWS_RELEASE, STATISTICAL_RELEASE, PRESS_RELEASE, MONETARY_POLICY_DECISION, MONETARY_POLICY_STATEMENT, MONETARY_POLICY_ACCOUNT. At most 10.","x-helperg-max-items":10},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Page size. Default 20, clamped to 100."},{"name":"cursor","in":"query","required":false,"description":"OPAQUE page token, taken verbatim from `meta.nextCursor`. Do not construct, parse or store one: it encodes an offset into the FILTERED result set, so it is meaningful only alongside the identical filter arguments, and reusing it with different filters pages through a different set without saying so. A token that does not decode returns `VALIDATION_FAILED`. A token that decodes past the end of the results returns an empty page with a null `nextCursor`, not an error. Paging is NOT a snapshot: an ingestion run can rebuild the corpus between two pages.","schema":{"type":"string"}}],"get":{"operationId":"getPublicNewsEvents","summary":"Public official economic news feed: BLS, Eurostat and ECB publications, newest first","description":"Every record is an official publication of its named publisher — a statistical agency's news release, a central bank's press release, monetary policy decision, statement, meeting account or statistical release — with the publisher's title, the excerpt only where the publisher's terms permit reuse, the canonical link to the publisher's own page, HELPERG's classification marked as such, and deterministic links (with evidence) to the calendar events, series and observations HELPERG holds. HELPERG is not the publisher and reproduces no full article.\n\nNo forecast, consensus, surprise or sentiment field exists. `state` is never inferred from a scheduled time having passed; CORRECTED and WITHDRAWN appear only on an explicit source signal, which no v1 feed carries.\n\nServed from a published snapshot: at most the thirteen month shards a 366-day window touches, never a listing. `meta.dataStatus` is `stale` when no news ingestion has succeeded for three days.\n\nNo user request ever reaches BLS, Eurostat or the ECB. Reads are served from persisted state, because a calendar request that fetched live would inherit six agencies' latency, rate limits and outages, and would make HELPERG's availability a function of theirs.\n\nSTORAGE. This route reads persisted state and cannot answer without it. Where no durable store is configured, every call returns 503 `SERVICE_UNAVAILABLE`; where one is configured but no ingestion has ever completed, the same code is returned rather than an empty 200, because an empty calendar is a statement about the world and \"nothing has been ingested here\" is a statement about this deployment. An index that EXISTS and matches nothing is still an honest empty 200. Read `x-helperg-durable-storage` at the root of this document for which of those applies to the deployment serving it.","tags":["news"],"security":[],"responses":{"200":{"description":"Matching publications, newest first. `meta.totalMatching` counts before paging; `meta.nextCursor` pages within one snapshot generation.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"ETag":{"$ref":"#/components/headers/ETag"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicNewsEventsResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"PUBLIC","x-helperg-required-scope":null,"x-helperg-route-name":"public.news.events","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED","ECONOMICS_RANGE_INVALID","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"],"x-helperg-public-cache-seconds":300},"head":{"operationId":"getPublicNewsEventsHead","summary":"Public official economic news feed: BLS, Eurostat and ECB publications, newest first (headers only)","description":"Every record is an official publication of its named publisher — a statistical agency's news release, a central bank's press release, monetary policy decision, statement, meeting account or statistical release — with the publisher's title, the excerpt only where the publisher's terms permit reuse, the canonical link to the publisher's own page, HELPERG's classification marked as such, and deterministic links (with evidence) to the calendar events, series and observations HELPERG holds. HELPERG is not the publisher and reproduces no full article.\n\nNo forecast, consensus, surprise or sentiment field exists. `state` is never inferred from a scheduled time having passed; CORRECTED and WITHDRAWN appear only on an explicit source signal, which no v1 feed carries.\n\nServed from a published snapshot: at most the thirteen month shards a 366-day window touches, never a listing. `meta.dataStatus` is `stale` when no news ingestion has succeeded for three days.\n\nNo user request ever reaches BLS, Eurostat or the ECB. Reads are served from persisted state, because a calendar request that fetched live would inherit six agencies' latency, rate limits and outages, and would make HELPERG's availability a function of theirs.\n\nSTORAGE. This route reads persisted state and cannot answer without it. Where no durable store is configured, every call returns 503 `SERVICE_UNAVAILABLE`; where one is configured but no ingestion has ever completed, the same code is returned rather than an empty 200, because an empty calendar is a statement about the world and \"nothing has been ingested here\" is a statement about this deployment. An index that EXISTS and matches nothing is still an honest empty 200. Read `x-helperg-durable-storage` at the root of this document for which of those applies to the deployment serving it.","tags":["news"],"security":[],"responses":{"200":{"description":"Matching publications, newest first. `meta.totalMatching` counts before paging; `meta.nextCursor` pages within one snapshot generation. HEAD returns the status, the headers and NO BODY; it is answered wherever GET is, because most uptime monitors send HEAD by default and a route that refused it would report itself unsupported to the tooling most likely to poll it.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"ETag":{"$ref":"#/components/headers/ETag"}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"403":{"$ref":"#/components/responses/Error403"},"405":{"$ref":"#/components/responses/Error405"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}},"x-helperg-access":"PUBLIC","x-helperg-required-scope":null,"x-helperg-route-name":"public.news.events","x-helperg-requires-durable-storage":true,"x-helperg-error-codes":["BAD_REQUEST","UNAUTHORIZED","FORBIDDEN","METHOD_NOT_ALLOWED","RATE_LIMITED","CONFIGURATION_ERROR","INTERNAL","VALIDATION_FAILED","ECONOMICS_RANGE_INVALID","SERVICE_UNAVAILABLE","STORAGE_UNAVAILABLE"],"x-helperg-public-cache-seconds":300}}},"components":{"securitySchemes":{"helpergApiKeyBearer":{"type":"http","scheme":"bearer","bearerFormat":"opaque","description":"The preferred form: `Authorization: Bearer <key>`. The header value is trimmed and matched case-insensitively against `Bearer`, so `bearer <key>` is also accepted, and the key itself is trimmed. Configuration holds SHA-256 digests of keys and never the keys themselves, so a leaked configuration value is not a usable credential. If an `Authorization` header is present but is NOT Bearer-shaped, it is ignored and the `X-Helperg-Api-Key` header is consulted instead."},"helpergApiKeyHeader":{"type":"apiKey","in":"header","name":"X-Helperg-Api-Key","description":"The fallback form, for clients whose transport already uses `Authorization` for something else. The value is trimmed; an empty or whitespace-only value counts as no credential at all. It is consulted only when no Bearer-shaped `Authorization` header was found."}},"headers":{"XRequestId":{"description":"Present on EVERY response, success or failure, and equal to `meta.requestId` or `error.requestId`. It is the only handle a caller has on a server log line.","schema":{"type":"string"}},"RateLimitLimit":{"description":"Requests permitted in the current window for this caller on THIS ROUTE. The counter is keyed per route name, so an allowance spent on one route is still available on another. The counter is also per instance: it is not a fleet-wide ceiling, and no fleet-wide limiter exists in this deployment.","schema":{"type":"integer"}},"RateLimitRemaining":{"description":"Requests left in the current window, on this instance.","schema":{"type":"integer"}},"RateLimitReset":{"description":"Seconds until the current window ends.","schema":{"type":"integer"}},"RetryAfter":{"description":"Seconds to wait before retrying. Sent on 429 only.","schema":{"type":"integer"}},"Allow":{"description":"The methods this route accepts. Sent on 405 only. `GET, HEAD` for every read route; `POST` for the ingestion routes, which is why a HEAD probe against one of those is a 405 rather than a health check.","schema":{"type":"string"},"example":"GET, HEAD"},"ETag":{"description":"Sent on every shared-cacheable PUBLIC success. A WEAK validator computed over the `data` section only — `meta.requestId` and `meta.generatedAt` change on every response and are deliberately outside it — so a request carrying `If-None-Match` with a matching tag receives 304 with no body when the data is unchanged. Rate-limit accounting and headers are identical on a 304.","schema":{"type":"string"},"example":"W/\"3f9a1c2b4d5e6f70819a2b3c4d5e6f70\""},"CacheControl":{"description":"Sent on every response. `no-store` for everything that is not PUBLIC, because an internal response sitting in a shared cache is an access-control failure waiting to happen. The PUBLIC registry routes send `public, max-age=3600, stale-while-revalidate=7200`: their content is compiled into the build and cannot change until the deployment does. `/api/v1/health` is PUBLIC but sends `no-store` to every caller, anonymous included: it answers one URL with two different bodies depending on whether a credential was presented, and a shared cache keys on the URL alone, so a cached anonymous body would be served to a credentialed operator.","schema":{"type":"string"},"example":"public, max-age=3600, stale-while-revalidate=7200"}},"responses":{"Error400":{"description":"Codes that can arrive with this status anywhere in this document. The subset one operation can produce is listed on that operation as `x-helperg-error-codes`.\n\n`BAD_REQUEST` — The query string exceeded 2048 characters and was refused before parsing. Reachable on every route.\n\n`VALIDATION_FAILED` — A required parameter was missing or empty; a parameter exceeded 200 characters; `tz` was not a known IANA zone; `limit` was not a positive integer; `cursor` did not decode; a comma-separated filter carried more values than its ceiling; or `region`, `importance` or `category` carried a value outside its closed vocabulary.\n\n`WEATHER_LOCATION_INVALID` — `lat`, `lon` or `altitude` was not a finite number or fell outside its permitted range.\n\n`ECONOMICS_RANGE_INVALID` — `from` or `to` was neither a calendar date nor an instant carrying a zone, or the range was inverted or longer than 400 days. Raised by the events and calendar routes only: the observations route compares `from` and `to` as reference-period codes and never parses them as dates.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Error404":{"description":"Codes that can arrive with this status anywhere in this document. The subset one operation can produce is listed on that operation as `x-helperg-error-codes`.\n\n`NOT_FOUND` — No indicator family or series is registered under the supplied slug, or the slug did not match `^[a-z0-9-]+$` and was refused rather than falling through to the collection listing. ALSO returned by every ADMIN_INTERNAL route when this deployment has no admin credential configured — a 401 there would confirm the surface exists.\n\n`SOURCE_DISABLED` — The MET Norway source is switched off in the source registry, which is how a provider is taken out of service without a code change at the adapter.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Error401":{"description":"Codes that can arrive with this status anywhere in this document. The subset one operation can produce is listed on that operation as `x-helperg-error-codes`.\n\n`UNAUTHORIZED` — No credential was presented to a route that requires one, or the presented credential is not recognised. A presented-but-unrecognised credential fails this way even on a PUBLIC route, rather than being quietly downgraded to anonymous.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Error403":{"description":"Codes that can arrive with this status anywhere in this document. The subset one operation can produce is listed on that operation as `x-helperg-error-codes`.\n\n`FORBIDDEN` — The credential verified, but its consumer is not enabled in the registry, or it does not hold the scope the route requires.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Error405":{"description":"Codes that can arrive with this status anywhere in this document. The subset one operation can produce is listed on that operation as `x-helperg-error-codes`.\n\n`METHOD_NOT_ALLOWED` — A method outside the route's allowlist was used. The read routes allow GET and HEAD; the ingestion routes allow POST ONLY, so a HEAD probe against one of those is a 405. OPTIONS reaches the allowlist on an ECOSYSTEM_INTERNAL or ADMIN_INTERNAL route; on a PUBLIC route it is answered as a preflight before the allowlist is consulted.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"Allow":{"$ref":"#/components/headers/Allow"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Error429":{"description":"Codes that can arrive with this status anywhere in this document. The subset one operation can produce is listed on that operation as `x-helperg-error-codes`.\n\n`RATE_LIMITED` — The per-instance fixed-window counter for this route name and subject was exhausted.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Error500":{"description":"Codes that can arrive with this status anywhere in this document. The subset one operation can produce is listed on that operation as `x-helperg-error-codes`.\n\n`INTERNAL` — A normalised record failed canonical validation, a stored document did not parse back into its envelope, or an unclassified error was thrown. No detail of any of them ever reaches the response.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Error503":{"description":"Codes that can arrive with this status anywhere in this document. The subset one operation can produce is listed on that operation as `x-helperg-error-codes`.\n\n`CONFIGURATION_ERROR` — The deployment is misconfigured — a cache driver this build cannot provide was named in production, the outbound URL was rejected by the SSRF guard, or the durable store rejected this deployment's credentials.\n\n`STORAGE_UNAVAILABLE` — A CONFIGURED durable store failed this request: a transport error, or a conditional write that lost its bounded retries. RETRYABLE — the condition clears on its own, which is the entire reason it is a separate code from SERVICE_UNAVAILABLE.\n\n`SERVICE_UNAVAILABLE` — Either this deployment has no durable storage configured at all, so every storage-backed route fails closed; or a store is configured and the materialised index this route reads has never been published, which is a state the surface refuses to answer from. NOT retryable: both end only when an operator acts.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Error502":{"description":"Codes that can arrive with this status anywhere in this document. The subset one operation can produce is listed on that operation as `x-helperg-error-codes`.\n\n`UPSTREAM_UNAVAILABLE` — MET Norway was unreachable, rate-limited HELPERG, or answered a non-2xx status. Weather only: an ingestion route reports a provider failure as a FAILED run, never as this code.\n\n`UPSTREAM_INVALID_PAYLOAD` — The MET Norway payload did not parse, failed the provider schema, declared units HELPERG does not accept, carried an empty timeseries, or answered 304 while HELPERG held no cached copy.\n\n`UPSTREAM_PAYLOAD_TOO_LARGE` — The MET Norway response exceeded the configured outbound byte ceiling.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"Error504":{"description":"Codes that can arrive with this status anywhere in this document. The subset one operation can produce is listed on that operation as `x-helperg-error-codes`.\n\n`UPSTREAM_TIMEOUT` — MET Norway did not answer, or did not finish sending its body, before the outbound timeout.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"schemas":{"GeoPoint":{"type":"object","description":"A WGS 84 point. Coordinates are rounded to four decimal places, which MET Norway's terms require and which is what makes the cache key space bounded.","properties":{"latitude":{"type":"number","minimum":-90,"maximum":90},"longitude":{"type":"number","minimum":-180,"maximum":180},"altitudeMetres":{"type":["number","null"],"description":"Metres above mean sea level, whole metres."}},"required":["latitude","longitude","altitudeMetres"],"additionalProperties":false},"WeatherCondition":{"type":"object","description":"What the sky is doing, decomposed into three orthogonal facts so that no consumer is bound to a provider's icon vocabulary.","properties":{"code":{"type":"string","enum":["CLEAR","FAIR","PARTLY_CLOUDY","CLOUDY","FOG","LIGHT_RAIN","RAIN","HEAVY_RAIN","LIGHT_RAIN_SHOWERS","RAIN_SHOWERS","HEAVY_RAIN_SHOWERS","LIGHT_SLEET","SLEET","HEAVY_SLEET","LIGHT_SLEET_SHOWERS","SLEET_SHOWERS","HEAVY_SLEET_SHOWERS","LIGHT_SNOW","SNOW","HEAVY_SNOW","LIGHT_SNOW_SHOWERS","SNOW_SHOWERS","HEAVY_SNOW_SHOWERS","UNKNOWN"],"description":"`UNKNOWN` is a real answer, not a failure: an unrecognised provider symbol becomes UNKNOWN rather than being guessed into the nearest familiar code."},"thunder":{"type":"boolean","description":"Whether thunder accompanies the condition."},"dayPart":{"type":["string","null"],"enum":["DAY","NIGHT","POLAR_TWILIGHT",null],"description":"Daylight state at that moment. Null when the provider does not distinguish day from night for this code."},"providerSymbol":{"type":["string","null"],"description":"The provider's own string, retained for traceability. Consumers MUST NOT branch on it; it is provenance, not contract."}},"required":["code","thunder","dayPart","providerSymbol"],"additionalProperties":false},"Provenance":{"type":"object","description":"Where this record came from and what HELPERG did to it. Carried by every canonical record on every surface, weather and economics alike. Never stripped, and never assembled by an adapter by hand: the licence fields are resolved from the source registry, so an adapter cannot claim a permission the registry does not grant.","properties":{"sourceId":{"type":"string","description":"Slug of the registered source. Always resolvable in `/api/v1/sources`."},"sourceRecordId":{"type":["string","null"],"description":"The source's own identifier for the record, when it publishes one."},"sourceUrl":{"type":["string","null"],"description":"The exact provider endpoint the value came from."},"fetchedAt":{"type":"string","format":"date-time","description":"When HELPERG retrieved it from the provider."},"sourcePublishedAt":{"type":["string","null"],"format":"date-time","description":"When the SOURCE says it was published. Null when the source is silent."},"sourceUpdatedAt":{"type":["string","null"],"format":"date-time","description":"When the SOURCE says it was last updated — for MET Norway, the model run time."},"normalizedAt":{"type":"string","format":"date-time","description":"When HELPERG normalised it into the canonical model."},"adapter":{"type":"string","description":"Stable adapter identifier, e.g. `met-no/locationforecast` or `bls/release-schedule`."},"adapterVersion":{"type":"string","description":"Adapter contract version. Bumped whenever normalisation output changes, so a record normalised by an older adapter stays identifiable."},"transformation":{"type":"array","items":{"type":"string","enum":["NONE","REMAP","UNIT_CONVERSION","CODE_MAPPING","AGGREGATION","TIMEZONE_NORMALISATION"]},"minItems":1,"description":"Every transformation applied, in the order applied."},"derivation":{"type":"string","enum":["SOURCE","HELPERG_DERIVED"],"description":"`HELPERG_DERIVED` means HELPERG computed the value and is accountable for it; it must not be presented as something the source published. Daily forecast records are derived; current conditions, hourly records and every economic record are not."},"attributionRequired":{"type":"boolean"},"attributionText":{"type":["string","null"],"description":"The credit sentence the licence requires, snapshotted at normalisation time."},"redistributionAllowed":{"type":"boolean","description":"Whether HELPERG may pass this record on to third parties. Copied from the licensing policy's decision, never asserted by the adapter."}},"required":["sourceId","sourceRecordId","sourceUrl","fetchedAt","sourcePublishedAt","sourceUpdatedAt","normalizedAt","adapter","adapterVersion","transformation","derivation","attributionRequired","attributionText","redistributionAllowed"],"additionalProperties":false},"AttributionRecord":{"type":"object","description":"A licensing obligation, resolved. Rendering the data while ignoring this is a breach of the source licence.","properties":{"sourceId":{"type":"string"},"sourceName":{"type":"string"},"sourceUrl":{"type":"string"},"required":{"type":"boolean","description":"Must this credit be displayed wherever the data appears?"},"text":{"type":["string","null"],"description":"The exact sentence to display. Null only when no credit is required."},"licenseName":{"type":["string","null"],"description":"e.g. `CC BY 4.0`."},"licenseUrl":{"type":["string","null"],"description":"Canonical URL of the licence text."},"termsUrl":{"type":["string","null"],"description":"The provider's terms of service."},"modifiedByHelperg":{"type":"boolean","description":"True when HELPERG transformed the values. CC BY and similar licences require changes to be indicated, and a consumer cannot know this from the source name alone. Always true on the weather surface and on the calendar, where provider vocabularies are remapped; false on the observations surface, where a published figure is reproduced exactly as the source stated it."}},"required":["sourceId","sourceName","sourceUrl","required","text","licenseName","licenseUrl","termsUrl","modifiedByHelperg"],"additionalProperties":false},"CurrentWeatherMetric":{"type":"object","description":"Instantaneous conditions in canonical units: Celsius, metres per second, hectopascals, millimetres.","properties":{"location":{"$ref":"#/components/schemas/GeoPoint"},"observedAt":{"type":"string","format":"date-time","description":"The instant these values describe, per the provider."},"feelsLikeC":{"type":["number","null"],"description":"Apparent temperature in degrees Celsius."},"dewPointC":{"type":["number","null"],"description":"Dew point in degrees Celsius."},"humidityPercent":{"type":["number","null"],"description":"Relative humidity, 0 to 100."},"pressureHpa":{"type":["number","null"],"description":"Air pressure at sea level in hectopascals."},"windSpeedMps":{"type":["number","null"],"description":"Wind speed in metres per second."},"windGustMps":{"type":["number","null"],"description":"Wind gust speed in metres per second."},"windDirectionDegrees":{"type":["number","null"],"description":"Direction the wind comes FROM, in degrees, 0 to 360. Meteorological convention."},"precipitationMm":{"type":["number","null"],"description":"Precipitation over the hour beginning at `observedAt`, in millimetres."},"cloudCoverPercent":{"type":["number","null"],"description":"Cloud cover, 0 to 100."},"visibilityMetres":{"type":["number","null"],"description":"Visibility in metres. Always null for MET Norway sourced records: MET publishes no visibility parameter, and deriving one would be an invented reading."},"uvIndex":{"type":["number","null"],"description":"Clear-sky ultraviolet index, 0 to 20."},"condition":{"$ref":"#/components/schemas/WeatherCondition"},"provenance":{"$ref":"#/components/schemas/Provenance"},"temperatureC":{"type":["number","null"],"description":"Air temperature in degrees Celsius."}},"required":["location","observedAt","feelsLikeC","dewPointC","humidityPercent","pressureHpa","windSpeedMps","windGustMps","windDirectionDegrees","precipitationMm","cloudCoverPercent","visibilityMetres","uvIndex","condition","provenance","temperatureC"],"additionalProperties":false},"CurrentWeatherImperial":{"type":"object","description":"The SAME record with six imperial fields ADDED and exactly one metric field REMOVED. Only `temperatureC` disappears. `feelsLikeC`, `dewPointC`, `windSpeedMps`, `windGustMps` and `precipitationMm` are all still present alongside their imperial siblings, and `humidityPercent`, `pressureHpa`, `windDirectionDegrees`, `cloudCoverPercent`, `visibilityMetres` and `uvIndex` are untouched — pressure is never converted to inches of mercury and visibility is never converted to feet. This is an additive projection, not a unit conversion of the payload.","properties":{"location":{"$ref":"#/components/schemas/GeoPoint"},"observedAt":{"type":"string","format":"date-time","description":"The instant these values describe, per the provider."},"feelsLikeC":{"type":["number","null"],"description":"Apparent temperature in degrees Celsius."},"dewPointC":{"type":["number","null"],"description":"Dew point in degrees Celsius."},"humidityPercent":{"type":["number","null"],"description":"Relative humidity, 0 to 100."},"pressureHpa":{"type":["number","null"],"description":"Air pressure at sea level in hectopascals."},"windSpeedMps":{"type":["number","null"],"description":"Wind speed in metres per second."},"windGustMps":{"type":["number","null"],"description":"Wind gust speed in metres per second."},"windDirectionDegrees":{"type":["number","null"],"description":"Direction the wind comes FROM, in degrees, 0 to 360. Meteorological convention."},"precipitationMm":{"type":["number","null"],"description":"Precipitation over the hour beginning at `observedAt`, in millimetres."},"cloudCoverPercent":{"type":["number","null"],"description":"Cloud cover, 0 to 100."},"visibilityMetres":{"type":["number","null"],"description":"Visibility in metres. Always null for MET Norway sourced records: MET publishes no visibility parameter, and deriving one would be an invented reading."},"uvIndex":{"type":["number","null"],"description":"Clear-sky ultraviolet index, 0 to 20."},"condition":{"$ref":"#/components/schemas/WeatherCondition"},"provenance":{"$ref":"#/components/schemas/Provenance"},"temperatureF":{"type":["number","null"],"description":"Air temperature in degrees Fahrenheit, rounded to one decimal place. The ONLY replacement for a removed field: `temperatureC` is absent from this variant."},"feelsLikeF":{"type":["number","null"],"description":"Apparent temperature in degrees Fahrenheit, rounded to one decimal place. Added beside `feelsLikeC`, which is retained."},"dewPointF":{"type":["number","null"],"description":"Dew point in degrees Fahrenheit, rounded to one decimal place. Added beside `dewPointC`, which is retained."},"windSpeedMph":{"type":["number","null"],"description":"Wind speed in miles per hour, rounded to one decimal place. Added beside `windSpeedMps`, which is retained."},"windGustMph":{"type":["number","null"],"description":"Wind gust speed in miles per hour, rounded to one decimal place. Added beside `windGustMps`, which is retained."},"precipitationIn":{"type":["number","null"],"description":"Precipitation in inches, rounded to one decimal place. Added beside `precipitationMm`, which is retained."}},"required":["location","observedAt","feelsLikeC","dewPointC","humidityPercent","pressureHpa","windSpeedMps","windGustMps","windDirectionDegrees","precipitationMm","cloudCoverPercent","visibilityMetres","uvIndex","condition","provenance","temperatureF","feelsLikeF","dewPointF","windSpeedMph","windGustMph","precipitationIn"],"additionalProperties":false},"WeatherCurrentDataMetric":{"type":"object","properties":{"units":{"const":"metric","description":"Served whenever `units` is anything other than the exact lower-case string `imperial` — including when it is absent, empty, `metric`, or `IMPERIAL`."},"current":{"$ref":"#/components/schemas/CurrentWeatherMetric"}},"required":["units","current"],"additionalProperties":false},"WeatherCurrentDataImperial":{"type":"object","properties":{"units":{"const":"imperial","description":"Served only when `units` is exactly `imperial`. The comparison is case-sensitive."},"current":{"$ref":"#/components/schemas/CurrentWeatherImperial"}},"required":["units","current"],"additionalProperties":false},"WeatherCurrentData":{"description":"Which variant arrives is decided entirely by the `units` query parameter, and the variant announces itself in `units` so a consumer never has to infer the system from a field name.","oneOf":[{"$ref":"#/components/schemas/WeatherCurrentDataMetric"},{"$ref":"#/components/schemas/WeatherCurrentDataImperial"}],"discriminator":{"propertyName":"units","mapping":{"metric":"#/components/schemas/WeatherCurrentDataMetric","imperial":"#/components/schemas/WeatherCurrentDataImperial"}}},"WeatherCurrentMeta":{"type":"object","description":"The envelope meta for this route. The four fields after `attributionRecords` are this route's own additions, spread flat into meta by `buildMeta` rather than nested under an `extra` key.","properties":{"requestId":{"type":"string","description":"Correlates with the `X-Request-Id` response header and with the server log line for this request."},"generatedAt":{"type":"string","format":"date-time","description":"When this response was assembled. Not the age of the data — read `cacheAgeSeconds` for that."},"cached":{"type":"boolean","description":"True when the payload behind this response came from the in-process cache rather than being assembled for this request."},"cacheAgeSeconds":{"type":"number","minimum":0,"description":"Age of the cached payload in seconds. Zero when nothing was served from cache."},"stale":{"type":"boolean","description":"True when the payload is past its freshness window and is being served anyway."},"degraded":{"type":"boolean","description":"True when a provider failed and the last good payload is being served in its place. The values are real, and `cacheAgeSeconds` states how old they are."},"sourceCount":{"type":"integer","minimum":0,"description":"Number of entries in `sources`."},"sources":{"type":"array","items":{"type":"string"},"description":"Registered source slugs behind this payload. Always `[\"met-no\"]` for the weather surface today."},"attribution":{"type":"array","items":{"type":"string"},"description":"Credit lines that MUST be displayed wherever this data appears. Retained as plain strings for v1 compatibility; `attributionRecords` carries the same obligation resolved."},"attributionRecords":{"type":"array","items":{"$ref":"#/components/schemas/AttributionRecord"},"description":"The attribution obligation with its licence name and URL attached, so a consumer never has to infer licensing rules from a provider name."},"observedAt":{"type":"string","format":"date-time","description":"The instant the readings describe, repeated from the payload so a consumer can check freshness without reaching into `data`."},"fetchedAt":{"type":"string","format":"date-time","description":"When HELPERG last retrieved the provider payload behind this response."},"providerModelUpdatedAt":{"type":["string","null"],"format":"date-time","description":"When the provider says the underlying model output was produced. Null when the provider states none."},"canonicalUnits":{"type":"object","description":"The units HELPERG stores in, always, whatever `units` was requested. Canonical storage stays metric so a presentation bug can never contaminate a stored record.","properties":{"temperature":{"const":"celsius"},"windSpeed":{"const":"m/s"},"pressure":{"const":"hPa"},"precipitation":{"const":"mm"}},"required":["temperature","windSpeed","pressure","precipitation"],"additionalProperties":false}},"required":["requestId","generatedAt","cached","cacheAgeSeconds","stale","degraded","sourceCount","sources","attribution","attributionRecords","observedAt","fetchedAt","providerModelUpdatedAt","canonicalUnits"],"additionalProperties":false},"HourlyForecast":{"type":"object","description":"One hour, beginning at `startsAt`. Only provider entries carrying a one-hour block become hours: beyond roughly two days the provider switches to six-hour blocks, and presenting one of those as an \"hour\" would overstate the forecast's resolution. At most 48 are returned.","properties":{"startsAt":{"type":"string","format":"date-time","description":"Start of the hour this entry describes."},"temperatureC":{"type":["number","null"],"description":"Air temperature in degrees Celsius."},"feelsLikeC":{"type":["number","null"],"description":"Apparent temperature in degrees Celsius."},"humidityPercent":{"type":["number","null"],"description":"Relative humidity, 0 to 100."},"pressureHpa":{"type":["number","null"],"description":"Air pressure at sea level in hectopascals."},"windSpeedMps":{"type":["number","null"],"description":"Wind speed in metres per second."},"windDirectionDegrees":{"type":["number","null"],"description":"Direction the wind comes FROM, in degrees, 0 to 360."},"precipitationMm":{"type":["number","null"],"description":"Total precipitation over this hour, in millimetres."},"precipitationProbabilityPercent":{"type":["number","null"],"description":"Probability of precipitation over this hour, 0 to 100."},"cloudCoverPercent":{"type":["number","null"],"description":"Cloud cover, 0 to 100."},"condition":{"$ref":"#/components/schemas/WeatherCondition"},"provenance":{"$ref":"#/components/schemas/Provenance"}},"required":["startsAt","temperatureC","feelsLikeC","humidityPercent","pressureHpa","windSpeedMps","windDirectionDegrees","precipitationMm","precipitationProbabilityPercent","cloudCoverPercent","condition","provenance"],"additionalProperties":false},"DailyForecast":{"type":"object","description":"One calendar day in `timeZone`. HELPERG assembles these from sub-daily provider values — the provider publishes no day record — so `provenance.derivation` is `HELPERG_DERIVED`. The first and last day of the range are normally partial.","properties":{"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Calendar date in `timeZone`. Meaningless without it: the same instants fall on different days in Prague and in Los Angeles."},"timeZone":{"type":"string","description":"The IANA zone this day was computed in. Echoes the request's `tz`."},"temperatureMinC":{"type":["number","null"],"description":"Lowest temperature for the day in degrees Celsius."},"temperatureMaxC":{"type":["number","null"],"description":"Highest temperature for the day in degrees Celsius. Never below `temperatureMinC`; a record that broke that is rejected before it can be served."},"precipitationMm":{"type":["number","null"],"description":"Total precipitation for the day in millimetres. Overlapping provider windows are never double-counted."},"precipitationProbabilityPercent":{"type":["number","null"],"description":"Highest probability of precipitation across the day's windows, 0 to 100."},"windSpeedMaxMps":{"type":["number","null"],"description":"Highest wind speed for the day in metres per second."},"condition":{"$ref":"#/components/schemas/WeatherCondition"},"provenance":{"$ref":"#/components/schemas/Provenance"}},"required":["date","timeZone","temperatureMinC","temperatureMaxC","precipitationMm","precipitationProbabilityPercent","windSpeedMaxMps","condition","provenance"],"additionalProperties":false},"WeatherForecastData":{"type":"object","properties":{"location":{"$ref":"#/components/schemas/GeoPoint"},"timeZone":{"type":"string","description":"The zone day boundaries were computed in. Echoes `tz`, defaulting to `UTC`. It is never guessed from the caller's IP or headers, which is how a forecast ends up filed under the wrong day."},"partialDayNote":{"type":"string","description":"A fixed sentence warning that the first and last day may be partial. Present on every response."},"daily":{"type":"array","items":{"$ref":"#/components/schemas/DailyForecast"},"description":"Calendar days, earliest first, truncated to `limit`."},"hourly":{"type":"array","items":{"$ref":"#/components/schemas/HourlyForecast"},"description":"Hourly entries, earliest first. NOT affected by `limit`, which truncates `daily` only."}},"required":["location","timeZone","partialDayNote","daily","hourly"],"additionalProperties":false},"WeatherForecastMeta":{"type":"object","description":"The envelope meta for this route. The four fields after `attributionRecords` are this route's own additions, spread flat into meta.","properties":{"requestId":{"type":"string","description":"Correlates with the `X-Request-Id` response header and with the server log line for this request."},"generatedAt":{"type":"string","format":"date-time","description":"When this response was assembled. Not the age of the data — read `cacheAgeSeconds` for that."},"cached":{"type":"boolean","description":"True when the payload behind this response came from the in-process cache rather than being assembled for this request."},"cacheAgeSeconds":{"type":"number","minimum":0,"description":"Age of the cached payload in seconds. Zero when nothing was served from cache."},"stale":{"type":"boolean","description":"True when the payload is past its freshness window and is being served anyway."},"degraded":{"type":"boolean","description":"True when a provider failed and the last good payload is being served in its place. The values are real, and `cacheAgeSeconds` states how old they are."},"sourceCount":{"type":"integer","minimum":0,"description":"Number of entries in `sources`."},"sources":{"type":"array","items":{"type":"string"},"description":"Registered source slugs behind this payload. Always `[\"met-no\"]` for the weather surface today."},"attribution":{"type":"array","items":{"type":"string"},"description":"Credit lines that MUST be displayed wherever this data appears. Retained as plain strings for v1 compatibility; `attributionRecords` carries the same obligation resolved."},"attributionRecords":{"type":"array","items":{"$ref":"#/components/schemas/AttributionRecord"},"description":"The attribution obligation with its licence name and URL attached, so a consumer never has to infer licensing rules from a provider name."},"fetchedAt":{"type":"string","format":"date-time","description":"When HELPERG last retrieved the provider payload behind this response."},"providerModelUpdatedAt":{"type":["string","null"],"format":"date-time","description":"When the provider says the underlying model output was produced."},"dayCount":{"type":"integer","minimum":0,"description":"Length of `data.daily` after truncation. Smaller than `limit` when the provider supplied fewer days."},"hourCount":{"type":"integer","minimum":0,"description":"Length of `data.hourly`."}},"required":["requestId","generatedAt","cached","cacheAgeSeconds","stale","degraded","sourceCount","sources","attribution","attributionRecords","fetchedAt","providerModelUpdatedAt","dayCount","hourCount"],"additionalProperties":false},"WeatherCurrentResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WeatherCurrentData"},"meta":{"$ref":"#/components/schemas/WeatherCurrentMeta"}},"required":["data","meta"],"additionalProperties":false},"WeatherForecastResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WeatherForecastData"},"meta":{"$ref":"#/components/schemas/WeatherForecastMeta"}},"required":["data","meta"],"additionalProperties":false},"ReferencePeriod":{"type":"object","description":"The period a statistic DESCRIBES, which is not the date it is published on. A figure published on 15 October describing September is not an October figure, and conflating the two is the commonest structural error in economic data handling — it is silent, every number looks plausible, and every chart is shifted by one period. It is never inferred from the publication date: where a source states no period, `kind` is `UNKNOWN` and stays that way.","properties":{"kind":{"type":"string","enum":["MONTH","QUARTER","YEAR","WEEK","DAY","RANGE","UNKNOWN"],"description":"Granularity of the period."},"code":{"type":"string","description":"Canonical, sortable string form: `2026-08` (month), `2026-Q2` (quarter), `2026` (year), `2026-W33` (ISO week), `2026-08-14` (day), `2026-01-01/2026-06-30` (range), or the literal `UNKNOWN`. This is also the value `from`, `to` and `period` are compared against on the observations route."},"startDate":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Inclusive first day, where the kind determines one. Null for `UNKNOWN`."},"endDate":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Inclusive last day, where the kind determines one. Null for `UNKNOWN`."}},"required":["kind","code","startDate","endDate"],"additionalProperties":false},"EventSourceReference":{"type":"object","description":"A source that reported this same canonical event. Cross-source references ATTACH to an existing event; they never create a second one, which is what keeps one release from appearing twice because two agencies list it.","properties":{"sourceId":{"type":"string"},"externalId":{"type":["string","null"],"description":"The source's own identifier for the record, when it publishes one."},"sourceUrl":{"type":["string","null"],"description":"The document or feed the reference was seen in."},"isPrimary":{"type":"boolean","description":"Whether this is the authoritative source for the event."},"detectedAt":{"type":"string","format":"date-time","description":"When HELPERG first saw this source reporting the event."}},"required":["sourceId","externalId","sourceUrl","isPrimary","detectedAt"],"additionalProperties":false},"EconomicEvent":{"type":"object","description":"One scheduled or actual occurrence of a statistical release. The release DEFINITION is \"the BLS Consumer Price Index\"; the event is \"the CPI published on 15 January 2026\".","properties":{"id":{"type":"string","description":"Stable identity, keyed on the release and the reference period rather than on the date — so a release that MOVES is the same event on a new date instead of a duplicate."},"releaseDefinitionSlug":{"type":"string","description":"The recurring release this occurrence belongs to."},"countryCode":{"type":["string","null"],"description":"ISO 3166-1 alpha-2. Exactly one of `countryCode` and `regionCode` is non-null."},"regionCode":{"type":["string","null"],"enum":["EA","EU27_2020","G7","G20","OECD","WORLD",null],"description":"HELPERG region code for a multinational release. Exactly one of `countryCode` and `regionCode` is non-null."},"currencyCode":{"type":["string","null"],"description":"ISO 4217 code of the currency the release bears on, where one applies."},"title":{"type":"string","description":"The release title as HELPERG records it."},"referencePeriod":{"$ref":"#/components/schemas/ReferencePeriod"},"scheduledDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Calendar date of publication IN `sourceTimezone`. Always known — this is the field the `from`/`to` filter compares against, and the only date a DATE_ONLY release has."},"scheduledAt":{"type":["string","null"],"format":"date-time","description":"The UTC instant of publication, and NULL whenever the source stated only a date (`timePrecision` `DATE_ONLY`) or stated nothing at all (`TBA`). THAT NULL IS A REAL STATE, NOT MISSING DATA. Writing midnight instead would be a lie with teeth: 00:00 UTC on the 15th is the 14th in New York, so a fabricated instant moves the release to the wrong day for a third of the world. A consumer must render the date alone for these events."},"sourceTimezone":{"type":["string","null"],"description":"The IANA zone the source schedules in. Null when the source states none."},"timePrecision":{"type":"string","enum":["EXACT_TIME","DATE_ONLY","TIME_WINDOW","TBA","UNKNOWN"],"description":"How precisely the source states the time. `DATE_ONLY` and `TBA` guarantee `scheduledAt` is null; `EXACT_TIME` guarantees it is not. The canonical schema refuses any record that breaks either rule."},"releasedAt":{"type":["string","null"],"format":"date-time","description":"When the source says it was actually published. Requires source evidence; never set because the clock passed the scheduled time."},"status":{"type":"string","enum":["SCHEDULED","RESCHEDULED","DELAYED","RELEASED","CANCELLED","POSTPONED","TENTATIVE","UNKNOWN"],"description":"AUTHORITATIVE status: what the SOURCE says. It never becomes `RELEASED` because time passed. For what the clock alone supports, read `derivedState` on the calendar entry."},"importance":{"type":"string","enum":["HIGH","MEDIUM","LOW","UNCLASSIFIED"],"description":"HELPERG editorial importance. NOT a measurement of market impact and not a claim about one; `importanceBasis` records how it was arrived at."},"importanceBasis":{"type":"string","enum":["HELPERG_TAXONOMY_V1","SOURCE_DECLARED","MANUAL_EDITORIAL","HISTORICAL_IMPACT_MODEL"],"description":"How the importance value was arrived at. Published with every event so the level is never mistaken for an observed effect."},"sourceId":{"type":"string","description":"Registered slug of the primary source."},"sourceExternalId":{"type":["string","null"],"description":"The primary source's own identifier for this occurrence."},"primarySourceUrl":{"type":["string","null"],"description":"Canonical URL at the primary source."},"sourceReferences":{"type":"array","items":{"$ref":"#/components/schemas/EventSourceReference"},"description":"Every source that reported this same canonical event."},"provenance":{"$ref":"#/components/schemas/Provenance"},"scheduleFetchedAt":{"type":"string","format":"date-time","description":"When the schedule document behind this event was last retrieved."},"normalizedAt":{"type":"string","format":"date-time","description":"When this record was normalised into the canonical model."},"actualDetectedAt":{"type":["string","null"],"format":"date-time","description":"When HELPERG detected the actual publication, where it has."},"lastCheckedAt":{"type":"string","format":"date-time","description":"When this event was last reconciled against its source. Kept separate from the three above rather than collapsed into one ambiguous `updatedAt`."},"createdAt":{"type":"string","format":"date-time","description":"When HELPERG first recorded this event. Preserved across re-ingestion, so a replay cannot make an old record look new."},"updatedAt":{"type":"string","format":"date-time","description":"When this record last changed."}},"required":["id","releaseDefinitionSlug","countryCode","regionCode","currencyCode","title","referencePeriod","scheduledDate","scheduledAt","sourceTimezone","timePrecision","releasedAt","status","importance","importanceBasis","sourceId","sourceExternalId","primarySourceUrl","sourceReferences","provenance","scheduleFetchedAt","normalizedAt","actualDetectedAt","lastCheckedAt","createdAt","updatedAt"],"additionalProperties":false},"EventLinks":{"type":"object","description":"What an event is deterministically connected to. Derived from the release registry at read time and never stored. The ONLY basis for a link is the release definition naming its series; nothing is linked by matching a date or a title, so `seriesSlugs` is empty for every release whose series are not registered — an honest empty, not a failure.","properties":{"releaseDefinitionSlug":{"type":"string","description":"The recurring release this event is an occurrence of."},"seriesSlugs":{"type":"array","items":{"type":"string"},"description":"Canonical series this release publishes. Empty when none is registered for it."},"observationPaths":{"type":"array","items":{"type":"string"},"description":"One `/api/v1/economics/series/{slug}/observations` path per series, in the same order."}},"required":["releaseDefinitionSlug","seriesSlugs","observationPaths"],"additionalProperties":false},"CalendarSourceIngestion":{"type":"object","description":"Freshness of one calendar source's ingestion job. One entry per source that contributes events, whether or not it has ever run on this deployment.","properties":{"sourceId":{"type":"string","description":"Registered source slug."},"jobName":{"type":"string","description":"The ingestion job, e.g. `economics:eurostat-release-calendar`."},"lastSuccessAt":{"type":["string","null"],"format":"date-time","description":"When this source's ingestion last succeeded here. Null means never."},"consecutiveFailures":{"type":"integer","minimum":0}},"required":["sourceId","jobName","lastSuccessAt","consecutiveFailures"],"additionalProperties":false},"CalendarEntry":{"type":"object","description":"An event plus the state the clock supports. This is the shape served by `/v1/economics/events` when NEITHER `tz` nor `timezone` was sent.","properties":{"event":{"$ref":"#/components/schemas/EconomicEvent"},"links":{"$ref":"#/components/schemas/EventLinks"},"derivedState":{"type":"string","enum":["UPCOMING","IMMINENT","PAST_DUE_AWAITING_CONFIRMATION","CONFIRMED_RELEASED","CLOSED"],"description":"What HELPERG can conclude from the clock, computed per request and never stored — which is what makes it impossible to mistake for something a source said. `PAST_DUE_AWAITING_CONFIRMATION` is the honest description of an event whose time has passed with no publication confirmation: HELPERG does not know whether it was released, and says so rather than guessing."}},"required":["event","links","derivedState"],"additionalProperties":false},"LocalisedCalendarEntry":{"type":"object","description":"The same entry with the caller's local projection attached. The projection is ADDITIVE: `event.scheduledDate` and `event.scheduledAt` keep their canonical values.","properties":{"event":{"$ref":"#/components/schemas/EconomicEvent"},"links":{"$ref":"#/components/schemas/EventLinks"},"derivedState":{"type":"string","enum":["UPCOMING","IMMINENT","PAST_DUE_AWAITING_CONFIRMATION","CONFIRMED_RELEASED","CLOSED"],"description":"As on `CalendarEntry`."},"localDate":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"The calendar date the event falls on in `localTimeZone`. NULL when `event.scheduledAt` is null, because an event with no instant has no local date either."},"localTime":{"type":["string","null"],"description":"Local time as `HH:mm`, 24-hour. NULL when `event.scheduledAt` is null. No time is manufactured from a midnight nobody published — doing so would move a DATE_ONLY release to the previous day for every caller west of the source."},"localTimeZone":{"type":"string","description":"The zone the projection was computed in. It comes from `tz`, NEVER from `timezone` and never from the caller's IP or headers."}},"required":["event","links","derivedState","localDate","localTime","localTimeZone"],"additionalProperties":false},"EconomicsEventsEntry":{"description":"Which of the two shapes arrives is decided by whether the request carried `tz` or `timezone` AT ALL. Sending either adds `localDate`, `localTime` and `localTimeZone`; sending neither omits all three. There is no discriminating field, so a consumer that needs the local projection should always send `tz` and read the localised shape unconditionally.","oneOf":[{"$ref":"#/components/schemas/CalendarEntry"},{"$ref":"#/components/schemas/LocalisedCalendarEntry"}]},"EconomicsEventsData":{"type":"object","properties":{"timeZone":{"type":"string","description":"The zone the local projection was computed in. Echoes `tz`, defaulting to `UTC`. Present whether or not the entries are localised."},"entries":{"type":"array","items":{"$ref":"#/components/schemas/EconomicsEventsEntry"},"description":"Events ordered by `scheduledDate` ascending, truncated to `limit`."}},"required":["timeZone","entries"],"additionalProperties":false},"EconomicsIngestionMeta":{"type":"object","description":"Ingestion freshness, spread flat into meta. These four fields are what let a consumer tell a genuinely empty calendar from a stalled pipeline.","properties":{"nextCursor":{"type":["string","null"],"description":"Pass this back as `cursor` for the next page. Null means this page was the last one. OPAQUE: it encodes an offset into the FILTERED result set, so it is valid only alongside the identical filter arguments, and the corpus underneath it can be rebuilt by an ingestion run between two pages. It is not a snapshot."},"lastIngestedAt":{"type":["string","null"],"format":"date-time","description":"When the materialised index behind this answer was last rebuilt. NOT the age of the underlying releases."},"lastSuccessfulIngestionAt":{"type":["string","null"],"format":"date-time","description":"When the ingestion job behind this domain last succeeded. Null means it has never succeeded on this deployment."},"consecutiveIngestionFailures":{"type":"integer","minimum":0,"description":"Failures since the last success. A non-zero value with a recent `lastIngestedAt` means correct data from a pipeline that is currently broken — two different incidents, and this is how they are told apart."}},"required":["nextCursor","lastIngestedAt","lastSuccessfulIngestionAt","consecutiveIngestionFailures"],"additionalProperties":false},"EconomicsEventsMeta":{"type":"object","description":"The envelope meta for this route. Everything after `attributionRecords` is this route's own addition, spread flat into meta rather than nested under an `extra` key.","properties":{"requestId":{"type":"string","description":"Correlates with the `X-Request-Id` response header and with the server log line for this request."},"generatedAt":{"type":"string","format":"date-time","description":"When this response was assembled. Not the age of the data — read `cacheAgeSeconds` for that."},"cached":{"type":"boolean","description":"True when the payload behind this response came from the in-process cache rather than being assembled for this request."},"cacheAgeSeconds":{"type":"number","minimum":0,"description":"Age of the cached payload in seconds. Zero when nothing was served from cache."},"stale":{"type":"boolean","description":"True when the payload is past its freshness window and is being served anyway."},"degraded":{"type":"boolean","description":"True when a provider failed and the last good payload is being served in its place. The values are real, and `cacheAgeSeconds` states how old they are."},"sourceCount":{"type":"integer","minimum":0,"description":"Number of entries in `sources`."},"sources":{"type":"array","items":{"type":"string"},"description":"Registered source slugs behind the records ON THIS PAGE, not behind the whole corpus. Derived from the page, so a different page of the same query can carry a different list, and an empty page carries an empty list."},"attribution":{"type":"array","items":{"type":"string"},"description":"Credit lines that MUST be displayed wherever this data appears. Retained as plain strings for v1 compatibility; `attributionRecords` carries the same obligation resolved."},"attributionRecords":{"type":"array","items":{"$ref":"#/components/schemas/AttributionRecord"},"description":"The attribution obligation with its licence name and URL attached, so a consumer never has to infer licensing rules from a provider name."},"nextCursor":{"type":["string","null"],"description":"Pass this back as `cursor` for the next page. Null means this page was the last one. OPAQUE: it encodes an offset into the FILTERED result set, so it is valid only alongside the identical filter arguments, and the corpus underneath it can be rebuilt by an ingestion run between two pages. It is not a snapshot."},"lastIngestedAt":{"type":["string","null"],"format":"date-time","description":"When the materialised calendar index behind this answer was last rebuilt."},"lastSuccessfulIngestionAt":{"type":["string","null"],"format":"date-time","description":"The OLDEST last-success across every calendar source that has ever run here — a calendar assembled from several sources is as stale as its stalest. Null means no source has succeeded, or one that has run never has. Before any source but BLS has run, this is exactly the BLS value."},"consecutiveIngestionFailures":{"type":"integer","minimum":0,"description":"The HIGHEST failure count among the calendar sources that have run here."},"ingestion":{"type":"array","items":{"$ref":"#/components/schemas/CalendarSourceIngestion"},"description":"Per source, so the two fields above can be traced to the source responsible."},"importanceMethodology":{"type":"string","description":"A fixed sentence restating that `importance` is HELPERG editorial methodology rather than a measurement of market impact. Present on every response so the caveat travels with the data instead of living in documentation nobody reads."},"totalMatching":{"type":"integer","minimum":0,"description":"How many records matched BEFORE the page limit was applied. Compare it with the page length to know whether more pages exist; `nextCursor` answers the same question directly."}},"required":["requestId","generatedAt","cached","cacheAgeSeconds","stale","degraded","sourceCount","sources","attribution","attributionRecords","nextCursor","lastIngestedAt","lastSuccessfulIngestionAt","consecutiveIngestionFailures","ingestion","importanceMethodology","totalMatching"],"additionalProperties":false},"EconomicsEventsResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/EconomicsEventsData"},"meta":{"$ref":"#/components/schemas/EconomicsEventsMeta"}},"required":["data","meta"],"additionalProperties":false},"PublicSourceRef":{"type":"object","description":"The registered source behind a public record: its slug, its name, and the most specific official URL known for the record.","properties":{"id":{"type":"string","description":"Registered source slug."},"name":{"type":"string"},"url":{"type":["string","null"],"description":"The official page for this record where the source publishes one, else the source's official site."}},"required":["id","name","url"],"additionalProperties":false},"PublicAttribution":{"type":"object","description":"The credit line the source requires wherever this record is displayed.","properties":{"text":{"type":["string","null"],"description":"Attribution text, exactly as it must be shown."},"required":{"type":"boolean"}},"required":["text","required"],"additionalProperties":false},"PublicLicensedAttribution":{"type":"object","properties":{"text":{"type":["string","null"],"description":"Attribution text, exactly as it must be shown."},"required":{"type":"boolean"},"licenseName":{"type":["string","null"],"description":"Licence name."},"licenseUrl":{"type":["string","null"],"description":"Licence URL."}},"required":["text","required","licenseName","licenseUrl"],"additionalProperties":false},"PublicCalendarEvent":{"type":"object","description":"One scheduled statistical release, as the public sees it. `status` is what the SOURCE says and never changes because time passed; `derivedState` is what the clock supports and is computed per request. A DATE_ONLY release has `scheduledAt` null and no time is manufactured. Nothing here is a forecast, a consensus or a market-impact claim: `importance` is HELPERG editorial methodology and says so.","properties":{"id":{"type":"string","description":"Opaque, stable identifier of the event."},"title":{"type":"string"},"release":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"publisher":{"type":"string"},"officialPageUrl":{"type":["string","null"],"description":"The release family's official page, when the registry records one."}},"required":["slug","name","publisher","officialPageUrl"],"additionalProperties":false},"country":{"type":["string","null"],"description":"ISO 3166-1 alpha-2, or null for a regional aggregate."},"region":{"type":["string","null"],"enum":["EA","EU27_2020","G7","G20","OECD","WORLD",null],"description":"Regional aggregate code, or null for a country."},"currency":{"type":["string","null"],"description":"ISO 4217."},"referencePeriod":{"type":"object","properties":{"kind":{"type":"string","enum":["MONTH","QUARTER","YEAR","WEEK","DAY","RANGE","UNKNOWN"],"description":"Kind of period the release describes. UNKNOWN when the source states none."},"code":{"type":"string"},"startDate":{"type":["string","null"],"description":"First day of the period, YYYY-MM-DD."},"endDate":{"type":["string","null"],"description":"Last day of the period, YYYY-MM-DD."}},"required":["kind","code","startDate","endDate"],"additionalProperties":false},"scheduledDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Source-stated calendar date, always present."},"scheduledAt":{"type":["string","null"],"format":"date-time","description":"The scheduled instant, or null when the source published only a date."},"timePrecision":{"type":"string","enum":["EXACT_TIME","DATE_ONLY","TIME_WINDOW","TBA","UNKNOWN"],"description":"How precise the schedule is."},"sourceTimezone":{"type":["string","null"],"description":"IANA zone the source states its schedule in."},"status":{"type":"string","enum":["SCHEDULED","RESCHEDULED","DELAYED","RELEASED","CANCELLED","POSTPONED","TENTATIVE","UNKNOWN"],"description":"What the SOURCE says. Never inferred from the clock."},"derivedState":{"type":"string","enum":["UPCOMING","IMMINENT","PAST_DUE_AWAITING_CONFIRMATION","CONFIRMED_RELEASED","CLOSED"],"description":"What the clock alone supports. Computed per request, never stored."},"publication":{"type":"object","properties":{"releasedAt":{"type":["string","null"],"format":"date-time","description":"When the source states the release happened. Null until confirmed."},"detectedAt":{"type":["string","null"],"format":"date-time","description":"When HELPERG detected an actual value for it. Null until then."}},"required":["releasedAt","detectedAt"],"additionalProperties":false},"importance":{"type":"object","properties":{"level":{"type":"string","enum":["HIGH","MEDIUM","LOW","UNCLASSIFIED"],"description":"HELPERG editorial importance, not market impact."},"basis":{"type":"string","description":"The methodology the level comes from."}},"required":["level","basis"],"additionalProperties":false},"source":{"$ref":"#/components/schemas/PublicSourceRef"},"attribution":{"$ref":"#/components/schemas/PublicAttribution"},"retrievedAt":{"type":"string","format":"date-time","description":"When HELPERG fetched the schedule this event comes from."},"sourceUpdatedAt":{"type":["string","null"],"format":"date-time","description":"What the source said about when its schedule last changed. Null when it states nothing."},"links":{"type":"object","properties":{"seriesSlugs":{"type":"array","items":{"type":"string"},"description":"Series the release family publishes, from the registry. Empty when none is registered."},"observations":{"type":"array","items":{"type":"string"},"description":"Public observation paths, one per series slug."},"calendar":{"type":"string","description":"Public calendar path filtered to this release family."}},"required":["seriesSlugs","observations","calendar"],"additionalProperties":false},"local":{"type":"object","description":"Present only when `tz` was sent. Null date and time for a DATE_ONLY release: no local instant exists to convert.","properties":{"date":{"type":["string","null"],"description":"Local calendar date, YYYY-MM-DD."},"time":{"type":["string","null"],"description":"Local wall-clock time, HH:MM."},"timeZone":{"type":"string"}},"required":["date","time","timeZone"],"additionalProperties":false}},"required":["id","title","release","country","region","currency","referencePeriod","scheduledDate","scheduledAt","timePrecision","sourceTimezone","status","derivedState","publication","importance","source","attribution","retrievedAt","sourceUpdatedAt","links"],"additionalProperties":false},"PublicCalendarDay":{"type":"object","properties":{"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"The local calendar date when `tz` was sent, else the source-stated date."},"events":{"type":"array","items":{"$ref":"#/components/schemas/PublicCalendarEvent"}}},"required":["date","events"],"additionalProperties":false},"PublicCalendarData":{"type":"object","properties":{"timeZone":{"type":"string","description":"Echoes `tz`, defaulting to `UTC`."},"window":{"type":"object","description":"The window that was actually queried, after defaults were applied.","properties":{"from":{"type":"string","format":"date-time","description":"Inclusive start."},"to":{"type":"string","format":"date-time","description":"Inclusive end."},"maxDays":{"type":"integer","description":"The ceiling a window may not exceed."}},"required":["from","to","maxDays"],"additionalProperties":false},"days":{"type":"array","items":{"$ref":"#/components/schemas/PublicCalendarDay"}}},"required":["timeZone","window","days"],"additionalProperties":false},"PublicCalendarMeta":{"type":"object","properties":{"requestId":{"type":"string","description":"Correlates with the `X-Request-Id` response header and with the server log line for this request."},"generatedAt":{"type":"string","format":"date-time","description":"When this response was assembled. Not the age of the data — read `cacheAgeSeconds` for that."},"cached":{"type":"boolean","description":"True when the payload behind this response came from the in-process cache rather than being assembled for this request."},"cacheAgeSeconds":{"type":"number","minimum":0,"description":"Age of the cached payload in seconds. Zero when nothing was served from cache."},"stale":{"type":"boolean","description":"True when the payload is past its freshness window and is being served anyway."},"degraded":{"type":"boolean","description":"True when a provider failed and the last good payload is being served in its place. The values are real, and `cacheAgeSeconds` states how old they are."},"sourceCount":{"type":"integer","minimum":0,"description":"Number of entries in `sources`."},"sources":{"type":"array","items":{"type":"string"},"description":"Registered source slugs behind the records ON THIS PAGE, not behind the whole corpus. Derived from the page, so a different page of the same query can carry a different list, and an empty page carries an empty list."},"attribution":{"type":"array","items":{"type":"string"},"description":"Credit lines that MUST be displayed wherever this data appears. Retained as plain strings for v1 compatibility; `attributionRecords` carries the same obligation resolved."},"attributionRecords":{"type":"array","items":{"$ref":"#/components/schemas/AttributionRecord"},"description":"The attribution obligation with its licence name and URL attached, so a consumer never has to infer licensing rules from a provider name."},"nextCursor":{"type":["string","null"],"description":"Pass this back as `cursor` for the next page. Null means this page was the last one. OPAQUE: it encodes an offset into the FILTERED result set, so it is valid only alongside the identical filter arguments, and the corpus underneath it can be rebuilt by an ingestion run between two pages. It is not a snapshot."},"truncated":{"type":"boolean","description":"True when the page was cut short by the response size ceiling. `nextCursor` is then null; narrow the window or lower `limit`."},"dataUpdatedAt":{"type":["string","null"],"format":"date-time","description":"When the corpus behind this answer was last rebuilt."},"dataStatus":{"type":"string","enum":["fresh","stale","unavailable"],"description":"fresh: the newest successful ingestion is within the freshness window. stale: it is older. unavailable: none has completed here."},"statusSemantics":{"type":"string"},"dateOnlyNote":{"type":"string"},"totalMatching":{"type":"integer","minimum":0,"description":"How many records matched BEFORE the page limit was applied. Compare it with the page length to know whether more pages exist; `nextCursor` answers the same question directly."}},"required":["requestId","generatedAt","cached","cacheAgeSeconds","stale","degraded","sourceCount","sources","attribution","attributionRecords","nextCursor","truncated","dataUpdatedAt","dataStatus","statusSemantics","dateOnlyNote","totalMatching"],"additionalProperties":false},"PublicCalendarResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PublicCalendarData"},"meta":{"$ref":"#/components/schemas/PublicCalendarMeta"}},"required":["data","meta"],"additionalProperties":false},"PublicSeriesHeader":{"type":"object","description":"The series a value belongs to, as registered.","properties":{"slug":{"type":"string"},"name":{"type":"string"},"family":{"type":"string","description":"Indicator family slug."},"country":{"type":["string","null"],"description":"ISO 3166-1 alpha-2."},"region":{"type":["string","null"],"enum":["EA","EU27_2020","G7","G20","OECD","WORLD",null],"description":"Regional aggregate code."},"frequency":{"type":"string"},"unit":{"type":"object","properties":{"code":{"type":"string"},"scale":{"type":"integer"}},"required":["code","scale"],"additionalProperties":false},"seasonalAdjustment":{"type":"string"},"transformation":{"type":"string","description":"The series' registered transformation (level, period change, and so on) — a registry dimension, not a processing step."}},"required":["slug","name","family","country","region","frequency","unit","seasonalAdjustment","transformation"],"additionalProperties":false},"PublicRelease":{"type":"object","description":"One published vintage of one series: an actual value from a verified ingested observation, with its revision facts, its publication and detection times, and where it came from.","properties":{"series":{"$ref":"#/components/schemas/PublicSeriesHeader"},"referencePeriod":{"type":"object","properties":{"code":{"type":"string"}},"required":["code"],"additionalProperties":false},"value":{"type":["string","null"],"pattern":"^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$","description":"A DECIMAL STRING exactly as published; null is a period the source scheduled and has not yet filled."},"publicationStatus":{"type":"string"},"publishedAt":{"type":["string","null"],"format":"date-time","description":"When the source states it published this vintage."},"sourceUpdatedAt":{"type":["string","null"],"format":"date-time","description":"What the source said about its own last change."},"ingestedAt":{"type":"string","format":"date-time","description":"When HELPERG detected it: the series index build time."},"revision":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"description":"Vintages beyond the first."},"isRevision":{"type":"boolean"},"previousValue":{"type":["string","null"],"pattern":"^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$"}},"required":["count","isRevision","previousValue"],"additionalProperties":false},"source":{"$ref":"#/components/schemas/PublicSourceRef"},"attribution":{"$ref":"#/components/schemas/PublicAttribution"},"links":{"type":"object","properties":{"observations":{"type":"string","description":"Public observations path for the series."},"calendar":{"type":["string","null"],"description":"Public calendar path filtered to the release families that publish this series, or null when none is registered."},"releaseFamilies":{"type":"array","items":{"type":"string"}}},"required":["observations","calendar","releaseFamilies"],"additionalProperties":false}},"required":["series","referencePeriod","value","publicationStatus","publishedAt","sourceUpdatedAt","ingestedAt","revision","source","attribution","links"],"additionalProperties":false},"PublicReleasesData":{"type":"object","properties":{"releases":{"type":"array","items":{"$ref":"#/components/schemas/PublicRelease"}}},"required":["releases"],"additionalProperties":false},"PublicReleasesMeta":{"type":"object","properties":{"requestId":{"type":"string","description":"Correlates with the `X-Request-Id` response header and with the server log line for this request."},"generatedAt":{"type":"string","format":"date-time","description":"When this response was assembled. Not the age of the data — read `cacheAgeSeconds` for that."},"cached":{"type":"boolean","description":"True when the payload behind this response came from the in-process cache rather than being assembled for this request."},"cacheAgeSeconds":{"type":"number","minimum":0,"description":"Age of the cached payload in seconds. Zero when nothing was served from cache."},"stale":{"type":"boolean","description":"True when the payload is past its freshness window and is being served anyway."},"degraded":{"type":"boolean","description":"True when a provider failed and the last good payload is being served in its place. The values are real, and `cacheAgeSeconds` states how old they are."},"sourceCount":{"type":"integer","minimum":0,"description":"Number of entries in `sources`."},"sources":{"type":"array","items":{"type":"string"},"description":"Registered source slugs behind the records ON THIS PAGE, not behind the whole corpus. Derived from the page, so a different page of the same query can carry a different list, and an empty page carries an empty list."},"attribution":{"type":"array","items":{"type":"string"},"description":"Credit lines that MUST be displayed wherever this data appears. Retained as plain strings for v1 compatibility; `attributionRecords` carries the same obligation resolved."},"attributionRecords":{"type":"array","items":{"$ref":"#/components/schemas/AttributionRecord"},"description":"The attribution obligation with its licence name and URL attached, so a consumer never has to infer licensing rules from a provider name."},"nextCursor":{"type":["string","null"],"description":"Pass this back as `cursor` for the next page. Null means this page was the last one. OPAQUE: it encodes an offset into the FILTERED result set, so it is valid only alongside the identical filter arguments, and the corpus underneath it can be rebuilt by an ingestion run between two pages. It is not a snapshot."},"truncated":{"type":"boolean"},"perSeries":{"type":"integer","minimum":1,"description":"How many newest vintages per series were merged into this listing."},"dataUpdatedAt":{"type":["string","null"],"format":"date-time","description":"The newest series index build time among the series listed."},"dataStatus":{"type":"string","enum":["fresh","stale","unavailable"],"description":"Derived from `dataUpdatedAt` against the economics freshness window."},"unavailableSeries":{"type":"array","items":{"type":"string"},"description":"Registered series whose observations have never been ingested on this deployment. Listed rather than silently omitted."},"valueNote":{"type":"string"},"revisionNote":{"type":"string"},"totalMatching":{"type":"integer","minimum":0,"description":"How many records matched BEFORE the page limit was applied. Compare it with the page length to know whether more pages exist; `nextCursor` answers the same question directly."}},"required":["requestId","generatedAt","cached","cacheAgeSeconds","stale","degraded","sourceCount","sources","attribution","attributionRecords","nextCursor","truncated","perSeries","dataUpdatedAt","dataStatus","unavailableSeries","valueNote","revisionNote","totalMatching"],"additionalProperties":false},"PublicReleasesResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PublicReleasesData"},"meta":{"$ref":"#/components/schemas/PublicReleasesMeta"}},"required":["data","meta"],"additionalProperties":false},"PublicObservation":{"type":"object","properties":{"referencePeriod":{"type":"string"},"value":{"type":["string","null"],"pattern":"^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$"},"unitCode":{"type":"string"},"scale":{"type":"integer"},"publicationStatus":{"type":"string"},"publishedAt":{"type":["string","null"],"format":"date-time","description":"When the source states it published this vintage."},"sourceUpdatedAt":{"type":["string","null"],"format":"date-time","description":"What the source said about its own last change."},"revisionCount":{"type":"integer","minimum":0},"isRevision":{"type":"boolean"},"previousValue":{"type":["string","null"],"pattern":"^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$"}},"required":["referencePeriod","value","unitCode","scale","publicationStatus","publishedAt","sourceUpdatedAt","revisionCount","isRevision","previousValue"],"additionalProperties":false},"PublicObservationsData":{"type":"object","properties":{"series":{"$ref":"#/components/schemas/PublicSeriesHeader"},"observations":{"type":"array","items":{"$ref":"#/components/schemas/PublicObservation"}}},"required":["series","observations"],"additionalProperties":false},"PublicObservationsMeta":{"type":"object","properties":{"requestId":{"type":"string","description":"Correlates with the `X-Request-Id` response header and with the server log line for this request."},"generatedAt":{"type":"string","format":"date-time","description":"When this response was assembled. Not the age of the data — read `cacheAgeSeconds` for that."},"cached":{"type":"boolean","description":"True when the payload behind this response came from the in-process cache rather than being assembled for this request."},"cacheAgeSeconds":{"type":"number","minimum":0,"description":"Age of the cached payload in seconds. Zero when nothing was served from cache."},"stale":{"type":"boolean","description":"True when the payload is past its freshness window and is being served anyway."},"degraded":{"type":"boolean","description":"True when a provider failed and the last good payload is being served in its place. The values are real, and `cacheAgeSeconds` states how old they are."},"sourceCount":{"type":"integer","minimum":0,"description":"Number of entries in `sources`."},"sources":{"type":"array","items":{"type":"string"},"description":"Registered source slugs behind the records ON THIS PAGE, not behind the whole corpus. Derived from the page, so a different page of the same query can carry a different list, and an empty page carries an empty list."},"attribution":{"type":"array","items":{"type":"string"},"description":"Credit lines that MUST be displayed wherever this data appears. Retained as plain strings for v1 compatibility; `attributionRecords` carries the same obligation resolved."},"attributionRecords":{"type":"array","items":{"$ref":"#/components/schemas/AttributionRecord"},"description":"The attribution obligation with its licence name and URL attached, so a consumer never has to infer licensing rules from a provider name."},"nextCursor":{"type":["string","null"],"description":"Pass this back as `cursor` for the next page. Null means this page was the last one. OPAQUE: it encodes an offset into the FILTERED result set, so it is valid only alongside the identical filter arguments, and the corpus underneath it can be rebuilt by an ingestion run between two pages. It is not a snapshot."},"truncated":{"type":"boolean"},"dataUpdatedAt":{"type":["string","null"],"format":"date-time","description":"When the series index behind this answer was last rebuilt."},"dataStatus":{"type":"string","enum":["fresh","stale","unavailable"],"description":"Derived from `dataUpdatedAt` against the economics freshness window."},"valueNote":{"type":"string"},"revisionNote":{"type":"string"},"totalMatching":{"type":"integer","minimum":0,"description":"How many records matched BEFORE the page limit was applied. Compare it with the page length to know whether more pages exist; `nextCursor` answers the same question directly."}},"required":["requestId","generatedAt","cached","cacheAgeSeconds","stale","degraded","sourceCount","sources","attribution","attributionRecords","nextCursor","truncated","dataUpdatedAt","dataStatus","valueNote","revisionNote","totalMatching"],"additionalProperties":false},"PublicObservationsResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PublicObservationsData"},"meta":{"$ref":"#/components/schemas/PublicObservationsMeta"}},"required":["data","meta"],"additionalProperties":false},"PublicWeatherCurrent":{"type":"object","description":"Current conditions at the served point. Measurement fields are present in the requested unit system only. The provider's raw symbol and the fetch provenance are not part of the public shape.","properties":{"location":{"$ref":"#/components/schemas/GeoPoint"},"observedAt":{"type":"string","format":"date-time","description":"The instant these values describe, per the provider."},"units":{"type":"string","enum":["metric","imperial"],"description":"Which unit system the measurement fields are in."},"temperatureC":{"type":["number","null"],"description":"Air temperature in degrees Celsius (metric only)."},"feelsLikeC":{"type":["number","null"],"description":"Apparent temperature in degrees Celsius (metric only)."},"dewPointC":{"type":["number","null"],"description":"Dew point in degrees Celsius (metric only)."},"temperatureF":{"type":["number","null"],"description":"Air temperature in degrees Fahrenheit (imperial only)."},"feelsLikeF":{"type":["number","null"],"description":"Apparent temperature in degrees Fahrenheit (imperial only)."},"dewPointF":{"type":["number","null"],"description":"Dew point in degrees Fahrenheit (imperial only)."},"humidityPercent":{"type":["number","null"],"description":"Relative humidity, 0 to 100."},"pressureHpa":{"type":["number","null"],"description":"Air pressure at sea level in hectopascals."},"windSpeedMps":{"type":["number","null"],"description":"Wind speed in metres per second (metric only)."},"windGustMps":{"type":["number","null"],"description":"Wind gust speed in metres per second (metric only)."},"windSpeedMph":{"type":["number","null"],"description":"Wind speed in miles per hour (imperial only)."},"windGustMph":{"type":["number","null"],"description":"Wind gust speed in miles per hour (imperial only)."},"windDirectionDegrees":{"type":["number","null"],"description":"Direction the wind comes FROM, in degrees."},"precipitationMm":{"type":["number","null"],"description":"Precipitation over the hour beginning at `observedAt`, in millimetres (metric only)."},"precipitationIn":{"type":["number","null"],"description":"Precipitation in inches (imperial only)."},"cloudCoverPercent":{"type":["number","null"],"description":"Cloud cover, 0 to 100."},"visibilityMetres":{"type":["number","null"],"description":"Always null for MET Norway sourced records."},"uvIndex":{"type":["number","null"],"description":"Clear-sky ultraviolet index."},"condition":{"type":"object","properties":{"code":{"type":"string","enum":["CLEAR","FAIR","PARTLY_CLOUDY","CLOUDY","FOG","LIGHT_RAIN","RAIN","HEAVY_RAIN","LIGHT_RAIN_SHOWERS","RAIN_SHOWERS","HEAVY_RAIN_SHOWERS","LIGHT_SLEET","SLEET","HEAVY_SLEET","LIGHT_SLEET_SHOWERS","SLEET_SHOWERS","HEAVY_SLEET_SHOWERS","LIGHT_SNOW","SNOW","HEAVY_SNOW","LIGHT_SNOW_SHOWERS","SNOW_SHOWERS","HEAVY_SNOW_SHOWERS","UNKNOWN"],"description":"HELPERG condition vocabulary."},"thunder":{"type":"boolean"},"dayPart":{"type":["string","null"],"enum":["DAY","NIGHT","POLAR_TWILIGHT",null],"description":"Null when the provider does not distinguish day from night for this code."}},"required":["code","thunder","dayPart"],"additionalProperties":false},"source":{"$ref":"#/components/schemas/PublicSourceRef"},"attribution":{"$ref":"#/components/schemas/PublicLicensedAttribution"},"providerModelUpdatedAt":{"type":["string","null"],"format":"date-time","description":"When the provider says the underlying model output was produced."}},"required":["location","observedAt","units","humidityPercent","pressureHpa","windDirectionDegrees","cloudCoverPercent","visibilityMetres","uvIndex","condition","source","attribution","providerModelUpdatedAt"],"additionalProperties":false},"PublicWeatherCurrentData":{"type":"object","properties":{"current":{"$ref":"#/components/schemas/PublicWeatherCurrent"}},"required":["current"],"additionalProperties":false},"PublicWeatherCurrentMeta":{"type":"object","properties":{"requestId":{"type":"string","description":"Correlates with the `X-Request-Id` response header and with the server log line for this request."},"generatedAt":{"type":"string","format":"date-time","description":"When this response was assembled. Not the age of the data — read `cacheAgeSeconds` for that."},"cached":{"type":"boolean","description":"True when the payload behind this response came from the in-process cache rather than being assembled for this request."},"cacheAgeSeconds":{"type":"number","minimum":0,"description":"Age of the cached payload in seconds. Zero when nothing was served from cache."},"stale":{"type":"boolean","description":"True when the payload is past its freshness window and is being served anyway."},"degraded":{"type":"boolean","description":"True when a provider failed and the last good payload is being served in its place. The values are real, and `cacheAgeSeconds` states how old they are."},"sourceCount":{"type":"integer","minimum":0,"description":"Number of entries in `sources`."},"sources":{"type":"array","items":{"type":"string"},"description":"Registered source slugs behind this payload. Always `[\"met-no\"]` for the weather surface today."},"attribution":{"type":"array","items":{"type":"string"},"description":"Credit lines that MUST be displayed wherever this data appears. Retained as plain strings for v1 compatibility; `attributionRecords` carries the same obligation resolved."},"attributionRecords":{"type":"array","items":{"$ref":"#/components/schemas/AttributionRecord"},"description":"The attribution obligation with its licence name and URL attached, so a consumer never has to infer licensing rules from a provider name."},"observedAt":{"type":"string","format":"date-time","description":"Repeated from the payload."},"fetchedAt":{"type":"string","format":"date-time","description":"When HELPERG last retrieved the provider payload behind this response."},"coordinatePrecision":{"type":"string","description":"A fixed sentence stating the rounding applied to `lat` and `lon` before the point was served."},"canonicalUnits":{"type":"object","properties":{"temperature":{"const":"celsius"},"windSpeed":{"const":"m/s"},"pressure":{"const":"hPa"},"precipitation":{"const":"mm"}},"required":["temperature","windSpeed","pressure","precipitation"],"additionalProperties":false}},"required":["requestId","generatedAt","cached","cacheAgeSeconds","stale","degraded","sourceCount","sources","attribution","attributionRecords","observedAt","fetchedAt","coordinatePrecision","canonicalUnits"],"additionalProperties":false},"NewsEntity":{"type":"object","properties":{"id":{"type":["string","null"],"description":"Stable HELPERG entity identifier where one exists."},"name":{"type":"string"},"type":{"type":"string","enum":["ORGANISATION","PERSON","PLACE","PRODUCT","GOVERNMENT_BODY","OTHER"],"description":"Entity kind."}},"required":["id","name","type"],"additionalProperties":false},"NewsEvent":{"type":"object","description":"One real-world development in the P1 news model, referencing its source items. For official economic news every publication is exactly one event; official publications are never merged. `canonicalTitle` starts as the publisher's headline and is never presented as HELPERG's words about the publication.","properties":{"id":{"type":"string"},"eventType":{"type":"string","enum":["CORPORATE_ANNOUNCEMENT","REGULATORY_ACTION","PUBLICATION","APPOINTMENT","INCIDENT","MARKET_EVENT","POLICY_DECISION","RESEARCH_RELEASE","OTHER"],"description":"HELPERG event kind."},"canonicalTitle":{"type":"string"},"summary":{"type":["string","null"],"description":"HELPERG-authored summary. Null until one exists; never generated automatically."},"occurredAt":{"type":["string","null"],"format":"date-time","description":"When the development happened, where established."},"detectedAt":{"type":"string","format":"date-time","description":"When HELPERG first saw evidence of it."},"status":{"type":"string","enum":["DETECTED","ENRICHED","PUBLISHED_INTERNALLY","SUPERSEDED","DISMISSED"],"description":"Editorial status."},"confidenceState":{"type":"string","enum":["PRIMARY_SOURCE_ONLY","SINGLE_SOURCE_REPORT","MULTI_SOURCE_CONFIRMED","PRIMARY_AND_INDEPENDENT_CONFIRMATION","UNVERIFIED_CLAIM","CORRECTED","RETRACTED"],"description":"A statement about SOURCES, never a probability of truth."},"topics":{"type":"array","items":{"type":"string"}},"entities":{"type":"array","items":{"$ref":"#/components/schemas/NewsEntity"}},"geography":{"type":"array","items":{"type":"string"},"description":"ISO 3166-1 alpha-2 codes or HELPERG region identifiers."},"sourceItemIds":{"type":"array","items":{"type":"string"}},"primarySourceItemId":{"type":["string","null"],"description":"The originating authority's item, where one exists."},"confirmationCount":{"type":"integer","minimum":0,"description":"Count of INDEPENDENT sources, not of documents."},"createdAt":{"type":"string","format":"date-time","description":"Record creation."},"updatedAt":{"type":"string","format":"date-time","description":"Record update."}},"required":["id","eventType","canonicalTitle","summary","occurredAt","detectedAt","status","confidenceState","topics","entities","geography","sourceItemIds","primarySourceItemId","confirmationCount","createdAt","updatedAt"],"additionalProperties":false},"OfficialNewsLinkEvidence":{"type":"object","description":"Why a link exists, in a form a human can check. `targetId` is null when the evidence explains why nothing was chosen (ambiguity, no observations on this deployment).","properties":{"kind":{"type":"string","enum":["UPSTREAM_IDENTIFIER","SOURCE_URL","STRUCTURED_SERIES_CODE","CURATED_FAMILY_MAPPING","SAME_SOURCE_FAMILY_PERIOD","TITLE_CLASSIFICATION"],"description":"Linking rule, in the contract's priority order."},"target":{"type":"string","enum":["CALENDAR_EVENT","SERIES","RELEASE","OBSERVATION"],"description":"What the evidence is about."},"targetId":{"type":["string","null"],"description":"The linked object's id or slug."},"explanation":{"type":"string"},"matched":{"type":"object","additionalProperties":{"type":"string"},"description":"The exact facts that matched."}},"required":["kind","target","targetId","explanation","matched"],"additionalProperties":false},"OfficialNewsLinks":{"type":"object","properties":{"calendarEventIds":{"type":"array","items":{"type":"string"}},"seriesSlugs":{"type":"array","items":{"type":"string"}},"releaseSlugs":{"type":"array","items":{"type":"string"}},"observationReferences":{"type":"array","items":{"type":"object","properties":{"seriesSlug":{"type":"string"},"periodCode":{"type":"string"}},"required":["seriesSlug","periodCode"],"additionalProperties":false}},"status":{"type":"string","enum":["LINKED","PARTIALLY_LINKED","UNLINKED","AMBIGUOUS"],"description":"LINKED when every implied target resolved; PARTIALLY_LINKED when some did; UNLINKED when no family or candidate; AMBIGUOUS when more than one calendar event matched and none was chosen."},"evidence":{"type":"array","items":{"$ref":"#/components/schemas/OfficialNewsLinkEvidence"}}},"required":["calendarEventIds","seriesSlugs","releaseSlugs","observationReferences","status","evidence"],"additionalProperties":false},"OfficialEconomicNewsEvent":{"type":"object","description":"One official publication of the BLS, Eurostat or the ECB, as stored: the publisher's title, identifier, canonical link and instants; the permitted excerpt or null; HELPERG's classification; provenance; deterministic links with evidence; and version lifecycle. Consumer surface only — the public feed serves an allow-listed projection.","properties":{"id":{"type":"string","description":"Shared with the NewsSourceItem the publication is registered as."},"sourceId":{"type":"string"},"upstreamId":{"type":"string","description":"The publisher's own identifier, verbatim."},"providerKey":{"type":"string","description":"The identity anchor the adapter chose for this source."},"canonicalUrl":{"type":"string","format":"uri"},"title":{"type":"string","description":"Verbatim official title."},"excerpt":{"type":["string","null"],"description":"Source-provided short summary where reuse is permitted; null for the ECB."},"language":{"type":"string"},"publicationType":{"type":"string","enum":["NEWS_RELEASE","STATISTICAL_RELEASE","PRESS_RELEASE","MONETARY_POLICY_DECISION","MONETARY_POLICY_STATEMENT","MONETARY_POLICY_ACCOUNT"],"description":"HELPERG classification."},"releaseFamily":{"type":["string","null"],"description":"Release-definition slug, or null when unclassified."},"jurisdictions":{"type":"array","items":{"type":"string"}},"countries":{"type":"array","items":{"type":"string"}},"currencies":{"type":"array","items":{"type":"string"}},"topics":{"type":"array","items":{"type":"string","enum":["inflation","prices","labour-market","employment","wages","gdp","national-accounts","monetary-policy","interest-rates","money-and-credit","external-sector","trade","government-finance","housing","production","retail","services","productivity","payments","financial-accounts"],"description":"HELPERG topic vocabulary."}},"referencePeriod":{"type":["object","null"],"properties":{"code":{"type":"string"},"kind":{"type":"string"},"matchedText":{"type":"string"}},"required":["code","kind","matchedText"],"additionalProperties":false},"publishedAt":{"type":["string","null"],"format":"date-time","description":"The publisher's publication instant."},"updatedAt":{"type":["string","null"],"format":"date-time","description":"Only when the publisher states one."},"retrievedAt":{"type":"string","format":"date-time","description":"When HELPERG read it."},"sourceLastModified":{"type":["string","null"],"description":"The feed's HTTP Last-Modified, when sent."},"state":{"type":"string","enum":["PUBLISHED","UPDATED","CORRECTED","WITHDRAWN","UNKNOWN"],"description":"PUBLISHED, UPDATED on a content change, UNKNOWN without a publication instant; CORRECTED and WITHDRAWN only from an explicit source signal, which no v1 feed carries."},"attribution":{"type":"object","properties":{"text":{"type":["string","null"],"description":"Credit line."},"required":{"type":"boolean"}},"required":["text","required"],"additionalProperties":false},"rightsUrl":{"type":"string","format":"uri"},"contentFingerprint":{"type":"string","description":"Change detection only. Never identity."},"provenance":{"$ref":"#/components/schemas/Provenance"},"links":{"$ref":"#/components/schemas/OfficialNewsLinks"},"firstSeenAt":{"type":"string","format":"date-time","description":"First ingestion."},"lastSeenAt":{"type":"string","format":"date-time","description":"Last time the feed still listed it. Absence from a rolling feed is not a withdrawal."},"version":{"type":"integer","minimum":1},"recordUpdatedAt":{"type":"string","format":"date-time","description":"When the record last changed."}},"required":["id","sourceId","upstreamId","providerKey","canonicalUrl","title","excerpt","language","publicationType","releaseFamily","jurisdictions","countries","currencies","topics","referencePeriod","publishedAt","updatedAt","retrievedAt","sourceLastModified","state","attribution","rightsUrl","contentFingerprint","provenance","links","firstSeenAt","lastSeenAt","version","recordUpdatedAt"],"additionalProperties":false},"NewsEventsData":{"type":"object","properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/NewsEvent"}},"officialPublications":{"type":"array","items":{"$ref":"#/components/schemas/OfficialEconomicNewsEvent"},"description":"Official economic news records matching the window and the P2F filters, newest first."}},"required":["events","officialPublications"],"additionalProperties":false},"NewsEventsMeta":{"type":"object","properties":{"requestId":{"type":"string","description":"Correlates with the `X-Request-Id` response header and with the server log line for this request."},"generatedAt":{"type":"string","format":"date-time","description":"When this response was assembled. Not the age of the data — read `cacheAgeSeconds` for that."},"cached":{"type":"boolean","description":"True when the payload behind this response came from the in-process cache rather than being assembled for this request."},"cacheAgeSeconds":{"type":"number","minimum":0,"description":"Age of the cached payload in seconds. Zero when nothing was served from cache."},"stale":{"type":"boolean","description":"True when the payload is past its freshness window and is being served anyway."},"degraded":{"type":"boolean","description":"True when a provider failed and the last good payload is being served in its place. The values are real, and `cacheAgeSeconds` states how old they are."},"sourceCount":{"type":"integer","minimum":0,"description":"Number of entries in `sources`."},"sources":{"type":"array","items":{"type":"string"},"description":"Registered source slugs behind the official publications ON THIS PAGE. Empty when the feed is unpublished on this deployment."},"attribution":{"type":"array","items":{"type":"string"},"description":"Credit lines that MUST be displayed wherever this data appears. Retained as plain strings for v1 compatibility; `attributionRecords` carries the same obligation resolved."},"attributionRecords":{"type":"array","items":{"$ref":"#/components/schemas/AttributionRecord"},"description":"The attribution obligation with its licence name and URL attached, so a consumer never has to infer licensing rules from a provider name."},"totalMatching":{"type":"integer","minimum":0,"description":"P1 events matching before paging."},"sourceItemCount":{"type":"integer","minimum":0},"confidenceSemantics":{"type":"string"},"officialPublicationCount":{"type":"integer","minimum":0},"officialTotalMatching":{"type":"integer","minimum":0},"officialFeedStatus":{"type":"string","enum":["published","unpublished"],"description":"Whether an official-news feed snapshot exists on this deployment."},"officialFeedUpdatedAt":{"type":["string","null"],"format":"date-time","description":"When the served snapshot was built."},"officialLastSuccessAt":{"type":["string","null"],"format":"date-time","description":"Oldest last success among the news jobs that have succeeded."}},"required":["requestId","generatedAt","cached","cacheAgeSeconds","stale","degraded","sourceCount","sources","attribution","attributionRecords","totalMatching","sourceItemCount","confidenceSemantics","officialPublicationCount","officialTotalMatching","officialFeedStatus","officialFeedUpdatedAt","officialLastSuccessAt"],"additionalProperties":false},"NewsEventsResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/NewsEventsData"},"meta":{"$ref":"#/components/schemas/NewsEventsMeta"}},"required":["data","meta"],"additionalProperties":false},"PublicNewsRelatedCalendarEvent":{"type":"object","properties":{"id":{"type":"string","description":"A calendar event id as served by the public calendar."},"releaseFamily":{"type":"string"},"calendar":{"type":"string","description":"Public calendar path filtered to the release family."}},"required":["id","releaseFamily","calendar"],"additionalProperties":false},"PublicNewsRelatedSeries":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"observations":{"type":"string","description":"Public observations path for the series."}},"required":["slug","name","observations"],"additionalProperties":false},"PublicNewsRelatedRelease":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"publisher":{"type":"string"},"calendar":{"type":"string"}},"required":["slug","name","publisher","calendar"],"additionalProperties":false},"PublicNewsRelatedObservation":{"type":"object","properties":{"seriesSlug":{"type":"string"},"periodCode":{"type":"string"},"observations":{"type":"string","description":"Public observations path narrowed to the period."}},"required":["seriesSlug","periodCode","observations"],"additionalProperties":false},"PublicNewsLinkEvidence":{"type":"object","properties":{"kind":{"type":"string","enum":["UPSTREAM_IDENTIFIER","SOURCE_URL","STRUCTURED_SERIES_CODE","CURATED_FAMILY_MAPPING","SAME_SOURCE_FAMILY_PERIOD","TITLE_CLASSIFICATION"],"description":"Linking rule."},"target":{"type":"string","enum":["CALENDAR_EVENT","SERIES","RELEASE","OBSERVATION"],"description":"What the evidence is about."},"targetId":{"type":["string","null"],"description":"The linked object, or null when the evidence explains why nothing was chosen."},"explanation":{"type":"string"}},"required":["kind","target","targetId","explanation"],"additionalProperties":false},"PublicOfficialNewsEvent":{"type":"object","description":"An official publication of its named publisher. HELPERG is not the publisher: `title`, `excerpt`, `publishedAt`, `updatedAt` and `canonicalUrl` are the publisher's; `classification` names the fields that are HELPERG's; `related` links are deterministic and carry their evidence. No forecast, consensus, surprise or sentiment field exists.","properties":{"id":{"type":"string"},"title":{"type":"string","description":"Verbatim official title."},"excerpt":{"type":["string","null"],"description":"Short source-provided summary, only where the publisher's terms permit reuse (BLS, Eurostat). Null for the ECB."},"language":{"type":"string"},"publisher":{"type":"string","description":"The publishing organisation."},"source":{"$ref":"#/components/schemas/PublicSourceRef"},"publicationType":{"type":"string","enum":["NEWS_RELEASE","STATISTICAL_RELEASE","PRESS_RELEASE","MONETARY_POLICY_DECISION","MONETARY_POLICY_STATEMENT","MONETARY_POLICY_ACCOUNT"],"description":"HELPERG classification."},"releaseFamily":{"type":["object","null"],"properties":{"slug":{"type":"string"},"name":{"type":"string"}},"required":["slug","name"],"additionalProperties":false},"jurisdictions":{"type":"array","items":{"type":"string"},"description":"HELPERG region codes or ISO 3166-1 alpha-2."},"countries":{"type":"array","items":{"type":"string"}},"currencies":{"type":"array","items":{"type":"string"}},"topics":{"type":"array","items":{"type":"string","enum":["inflation","prices","labour-market","employment","wages","gdp","national-accounts","monetary-policy","interest-rates","money-and-credit","external-sector","trade","government-finance","housing","production","retail","services","productivity","payments","financial-accounts"],"description":"HELPERG topic vocabulary."}},"referencePeriod":{"type":["object","null"],"properties":{"code":{"type":"string"},"kind":{"type":"string"}},"required":["code","kind"],"additionalProperties":false},"publishedAt":{"type":["string","null"],"format":"date-time","description":"The publisher's publication instant, UTC."},"updatedAt":{"type":["string","null"],"format":"date-time","description":"Only when the publisher states one."},"state":{"type":"string","enum":["PUBLISHED","UPDATED","CORRECTED","WITHDRAWN","UNKNOWN"],"description":"Never inferred from a scheduled time having passed."},"canonicalUrl":{"type":"string","format":"uri","description":"The publisher's own page. The full publication lives there."},"rightsUrl":{"type":"string","format":"uri","description":"The publisher's reuse terms."},"attribution":{"$ref":"#/components/schemas/PublicLicensedAttribution"},"classification":{"type":"object","properties":{"basis":{"type":"string"},"sourceProvided":{"type":"array","items":{"type":"string"}},"helpergClassified":{"type":"array","items":{"type":"string"}}},"required":["basis","sourceProvided","helpergClassified"],"additionalProperties":false},"related":{"type":"object","properties":{"calendarEvents":{"type":"array","items":{"$ref":"#/components/schemas/PublicNewsRelatedCalendarEvent"}},"series":{"type":"array","items":{"$ref":"#/components/schemas/PublicNewsRelatedSeries"}},"releases":{"type":"array","items":{"$ref":"#/components/schemas/PublicNewsRelatedRelease"}},"observations":{"type":"array","items":{"$ref":"#/components/schemas/PublicNewsRelatedObservation"}}},"required":["calendarEvents","series","releases","observations"],"additionalProperties":false},"linkStatus":{"type":"string","enum":["LINKED","PARTIALLY_LINKED","UNLINKED","AMBIGUOUS"],"description":"Whether the links the classification implies resolved."},"linkEvidence":{"type":"array","items":{"$ref":"#/components/schemas/PublicNewsLinkEvidence"}}},"required":["id","title","excerpt","language","publisher","source","publicationType","releaseFamily","jurisdictions","countries","currencies","topics","referencePeriod","publishedAt","updatedAt","state","canonicalUrl","rightsUrl","attribution","classification","related","linkStatus","linkEvidence"],"additionalProperties":false},"PublicNewsEventsData":{"type":"object","properties":{"window":{"type":"object","properties":{"from":{"type":"string","format":"date-time","description":"Inclusive start."},"to":{"type":"string","format":"date-time","description":"Inclusive end."},"maxDays":{"type":"integer"}},"required":["from","to","maxDays"],"additionalProperties":false},"events":{"type":"array","items":{"$ref":"#/components/schemas/PublicOfficialNewsEvent"},"description":"Newest first; ties by source slug then id."}},"required":["window","events"],"additionalProperties":false},"PublicNewsEventsMeta":{"type":"object","properties":{"requestId":{"type":"string","description":"Correlates with the `X-Request-Id` response header and with the server log line for this request."},"generatedAt":{"type":"string","format":"date-time","description":"When this response was assembled. Not the age of the data — read `cacheAgeSeconds` for that."},"cached":{"type":"boolean","description":"True when the payload behind this response came from the in-process cache rather than being assembled for this request."},"cacheAgeSeconds":{"type":"number","minimum":0,"description":"Age of the cached payload in seconds. Zero when nothing was served from cache."},"stale":{"type":"boolean","description":"True when the payload is past its freshness window and is being served anyway."},"degraded":{"type":"boolean","description":"True when a provider failed and the last good payload is being served in its place. The values are real, and `cacheAgeSeconds` states how old they are."},"sourceCount":{"type":"integer","minimum":0,"description":"Number of entries in `sources`."},"sources":{"type":"array","items":{"type":"string"},"description":"Registered source slugs behind the publications ON THIS PAGE."},"attribution":{"type":"array","items":{"type":"string"},"description":"Credit lines that MUST be displayed wherever this data appears. Retained as plain strings for v1 compatibility; `attributionRecords` carries the same obligation resolved."},"attributionRecords":{"type":"array","items":{"$ref":"#/components/schemas/AttributionRecord"},"description":"The attribution obligation with its licence name and URL attached, so a consumer never has to infer licensing rules from a provider name."},"totalMatching":{"type":"integer","minimum":0},"nextCursor":{"type":["string","null"],"description":"Opaque cursor for the next page, or null."},"truncated":{"type":"boolean","description":"True when the page was cut short by the response-size ceiling."},"dataUpdatedAt":{"type":"string","format":"date-time","description":"When the served feed snapshot was built."},"dataStatus":{"type":"string","enum":["fresh","stale","unavailable"],"description":"fresh within three days of a successful ingestion; stale beyond it; unavailable when no news ingestion has succeeded."},"publisherNote":{"type":"string"},"classificationNote":{"type":"string"}},"required":["requestId","generatedAt","cached","cacheAgeSeconds","stale","degraded","sourceCount","sources","attribution","attributionRecords","totalMatching","nextCursor","truncated","dataUpdatedAt","dataStatus","publisherNote","classificationNote"],"additionalProperties":false},"PublicNewsEventsResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PublicNewsEventsData"},"meta":{"$ref":"#/components/schemas/PublicNewsEventsMeta"}},"required":["data","meta"],"additionalProperties":false},"PublicWeatherCurrentResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PublicWeatherCurrentData"},"meta":{"$ref":"#/components/schemas/PublicWeatherCurrentMeta"}},"required":["data","meta"],"additionalProperties":false},"EconomicsCalendarDay":{"type":"object","description":"One local day and the events that fall on it. Grouping is by `localDate` where the event has one, and by `event.scheduledDate` where it does not — which is the only date a DATE_ONLY release has.","properties":{"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"The local calendar date, or the source-stated date for an event with no instant."},"entries":{"type":"array","items":{"$ref":"#/components/schemas/LocalisedCalendarEntry"}}},"required":["date","entries"],"additionalProperties":false},"EconomicsCalendarData":{"type":"object","properties":{"timeZone":{"type":"string","description":"The zone days were computed in. Echoes `tz`, defaulting to `UTC`."},"days":{"type":"array","items":{"$ref":"#/components/schemas/EconomicsCalendarDay"},"description":"Days ascending by date. Days with no matching event are absent rather than present and empty."}},"required":["timeZone","days"],"additionalProperties":false},"EconomicsCalendarMeta":{"type":"object","description":"The envelope meta for this route. Everything after `attributionRecords` is spread flat into meta, not nested.","properties":{"requestId":{"type":"string","description":"Correlates with the `X-Request-Id` response header and with the server log line for this request."},"generatedAt":{"type":"string","format":"date-time","description":"When this response was assembled. Not the age of the data — read `cacheAgeSeconds` for that."},"cached":{"type":"boolean","description":"True when the payload behind this response came from the in-process cache rather than being assembled for this request."},"cacheAgeSeconds":{"type":"number","minimum":0,"description":"Age of the cached payload in seconds. Zero when nothing was served from cache."},"stale":{"type":"boolean","description":"True when the payload is past its freshness window and is being served anyway."},"degraded":{"type":"boolean","description":"True when a provider failed and the last good payload is being served in its place. The values are real, and `cacheAgeSeconds` states how old they are."},"sourceCount":{"type":"integer","minimum":0,"description":"Number of entries in `sources`."},"sources":{"type":"array","items":{"type":"string"},"description":"Registered source slugs behind the records ON THIS PAGE, not behind the whole corpus. Derived from the page, so a different page of the same query can carry a different list, and an empty page carries an empty list."},"attribution":{"type":"array","items":{"type":"string"},"description":"Credit lines that MUST be displayed wherever this data appears. Retained as plain strings for v1 compatibility; `attributionRecords` carries the same obligation resolved."},"attributionRecords":{"type":"array","items":{"$ref":"#/components/schemas/AttributionRecord"},"description":"The attribution obligation with its licence name and URL attached, so a consumer never has to infer licensing rules from a provider name."},"nextCursor":{"type":["string","null"],"description":"Pass this back as `cursor` for the next page. Null means this page was the last one. OPAQUE: it encodes an offset into the FILTERED result set, so it is valid only alongside the identical filter arguments, and the corpus underneath it can be rebuilt by an ingestion run between two pages. It is not a snapshot."},"lastIngestedAt":{"type":["string","null"],"format":"date-time","description":"When the materialised calendar index behind this answer was last rebuilt."},"lastSuccessfulIngestionAt":{"type":["string","null"],"format":"date-time","description":"When the release-schedule ingestion last succeeded. Null means never, on this deployment."},"consecutiveIngestionFailures":{"type":"integer","minimum":0,"description":"The HIGHEST failure count among the calendar sources that have run here."},"ingestion":{"type":"array","items":{"$ref":"#/components/schemas/CalendarSourceIngestion"},"description":"Per source, so the stalest one can be named."},"statusSemantics":{"type":"string","description":"A fixed sentence restating that `status` is what the source says while `derivedState` is what the clock alone supports. The two are deliberately given different vocabularies so they cannot be confused."},"dateOnlyNote":{"type":"string","description":"A fixed sentence restating that a date-only event carries `scheduledAt`, `localDate` and `localTime` null, and that no time is manufactured."},"totalMatching":{"type":"integer","minimum":0,"description":"How many records matched BEFORE the page limit was applied. Compare it with the page length to know whether more pages exist; `nextCursor` answers the same question directly."}},"required":["requestId","generatedAt","cached","cacheAgeSeconds","stale","degraded","sourceCount","sources","attribution","attributionRecords","nextCursor","lastIngestedAt","lastSuccessfulIngestionAt","consecutiveIngestionFailures","ingestion","statusSemantics","dateOnlyNote","totalMatching"],"additionalProperties":false},"EconomicsCalendarResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/EconomicsCalendarData"},"meta":{"$ref":"#/components/schemas/EconomicsCalendarMeta"}},"required":["data","meta"],"additionalProperties":false},"ObservationEntry":{"type":"object","description":"One reference period, carrying the CURRENT value and how many times it has been revised.","properties":{"referencePeriod":{"type":"string","description":"The canonical reference-period code. Sortable within a kind, and the value `from` and `to` are compared against."},"value":{"type":["string","null"],"pattern":"^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$","description":"A DECIMAL STRING — `\"3.1\"`, never `3.1`. Serialising through a JSON number reintroduces the binary floating-point error the storage model exists to avoid, and a consumer comparing two vintages would then see revisions that never happened. Trailing zeros are preserved because `3.10` asserts a precision `3.1` does not. NULL is a real state: a period the source has scheduled and not yet published. It is never zero."},"unitCode":{"type":"string","enum":["PERCENT","PERCENTAGE_POINTS","INDEX_POINTS","COUNT","PERSONS","HOURS","LOCAL_CURRENCY","USD","EUR","CZK","GBP","RATIO","OTHER"],"description":"Unit of this value. Falls back to the series' registered unit when no version has been published yet."},"scale":{"type":"integer","description":"Power of ten already applied to `value`."},"publicationStatus":{"type":"string","enum":["FLASH","PRELIMINARY","PROVISIONAL","NORMAL","REVISED","FINAL","ESTIMATED","UNKNOWN"],"description":"The SOURCE's own classification of the publication stage. `FINAL` is never inferred from age — a ten-year-old national-accounts figure can still be revised. `UNKNOWN` is what a period with no published version reports."},"publishedAt":{"type":["string","null"],"format":"date-time","description":"When the source published this version."},"sourceUpdatedAt":{"type":["string","null"],"format":"date-time","description":"When the source says the underlying dataset last changed."},"revisionCount":{"type":"integer","minimum":0,"description":"Vintages beyond the first. Zero means the figure has never been revised, so a consumer can tell at a glance without fetching the full history."},"isRevision":{"type":"boolean","description":"Whether the CURRENT version replaced a different value, as opposed to being the first publication."},"previousValue":{"type":["string","null"],"pattern":"^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$","description":"The value this version replaced, as a decimal string. Null on a first publication."}},"required":["referencePeriod","value","unitCode","scale","publicationStatus","publishedAt","sourceUpdatedAt","revisionCount","isRevision","previousValue"],"additionalProperties":false},"ObservationVersion":{"type":"object","description":"One vintage. An observation is (series + reference period) and has NO value of its own; the value lives here, and there can be many. That split is why \"what did we believe in October?\" is answerable at all: agencies revise, US GDP is published three times for one quarter, and a model that stored the value on the observation would have to overwrite it.","properties":{"id":{"type":"string","description":"Version identity, derived from the value and publication status so re-seeing the same figure cannot append a phantom revision."},"observationId":{"type":"string","description":"The observation this version belongs to."},"value":{"type":["string","null"],"pattern":"^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$","description":"The published figure as a decimal string, or null for a scheduled-but-unpublished period."},"unitCode":{"type":"string","enum":["PERCENT","PERCENTAGE_POINTS","INDEX_POINTS","COUNT","PERSONS","HOURS","LOCAL_CURRENCY","USD","EUR","CZK","GBP","RATIO","OTHER"],"description":"Unit of this version's value."},"scale":{"type":"integer","description":"Power of ten already applied to `value`."},"publicationStatus":{"type":"string","enum":["FLASH","PRELIMINARY","PROVISIONAL","NORMAL","REVISED","FINAL","ESTIMATED","UNKNOWN"],"description":"The source's classification at the time of this version. A status change alone IS a revision: \"3.1 preliminary\" becoming \"3.1 final\" is a separately meaningful publication event."},"publishedAt":{"type":["string","null"],"format":"date-time","description":"When the source published this version."},"sourceUpdatedAt":{"type":["string","null"],"format":"date-time","description":"When the source says the dataset last changed."},"detectedAt":{"type":"string","format":"date-time","description":"When HELPERG first saw this version."},"revisionSequence":{"type":"integer","minimum":0,"description":"0 is the first publication; each later vintage increments. Versions are appended, never rewritten."},"isRevision":{"type":"boolean","description":"False at sequence 0 by construction."},"previousValue":{"type":["string","null"],"pattern":"^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$","description":"The value this version replaced. Null at sequence 0 by construction."},"provenance":{"$ref":"#/components/schemas/Provenance"},"createdAt":{"type":"string","format":"date-time","description":"When this version record was written."}},"required":["id","observationId","value","unitCode","scale","publicationStatus","publishedAt","sourceUpdatedAt","detectedAt","revisionSequence","isRevision","previousValue","provenance","createdAt"],"additionalProperties":false},"ObservationsSeriesSummary":{"type":"object","description":"Enough of the series definition to read the values without a second request. The full definition is at `/api/v1/economics/series/{slug}`.","properties":{"slug":{"type":"string"},"canonicalName":{"type":"string"},"familySlug":{"type":"string"},"countryCode":{"type":["string","null"],"description":"ISO 3166-1 alpha-2, or null for a multinational aggregate."},"regionCode":{"type":["string","null"],"enum":["EA","EU27_2020","G7","G20","OECD","WORLD",null],"description":"HELPERG region code, or null for a single country."},"frequency":{"type":"string","enum":["DAILY","WEEKLY","MONTHLY","QUARTERLY","ANNUAL","IRREGULAR"],"description":"How often the source publishes."},"transformation":{"type":"string","enum":["NONE","MOM","YOY","QOQ","QOQ_ANNUALIZED","YTD","ROLLING_12_MONTH","OTHER"],"description":"Rate-of-change convention. Read it before comparing this series with any other."},"seasonalAdjustment":{"type":"string","enum":["SEASONALLY_ADJUSTED","NOT_SEASONALLY_ADJUSTED","WORKING_DAY_ADJUSTED","CALENDAR_ADJUSTED","UNKNOWN","NOT_APPLICABLE"],"description":"Adjusted and unadjusted are different statistics."},"unitCode":{"type":"string","enum":["PERCENT","PERCENTAGE_POINTS","INDEX_POINTS","COUNT","PERSONS","HOURS","LOCAL_CURRENCY","USD","EUR","CZK","GBP","RATIO","OTHER"],"description":"Unit of every value in `observations`."},"scale":{"type":"integer","description":"Power of ten already applied to the values."}},"required":["slug","canonicalName","familySlug","countryCode","regionCode","frequency","transformation","seasonalAdjustment","unitCode","scale"],"additionalProperties":false},"EconomicsObservationsData":{"type":"object","properties":{"series":{"$ref":"#/components/schemas/ObservationsSeriesSummary"},"observations":{"type":"array","items":{"$ref":"#/components/schemas/ObservationEntry"},"description":"One entry per reference period, ascending by period code, truncated to `limit`."}},"required":["series","observations"],"additionalProperties":false},"EconomicsObservationsMeta":{"type":"object","properties":{"requestId":{"type":"string","description":"Correlates with the `X-Request-Id` response header and with the server log line for this request."},"generatedAt":{"type":"string","format":"date-time","description":"When this response was assembled. Not the age of the data — read `cacheAgeSeconds` for that."},"cached":{"type":"boolean","description":"True when the payload behind this response came from the in-process cache rather than being assembled for this request."},"cacheAgeSeconds":{"type":"number","minimum":0,"description":"Age of the cached payload in seconds. Zero when nothing was served from cache."},"stale":{"type":"boolean","description":"True when the payload is past its freshness window and is being served anyway."},"degraded":{"type":"boolean","description":"True when a provider failed and the last good payload is being served in its place. The values are real, and `cacheAgeSeconds` states how old they are."},"sourceCount":{"type":"integer","minimum":0,"description":"Number of entries in `sources`."},"sources":{"type":"array","items":{"type":"string"},"description":"The registered primary source for this series. Empty when the series has none recorded."},"attribution":{"type":"array","items":{"type":"string"},"description":"Credit lines that MUST be displayed wherever this data appears. Retained as plain strings for v1 compatibility; `attributionRecords` carries the same obligation resolved."},"attributionRecords":{"type":"array","items":{"$ref":"#/components/schemas/AttributionRecord"},"description":"The attribution obligation with its licence name and URL attached, so a consumer never has to infer licensing rules from a provider name."},"nextCursor":{"type":["string","null"],"description":"Pass this back as `cursor` for the next page. Null means this page was the last one. OPAQUE: it encodes an offset into the FILTERED result set, so it is valid only alongside the identical filter arguments, and the corpus underneath it can be rebuilt by an ingestion run between two pages. It is not a snapshot."},"lastIngestedAt":{"type":["string","null"],"format":"date-time","description":"When this series' materialised index was last rebuilt."},"valueNote":{"type":"string","description":"A fixed sentence restating that values are decimal strings and that a null value is an unpublished period rather than a zero."},"revisionNote":{"type":"string","description":"A fixed sentence restating what `revisionCount` counts and how to fetch the full vintage history."},"totalMatching":{"type":"integer","minimum":0,"description":"How many records matched BEFORE the page limit was applied. Compare it with the page length to know whether more pages exist; `nextCursor` answers the same question directly."}},"required":["requestId","generatedAt","cached","cacheAgeSeconds","stale","degraded","sourceCount","sources","attribution","attributionRecords","nextCursor","lastIngestedAt","valueNote","revisionNote","totalMatching"],"additionalProperties":false},"EconomicsObservationsResponse":{"type":"object","description":"The history listing, served when `period` is absent.","properties":{"data":{"$ref":"#/components/schemas/EconomicsObservationsData"},"meta":{"$ref":"#/components/schemas/EconomicsObservationsMeta"}},"required":["data","meta"],"additionalProperties":false},"VintageSeriesSummary":{"type":"object","description":"The four fields needed to read a vintage. The vintage view deliberately carries less of the definition than the history view.","properties":{"slug":{"type":"string"},"canonicalName":{"type":"string"},"unitCode":{"type":"string","enum":["PERCENT","PERCENTAGE_POINTS","INDEX_POINTS","COUNT","PERSONS","HOURS","LOCAL_CURRENCY","USD","EUR","CZK","GBP","RATIO","OTHER"],"description":"The series' registered unit."},"scale":{"type":"integer"}},"required":["slug","canonicalName","unitCode","scale"],"additionalProperties":false},"EconomicsVintageData":{"type":"object","properties":{"series":{"$ref":"#/components/schemas/VintageSeriesSummary"},"referencePeriod":{"type":"string","description":"Echoes the `period` argument VERBATIM. It is not validated against the period vocabulary, so a code no version was ever published under is echoed back beside an empty `versions`."},"versions":{"type":"array","items":{"$ref":"#/components/schemas/ObservationVersion"},"description":"Every version ever published for this period, OLDEST FIRST. Empty — with a 200 — when nothing has been published under this code. That is not an error: it is the honest answer to \"what vintages exist\", and it is only answerable at all because versions are appended rather than overwritten."}},"required":["series","referencePeriod","versions"],"additionalProperties":false},"EconomicsVintageMeta":{"type":"object","description":"The vintage view passes no sources and no total to the envelope, so `sources`, `attribution` and `attributionRecords` are empty and `totalMatching` is absent. The attribution obligation for these values is the one already carried by the history listing for the same series.","properties":{"requestId":{"type":"string","description":"Correlates with the `X-Request-Id` response header and with the server log line for this request."},"generatedAt":{"type":"string","format":"date-time","description":"When this response was assembled. Not the age of the data — read `cacheAgeSeconds` for that."},"cached":{"type":"boolean","description":"True when the payload behind this response came from the in-process cache rather than being assembled for this request."},"cacheAgeSeconds":{"type":"number","minimum":0,"description":"Age of the cached payload in seconds. Zero when nothing was served from cache."},"stale":{"type":"boolean","description":"True when the payload is past its freshness window and is being served anyway."},"degraded":{"type":"boolean","description":"True when a provider failed and the last good payload is being served in its place. The values are real, and `cacheAgeSeconds` states how old they are."},"sourceCount":{"type":"integer","minimum":0,"description":"Number of entries in `sources`."},"sources":{"type":"array","items":{"type":"string"},"description":"Always empty on this view. The vintage query passes no source list to the envelope."},"attribution":{"type":"array","items":{"type":"string"},"description":"Credit lines that MUST be displayed wherever this data appears. Retained as plain strings for v1 compatibility; `attributionRecords` carries the same obligation resolved."},"attributionRecords":{"type":"array","items":{"$ref":"#/components/schemas/AttributionRecord"},"description":"The attribution obligation with its licence name and URL attached, so a consumer never has to infer licensing rules from a provider name."},"vintageNote":{"type":"string","description":"A fixed sentence restating that the versions are oldest-first and that `revisionSequence` 0 is the first publication."}},"required":["requestId","generatedAt","cached","cacheAgeSeconds","stale","degraded","sourceCount","sources","attribution","attributionRecords","vintageNote"],"additionalProperties":false},"EconomicsVintageResponse":{"type":"object","description":"The single-period vintage history, served when `period` is present and non-empty.","properties":{"data":{"$ref":"#/components/schemas/EconomicsVintageData"},"meta":{"$ref":"#/components/schemas/EconomicsVintageMeta"}},"required":["data","meta"],"additionalProperties":false},"EconomicsObservationsEnvelope":{"description":"One route, two shapes. `?period={code}` returns the vintage history for that one period; its absence returns the period-by-period listing. The two are distinguished by which key `data` carries, `observations` or `versions`.","oneOf":[{"$ref":"#/components/schemas/EconomicsObservationsResponse"},{"$ref":"#/components/schemas/EconomicsVintageResponse"}]},"EconomicIndicatorFamily":{"type":"object","description":"A broad economic CONCEPT: \"consumer price inflation\", \"unemployment\". It has no country, no frequency, no unit and no value. \"CPI\" is a family; \"United States CPI, all items, year-on-year\" is a series, and conflating the two is what makes a number uninterpretable.","properties":{"id":{"type":"string"},"slug":{"type":"string","description":"URL identifier. Lower-case, digits and hyphens."},"canonicalName":{"type":"string"},"shortName":{"type":["string","null"],"description":"Conventional abbreviation, e.g. `CPI`."},"description":{"type":"string","description":"Factual description of what the concept measures. A reference table, not marketing copy."},"category":{"type":"string","enum":["PRICES","LABOUR","GROWTH","PRODUCTION","CONSUMPTION","TRADE","HOUSING","BUSINESS_ACTIVITY","MONETARY_POLICY","MONEY_AND_CREDIT","PUBLIC_FINANCE","EXTERNAL_SECTOR","CONFIDENCE","WAGES_AND_COSTS","FINANCIAL_STABILITY","OTHER"],"description":"Top-level filing category. A filing decision, not an analytical claim."},"subcategory":{"type":["string","null"],"description":"Finer classification where one applies."},"aliases":{"type":"array","items":{"type":"string"},"description":"Alternative names a HUMAN might search by. A discovery aid ONLY: aliases are never used to map a provider record onto this family, because name-based mapping is how a provider record lands in the wrong series."},"defaultImportance":{"type":"string","enum":["HIGH","MEDIUM","LOW","UNCLASSIFIED"],"description":"Starting point for event importance. A series or release may refine it."},"methodologyNotes":{"type":["string","null"],"description":"How the concept is measured, and what makes versions of it incomparable across countries."},"relatedFamilySlugs":{"type":"array","items":{"type":"string"},"description":"Related but NOT equivalent families. Records kinship without asserting the comparison the methodologies do not support — national CPI and harmonised HICP are related and are not the same statistic."},"active":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","description":"When this registry entry was authored."},"updatedAt":{"type":"string","format":"date-time","description":"When this registry entry last changed."}},"required":["id","slug","canonicalName","shortName","description","category","subcategory","aliases","defaultImportance","methodologyNotes","relatedFamilySlugs","active","createdAt","updatedAt"],"additionalProperties":false},"FamilySeriesSummary":{"type":"object","description":"A concrete series under the family. This is the link that stops a reader treating a family as measurable.","properties":{"slug":{"type":"string"},"canonicalName":{"type":"string"},"countryCode":{"type":["string","null"],"description":"ISO 3166-1 alpha-2, or null for a multinational aggregate."},"regionCode":{"type":["string","null"],"enum":["EA","EU27_2020","G7","G20","OECD","WORLD",null],"description":"HELPERG region code, or null for a single country."},"frequency":{"type":"string","enum":["DAILY","WEEKLY","MONTHLY","QUARTERLY","ANNUAL","IRREGULAR"],"description":"How often the source publishes."},"transformation":{"type":"string","enum":["NONE","MOM","YOY","QOQ","QOQ_ANNUALIZED","YTD","ROLLING_12_MONTH","OTHER"],"description":"Rate-of-change convention."},"seasonalAdjustment":{"type":"string","enum":["SEASONALLY_ADJUSTED","NOT_SEASONALLY_ADJUSTED","WORKING_DAY_ADJUSTED","CALENDAR_ADJUSTED","UNKNOWN","NOT_APPLICABLE"],"description":"Adjustment convention."},"unitCode":{"type":"string","enum":["PERCENT","PERCENTAGE_POINTS","INDEX_POINTS","COUNT","PERSONS","HOURS","LOCAL_CURRENCY","USD","EUR","CZK","GBP","RATIO","OTHER"],"description":"Unit of the published values."}},"required":["slug","canonicalName","countryCode","regionCode","frequency","transformation","seasonalAdjustment","unitCode"],"additionalProperties":false},"EconomicsIndicatorsData":{"type":"object","properties":{"families":{"type":"array","items":{"$ref":"#/components/schemas/EconomicIndicatorFamily"},"description":"Active families, in registry order. The whole set is returned: there is no `limit` on this route, because the registry is compiled into the build and is small and bounded by construction."},"categories":{"type":"array","items":{"type":"string","enum":["PRICES","LABOUR","GROWTH","PRODUCTION","CONSUMPTION","TRADE","HOUSING","BUSINESS_ACTIVITY","MONETARY_POLICY","MONEY_AND_CREDIT","PUBLIC_FINANCE","EXTERNAL_SECTOR","CONFIDENCE","WAGES_AND_COSTS","FINANCIAL_STABILITY","OTHER"],"description":"A filing category."},"description":"The complete closed category vocabulary, returned whether or not `category` was used to filter, so a client can populate a filter without a second call."}},"required":["families","categories"],"additionalProperties":false},"EconomicsIndicatorsMeta":{"type":"object","properties":{"requestId":{"type":"string","description":"Correlates with the `X-Request-Id` response header and with the server log line for this request."},"generatedAt":{"type":"string","format":"date-time","description":"When this response was assembled. Not the age of the data — read `cacheAgeSeconds` for that."},"cached":{"type":"boolean","description":"True when the payload behind this response came from the in-process cache rather than being assembled for this request."},"cacheAgeSeconds":{"type":"number","minimum":0,"description":"Age of the cached payload in seconds. Zero when nothing was served from cache."},"stale":{"type":"boolean","description":"True when the payload is past its freshness window and is being served anyway."},"degraded":{"type":"boolean","description":"True when a provider failed and the last good payload is being served in its place. The values are real, and `cacheAgeSeconds` states how old they are."},"sourceCount":{"type":"integer","minimum":0,"description":"Number of entries in `sources`."},"sources":{"type":"array","items":{"type":"string"},"description":"Always empty here. The registry is compiled into the build rather than ingested, so no registered source stands behind a definition — and an attribution obligation that does not exist is reported as absent rather than invented."},"attribution":{"type":"array","items":{"type":"string"},"description":"Credit lines that MUST be displayed wherever this data appears. Retained as plain strings for v1 compatibility; `attributionRecords` carries the same obligation resolved."},"attributionRecords":{"type":"array","items":{"$ref":"#/components/schemas/AttributionRecord"},"description":"The attribution obligation with its licence name and URL attached, so a consumer never has to infer licensing rules from a provider name."},"modelNote":{"type":"string","description":"A fixed sentence restating that a family is a concept with no country, frequency, unit or value, and pointing at the series route for measurable data."}},"required":["requestId","generatedAt","cached","cacheAgeSeconds","stale","degraded","sourceCount","sources","attribution","attributionRecords","modelNote"],"additionalProperties":false},"EconomicsIndicatorsResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/EconomicsIndicatorsData"},"meta":{"$ref":"#/components/schemas/EconomicsIndicatorsMeta"}},"required":["data","meta"],"additionalProperties":false},"RegistryMeta":{"type":"object","description":"The base envelope meta, with no route-specific additions. Used by the two detail views, which pass nothing extra.","properties":{"requestId":{"type":"string","description":"Correlates with the `X-Request-Id` response header and with the server log line for this request."},"generatedAt":{"type":"string","format":"date-time","description":"When this response was assembled. Not the age of the data — read `cacheAgeSeconds` for that."},"cached":{"type":"boolean","description":"True when the payload behind this response came from the in-process cache rather than being assembled for this request."},"cacheAgeSeconds":{"type":"number","minimum":0,"description":"Age of the cached payload in seconds. Zero when nothing was served from cache."},"stale":{"type":"boolean","description":"True when the payload is past its freshness window and is being served anyway."},"degraded":{"type":"boolean","description":"True when a provider failed and the last good payload is being served in its place. The values are real, and `cacheAgeSeconds` states how old they are."},"sourceCount":{"type":"integer","minimum":0,"description":"Number of entries in `sources`."},"sources":{"type":"array","items":{"type":"string"},"description":"Always empty here. The registry is compiled into the build rather than ingested, so no registered source stands behind a definition — and an attribution obligation that does not exist is reported as absent rather than invented."},"attribution":{"type":"array","items":{"type":"string"},"description":"Credit lines that MUST be displayed wherever this data appears. Retained as plain strings for v1 compatibility; `attributionRecords` carries the same obligation resolved."},"attributionRecords":{"type":"array","items":{"$ref":"#/components/schemas/AttributionRecord"},"description":"The attribution obligation with its licence name and URL attached, so a consumer never has to infer licensing rules from a provider name."}},"required":["requestId","generatedAt","cached","cacheAgeSeconds","stale","degraded","sourceCount","sources","attribution","attributionRecords"],"additionalProperties":false},"EconomicsIndicatorDetailData":{"type":"object","properties":{"family":{"$ref":"#/components/schemas/EconomicIndicatorFamily"},"series":{"type":"array","items":{"$ref":"#/components/schemas/FamilySeriesSummary"},"description":"Every ACTIVE series registered under this family. Empty means the concept is registered and nothing measurable has been wired to it yet."}},"required":["family","series"],"additionalProperties":false},"EconomicsIndicatorDetailResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/EconomicsIndicatorDetailData"},"meta":{"$ref":"#/components/schemas/RegistryMeta"}},"required":["data","meta"],"additionalProperties":false},"EconomicSeries":{"type":"object","description":"One specific measurable statistical series. Two series are the same series only if EVERY identifying dimension matches — not the title, and not the country plus the title. Year-on-year and month-on-month, adjusted and unadjusted, headline and core are separate series even where the display names are nearly identical, and \"US CPI 3.1%\" is not a fact until you know which.","properties":{"id":{"type":"string"},"slug":{"type":"string","description":"Authored, human-readable, used in URLs. Convenience — `identityKey` is the guarantee."},"identityKey":{"type":"string","description":"Derived from the dimensions ALONE, so renaming a series or correcting its description cannot detach its observation history. No two registered series share one; a duplicate defined under another name fails the build."},"indicatorFamilyId":{"type":"string"},"canonicalName":{"type":"string"},"shortName":{"type":["string","null"],"description":"Conventional short label."},"currencyCode":{"type":["string","null"],"description":"ISO 4217, where the series is denominated in a currency."},"primarySourceId":{"type":["string","null"],"description":"Registered slug of the organisation that PUBLISHES this series."},"referenceMetadata":{"type":["string","null"],"description":"Anything a consumer needs in order not to misread the number."},"active":{"type":"boolean","description":"Inactive series are omitted from the listing but remain resolvable by slug, so a stored reference to one does not become a 404."},"createdAt":{"type":"string","format":"date-time","description":"When this registry entry was authored."},"updatedAt":{"type":"string","format":"date-time","description":"When this registry entry last changed."},"familySlug":{"type":"string","description":"The indicator family this series measures. A family is a CONCEPT with no country, unit or value; see `/api/v1/economics/indicators/{slug}`."},"countryCode":{"type":["string","null"],"description":"ISO 3166-1 alpha-2. Null for a multinational aggregate; exactly one of `countryCode` and `regionCode` is set."},"regionCode":{"type":["string","null"],"enum":["EA","EU27_2020","G7","G20","OECD","WORLD",null],"description":"HELPERG region code. Null for a single country. A region is NOT a country: the euro area has no ISO 3166 code and its membership changes."},"frequency":{"type":"string","enum":["DAILY","WEEKLY","MONTHLY","QUARTERLY","ANNUAL","IRREGULAR"],"description":"How often the source publishes an observation."},"measurementType":{"type":"string","enum":["LEVEL","RATE","INDEX","PERCENT","PERCENT_CHANGE","COUNT","CURRENCY","RATIO","OTHER"],"description":"What kind of quantity the number is. Distinct from its unit."},"unitCode":{"type":"string","enum":["PERCENT","PERCENTAGE_POINTS","INDEX_POINTS","COUNT","PERSONS","HOURS","LOCAL_CURRENCY","USD","EUR","CZK","GBP","RATIO","OTHER"],"description":"Unit of the published value. Read with `scale`: a bare label cannot be validated, converted or compared, and a code plus a power of ten can be all three."},"scale":{"type":"integer","description":"Power of ten ALREADY APPLIED to stored values. `scale: 3` with `unitCode: \"PERSONS\"` and a value of `287.6` means 287,600 persons. Keeping the scale separate preserves the source's own precision instead of multiplying it out and inventing significant figures."},"transformation":{"type":"string","enum":["NONE","MOM","YOY","QOQ","QOQ_ANNUALIZED","YTD","ROLLING_12_MONTH","OTHER"],"description":"Rate-of-change convention. `NONE` is a level or an index. Mixing two of these is the commonest way an economic calendar reports a wrong number: 3.1 per cent year-on-year and 0.3 per cent month-on-month describe the same index and are not interchangeable."},"seasonalAdjustment":{"type":"string","enum":["SEASONALLY_ADJUSTED","NOT_SEASONALLY_ADJUSTED","WORKING_DAY_ADJUSTED","CALENDAR_ADJUSTED","UNKNOWN","NOT_APPLICABLE"],"description":"A materially different statistic, not a presentation choice: adjusted and unadjusted versions of one concept routinely disagree by more than the change being reported."},"priceAdjustment":{"type":"string","enum":["NOMINAL","REAL","CHAIN_LINKED_VOLUME","UNKNOWN","NOT_APPLICABLE"],"description":"Price basis. Nominal and real series are different statistics."},"populationScope":{"type":["string","null"],"description":"Population or coverage restriction, e.g. `ALL_URBAN_CONSUMERS`."},"productScope":{"type":["string","null"],"description":"Product or item restriction, e.g. `ALL_ITEMS_LESS_FOOD_ENERGY`."},"rateBasis":{"type":"string","enum":["ANNUAL_RATE","PERIOD_RATE","NOT_APPLICABLE"],"description":"Whether an annualisation convention is applied."}},"required":["id","slug","identityKey","indicatorFamilyId","canonicalName","shortName","currencyCode","primarySourceId","referenceMetadata","active","createdAt","updatedAt","familySlug","countryCode","regionCode","frequency","measurementType","unitCode","scale","transformation","seasonalAdjustment","priceAdjustment","populationScope","productScope","rateBasis"],"additionalProperties":false},"EconomicsSeriesListData":{"type":"object","properties":{"series":{"type":"array","items":{"$ref":"#/components/schemas/EconomicSeries"},"description":"Active series matching the filters, truncated to `limit`."},"totalMatching":{"type":"integer","minimum":0,"description":"How many matched before truncation. Repeated in `meta.totalMatching`; both are the same number."}},"required":["series","totalMatching"],"additionalProperties":false},"EconomicsSeriesListMeta":{"type":"object","properties":{"requestId":{"type":"string","description":"Correlates with the `X-Request-Id` response header and with the server log line for this request."},"generatedAt":{"type":"string","format":"date-time","description":"When this response was assembled. Not the age of the data — read `cacheAgeSeconds` for that."},"cached":{"type":"boolean","description":"True when the payload behind this response came from the in-process cache rather than being assembled for this request."},"cacheAgeSeconds":{"type":"number","minimum":0,"description":"Age of the cached payload in seconds. Zero when nothing was served from cache."},"stale":{"type":"boolean","description":"True when the payload is past its freshness window and is being served anyway."},"degraded":{"type":"boolean","description":"True when a provider failed and the last good payload is being served in its place. The values are real, and `cacheAgeSeconds` states how old they are."},"sourceCount":{"type":"integer","minimum":0,"description":"Number of entries in `sources`."},"sources":{"type":"array","items":{"type":"string"},"description":"Always empty here. The registry is compiled into the build rather than ingested, so no registered source stands behind a definition — and an attribution obligation that does not exist is reported as absent rather than invented."},"attribution":{"type":"array","items":{"type":"string"},"description":"Credit lines that MUST be displayed wherever this data appears. Retained as plain strings for v1 compatibility; `attributionRecords` carries the same obligation resolved."},"attributionRecords":{"type":"array","items":{"$ref":"#/components/schemas/AttributionRecord"},"description":"The attribution obligation with its licence name and URL attached, so a consumer never has to infer licensing rules from a provider name."},"identityNote":{"type":"string","description":"A fixed sentence restating that two series are the same only if every dimension matches."},"totalMatching":{"type":"integer","minimum":0,"description":"How many records matched BEFORE the page limit was applied. Compare it with the page length to know whether more pages exist; `nextCursor` answers the same question directly."}},"required":["requestId","generatedAt","cached","cacheAgeSeconds","stale","degraded","sourceCount","sources","attribution","attributionRecords","identityNote","totalMatching"],"additionalProperties":false},"EconomicsSeriesListResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/EconomicsSeriesListData"},"meta":{"$ref":"#/components/schemas/EconomicsSeriesListMeta"}},"required":["data","meta"],"additionalProperties":false},"SeriesSourceMapping":{"type":"object","description":"WHICH registered sources supply this series, and which one is authoritative. The provider's own dataset identifier is deliberately withheld: it is a HELPERG integration detail, not part of the canonical contract, and publishing it would invite consumers to bypass the canonical model.","properties":{"sourceId":{"type":"string","description":"Registered source slug."},"isPrimary":{"type":"boolean","description":"Whether this source is authoritative when two disagree. Recorded in the registry, never decided on the fly by comparing values."},"active":{"type":"boolean","description":"Whether the mapping is currently ingested."}},"required":["sourceId","isPrimary","active"],"additionalProperties":false},"EconomicsSeriesDetailData":{"type":"object","properties":{"series":{"$ref":"#/components/schemas/EconomicSeries"},"family":{"description":"The concept this series measures. NULL only if the registry were inconsistent — a series whose family does not exist fails at module load, so in practice this is always an object.","oneOf":[{"$ref":"#/components/schemas/EconomicIndicatorFamily"},{"type":"null"}]},"sources":{"type":"array","items":{"$ref":"#/components/schemas/SeriesSourceMapping"}}},"required":["series","family","sources"],"additionalProperties":false},"EconomicsSeriesDetailResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/EconomicsSeriesDetailData"},"meta":{"$ref":"#/components/schemas/RegistryMeta"}},"required":["data","meta"],"additionalProperties":false},"JobCounters":{"type":"object","description":"What the run did. `unchanged` is the idempotency assertion: a replay of an unchanged source must report zero created and zero updated.","properties":{"seen":{"type":"integer","minimum":0,"description":"Records the provider offered."},"created":{"type":"integer","minimum":0,"description":"Records that did not previously exist."},"updated":{"type":"integer","minimum":0,"description":"Records that existed and differed."},"unchanged":{"type":"integer","minimum":0,"description":"Records that existed and were byte-identical."},"rejected":{"type":"integer","minimum":0,"description":"Records refused by validation or quarantined, and NOT written. A record that cannot be mapped safely is retained in quarantine rather than discarded or guessed into a canonical series."}},"required":["seen","created","updated","unchanged","rejected"],"additionalProperties":false},"IngestionRun":{"type":"object","description":"The audit record of one ingestion run. READ `state` — the HTTP status alone does not tell you whether the ingestion worked, and a caller that trusts the status will record a failure as a success.","properties":{"id":{"type":"string"},"jobName":{"type":"string","description":"Stable job identifier, e.g. `economics:bls-release-schedule`."},"trigger":{"type":"string","enum":["SCHEDULE","MANUAL","WEBHOOK"],"description":"What caused this run. `SCHEDULE` means a GitHub Actions cron fired it; `MANUAL` means a person or a manual dispatch did. Taken from the `X-Helperg-Ingest-Trigger` request header and defaulting to `MANUAL` when that header is absent, so it records what the CALLER declared rather than anything the route inferred. `WEBHOOK` is in the model and no caller produces it today."},"state":{"type":"string","enum":["PENDING","RUNNING","SUCCEEDED","FAILED","PARTIAL","SKIPPED"],"description":"`SUCCEEDED` and `PARTIAL` both wrote; `PARTIAL` means some records were rejected. A partial run is operationally incomplete: it does not advance the durable last-success timestamp or provider checkpoint, and scheduled automation treats it as a failure until a clean replay. `SKIPPED` means either the provider reported no change (a conditional request answered 304) or another run held the ingestion lease — both are the mechanism working, and neither refreshes the last-success timestamp when the lease was the cause. `FAILED` arrives with HTTP 500."},"startedAt":{"type":"string","format":"date-time","description":"When the run began."},"finishedAt":{"type":["string","null"],"format":"date-time","description":"When it ended."},"durationMs":{"type":["number","null"],"description":"Wall-clock duration in milliseconds."},"counters":{"$ref":"#/components/schemas/JobCounters"},"error":{"type":["string","null"],"description":"A safe `CODE: message` summary when the run failed. Never a stack trace, never a provider body."},"rejections":{"type":"array","items":{"type":"string"},"description":"Why individual records were rejected or quarantined, capped at 50 so a run record cannot grow without bound."}},"required":["id","jobName","trigger","state","startedAt","finishedAt","durationMs","counters","error","rejections"],"additionalProperties":false},"AdminIngestionData":{"type":"object","properties":{"run":{"$ref":"#/components/schemas/IngestionRun"}},"required":["run"],"additionalProperties":false},"AdminIngestionResponse":{"type":"object","description":"The ordinary success envelope. It is returned with 202 for every terminal state EXCEPT `FAILED`, which is returned with 500 — in this same shape, NOT as an error envelope.","properties":{"data":{"$ref":"#/components/schemas/AdminIngestionData"},"meta":{"$ref":"#/components/schemas/RegistryMeta"}},"required":["data","meta"],"additionalProperties":false},"MigrationResult":{"type":"object","description":"What the migration found, and what it did or would do.","properties":{"found":{"type":"integer","minimum":0,"description":"v1 records recognised in storage."},"migrated":{"type":"integer","minimum":0,"description":"Records rewritten into the v2 model. Always 0 unless `apply=true`."},"alreadyCurrent":{"type":"integer","minimum":0,"description":"Records already in the v2 model and left alone."},"quarantined":{"type":"integer","minimum":0,"description":"v1 records whose title has no registered release mapping. Retained, never guessed into a series."},"failed":{"type":"integer","minimum":0,"description":"Records that parsed as neither v1 nor v2."},"notes":{"type":"array","items":{"type":"string"},"description":"Per-record explanations for the quarantined and failed counts."}},"required":["found","migrated","alreadyCurrent","quarantined","failed","notes"],"additionalProperties":false},"AdminMigrationData":{"type":"object","properties":{"migration":{"$ref":"#/components/schemas/MigrationResult"},"applied":{"type":"boolean","description":"Whether anything was written. False is the default: the operation is a preview unless `apply=true` was sent, so probing what it would do cannot accidentally do it."}},"required":["migration","applied"],"additionalProperties":false},"AdminMigrationResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AdminMigrationData"},"meta":{"$ref":"#/components/schemas/RegistryMeta"}},"required":["data","meta"],"additionalProperties":false},"ErrorResponse":{"type":"object","description":"Every failure, from every status. Branch on `error.code`, never on `error.message`: codes are part of the v1 contract and messages are not.","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["BAD_REQUEST","VALIDATION_FAILED","WEATHER_LOCATION_INVALID","ECONOMICS_RANGE_INVALID","NOT_FOUND","UNAUTHORIZED","FORBIDDEN","SOURCE_DISABLED","METHOD_NOT_ALLOWED","RATE_LIMITED","INTERNAL","CONFIGURATION_ERROR","STORAGE_UNAVAILABLE","SERVICE_UNAVAILABLE","UPSTREAM_UNAVAILABLE","UPSTREAM_INVALID_PAYLOAD","UPSTREAM_PAYLOAD_TOO_LARGE","UPSTREAM_TIMEOUT"],"description":"Stable machine-readable code. This enum is the UNION across every operation in this document; the exact set one operation can produce is on that operation as `x-helperg-error-codes`. See `x-helperg-unreachable-error-codes` at the root for the rest of the taxonomy and why it cannot appear here at all."},"message":{"type":"string","description":"Safe, non-specific text. No stack, no provider body, no URL with credentials in it, and no environment value ever reaches this field."},"requestId":{"type":"string","description":"Matches the `X-Request-Id` header. Quote it in a support request."},"details":{"type":"object","additionalProperties":{"type":["string","number","boolean"]},"description":"Present only when there is something safe to add — typically `{ \"parameter\": \"lat\" }` or `{ \"parameter\": \"cursor\" }` on a validation failure, `{ \"requiredScope\": \"economics:read\" }` on a scope failure, `{ \"maxDays\": 400 }` on an over-long range, or `{ \"retryAfterSeconds\": 30 }` on a rate limit. At most twelve keys, each string bounded to 120 characters."}},"required":["code","message","requestId"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}},"x-helperg-unreachable-error-codes":{"SOURCE_NOT_FOUND":"Raised only when a lookup by source slug misses. Every documented route resolves its sources from the compiled registry entry itself — the weather adapter from its own registration, the economics routes from a series' recorded primary source — so the lookup has no way to miss. It remains reachable on the source-registry route, which this document does not describe."},"x-helperg-cache-scope":"per-instance","x-helperg-rate-limit-scope":"per-instance","x-helperg-rate-limit-bucket":"PER_ROUTE_NAME","x-helperg-durable-storage":{"driver":"vercel-blob","configuredDriver":"vercel-blob","matchesConfiguration":true,"available":true,"durable":true,"reason":"BLOB_ACTIVATED","note":"A durable store is bound to this deployment, so the operations marked `x-helperg-requires-durable-storage` can answer here. Durable storage is a PER-DEPLOYMENT fact: a sibling deployment of the same build may resolve `unavailable` and fail those operations closed with 503."}}