PxWebApi v2 — Generic Skill
This skill guides you through using PxWebApi v2 to search, explore, and retrieve official statistics. PxWebApi v2 is developed by Statistics Sweden (SCB) and used by several national statistical institutes. Everything below that says varies by installation has been verified to vary on the three installations listed under "Known installations".
Data integrity — the base rule
Official statistics carry an agency's name. A wrong number under that citation damages trust in the agency, not just in the answer. This rule outranks everything else in this skill:
Never state a number you have not fetched from the API in this conversation.
- No numbers from memory. If you did not run the query, you do not have the number — including numbers you are confident about. Populations, price indices and unemployment rates all move, and training data has a cutoff.
- No numbers from other sources in the same answer. Point the user elsewhere rather than blending.
- If the API fails, say so. No estimates, no "roughly". See Fallback.
- No interpolation or projection. A period missing from the extract is missing from the answer.
- Mark your own calculations. Growth rates, shares and sums are yours, not the agency's — show which fetched numbers they rest on, and keep the API's decimals (
category.unit.decimals). - Show
statusvalues as they are. Missing, provisional and confidential values belong in the table, not hidden or replaced by zero. - Show mandatory notes. When
extension.noteMandatoryis set, the agency has decided the note travels with the figure. Omitting it presents the number without the caveat the agency attached. See Step 5. - Check
discontinuedandlastPeriod. Tables are closed and the series often continues in a new one. If you use a discontinued table, say so and give the last period.
Not finding the number is a valid answer. An honest "not found", with suggested search terms, beats a plausible number that is wrong.
Known PxWebApi v2 installations
| Agency | Country | Base URL | Languages |
|---|---|---|---|
| Statistics Norway (SSB) | Norway | https://data.ssb.no/api/pxwebapi/v2 |
no, en |
| Statistics Sweden (SCB) | Sweden | https://statistikdatabasen.scb.se/api/v2 |
sv, en |
| Official Statistics Portal of Latvia (CSP) | Latvia | https://api.stat.gov.lv/api/v2 |
lv, en |
All three verified live 2026-09-08. They differ more than the shared API shape suggests — the full comparison is in references/api-details.md; the four differences that change how you query:
- Default data format. SSB and SCB return json-stat2 when
outputFormatis omitted; Latvia returns PX (application/octet-stream). Always setoutputFormat=json-stat2explicitly. - Cell limit.
maxDataCellsis 800 000 at SSB, 150 000 at SCB and 10 000 at Latvia — an 80× spread. Read it from/configevery time. - Variable names. The metric is
ContentsCodeon all three, but time isTid(SSB, SCB) orTIME(Latvia) and geography isRegionorAREA. Read names fromroleandid, never assume. - Period codes. Quarters are
2024K2at SSB/SCB and2026Q2at Latvia; weeks are2026U35(SSB),2026V30(SCB),2026W13(Latvia). Read the codes from metadata before writing a time filter.
These agencies were checked on 2026-08-30 and are still v1-only — no v2 endpoint responds at any of the usual URL patterns. Use generic-pxweb-v1-skill for them:
| Agency | Country | v1 base URL |
|---|---|---|
| Statistics Finland | Finland | https://pxdata.stat.fi/PXWeb/api/v1/{fi|sv|en}/StatFin |
| Statistics Iceland | Iceland | https://px.hagstofa.is/pxis/api/v1/is/{database} |
| Statistics Faroe Islands | Faroe Islands | https://statbank.hagstova.fo/api/v1/{fo|en}/H2 |
| Statistics Greenland | Greenland | https://bank.stat.gl/api/v1/{da|en|kl}/Greenland |
| Statistics Estonia | Estonia | https://andmed.stat.ee/api/v1/{et|en}/stat |
SSB and SCB each still run their v1 API alongside v2 (data.ssb.no/api/v0/, api.scb.se/OV0104/v1/doris/), so a v1 URL from a user's old script is not evidence that the agency lacks v2.
Which version is this installation?
GET {base}/config— a v2 installation returns a JSON object withapiVersion,maxDataCells,dataFormats.- If that fails, try v1's config, which is a query parameter, not a path:
GET {v1_base}?config. The v1 path form/configreturns 400.
Check that the body parses as JSON — do not trust the status code. Statistics Greenland answers GET /api/v2/config with HTTP 200, Content-Type: text/html, and the body CONFIG404;…<br>Filen findes ikke - File not found (re-verified 2026-09-08). A version check that only looks at the status code concludes "v2 exists" and every subsequent call fails confusingly.
For SSB or SCB queries, prefer the dedicated sibling skills (ssb-pxwebapi-v2, scb-pxwebapi-v2) — they have agency-specific examples, curated table catalogs, and operational details that this generic skill does not cover.
API endpoints
| Endpoint | Method | Purpose |
|---|---|---|
/config |
GET | Call first. maxDataCells, rate limit, defaultDataFormat, dataFormats, languages, sourceReferences |
/tables |
GET | Search for a table when you do not know the ID |
/tables/{id} |
GET | One table: firstPeriod/lastPeriod, timeUnit, discontinued |
/tables/{id}/metadata |
GET | Variables, codes and codelists — mandatory before fetching data |
/tables/{id}/defaultselection |
GET | The table's default selection — a starting point for large tables |
/tables/{id}/data |
GET / POST | Fetch data. POST for complex queries, GET for a shareable URL |
/codelists/{id} |
GET | Look up one codelist in isolation |
/savedqueries |
POST | Create a shareable, reusable query |
/savedqueries/{id} |
GET | The definition of a saved query |
/savedqueries/{id}/data |
GET | Run a saved query and get data |
/savedqueries/{id}/selection |
GET | The selection of a saved query |
All endpoints accept the lang parameter; supported languages depend on the installation. Installation-specific configuration, the rate-limit table and output formats: references/api-details.md. The json-stat2 document itself (structure, indexing, extension, status codes): references/json-stat2.md — the format is shared with Eurostat and other non-PxWeb providers.
Workflow
Follow these steps in order. Never skip the metadata step.
Step 1: Identify the installation
Determine which PxWebApi v2 installation the user needs. If unclear, ask. Set the base URL, then GET {base_url}/config first — the version check above tells you how to read it, and the fields are in references/api-details.md.
Step 2: Search for tables
Use GET {base_url}/tables with the query parameter.
Search parameters:
| Parameter | Type | Description |
|---|---|---|
query |
string | Free-text search keywords |
pastDays |
int | Limit to tables updated in the last N days |
includeDiscontinued |
bool | Include discontinued series (default: false) |
pageNumber |
int | Page number for pagination |
pageSize |
int | Number of results per page (default: 20) |
Search tips:
- Use the agency's local-language terms; add a word from the expected title when the hit list is long
title:restricts to the title field;AND,ORand"phrase"~Nwork (the engine is Lucene). The default operator between words differs: AND at SSB and SCB (population region=population AND region), OR at Latvia (population region=population OR region, 494 hits against 125). Write the operator out in any query you intend to share- Truncation matches the stemmed index term, so truncate short.
population*returns 0 hits at Latvia and at SSB's English index;popul*returns 203 and 403. Swedishfolkmängd*returns 0 at SCB,folkmäng*returns 117. Cut the word before its ending, and checkpage.totalElementsbefore concluding a table does not exist. A leading wildcard (*ulation) returns HTTP 500 at Latvia - Default
pageSizeis 20. A table in position 47 is never on page 1 — raisepageSizeor page through
Screen candidates from the search hit, before spending a metadata call each. A /tables hit carries more than the title:
| Field | Use |
|---|---|
variableNames |
The table's variable labels. Lets you reject "wrong breakdown" candidates without fetching metadata at all — the single biggest call saver when several tables match |
firstPeriod / lastPeriod |
Coverage. These live here, not in the json-stat2 dataset |
timeUnit |
Annual, Quarterly, Monthly, Weekly, Other. The only place frequency is published — json-stat2 has no field for it. Other means the codes are not dates at all (Latvia IRJ010: Time01) |
paths |
Where the table sits in the agency's subject hierarchy, as full breadcrumb arrays. Useful for finding sibling tables and for explaining provenance |
discontinued |
Whether the series has stopped — but see below |
source, subjectCode, updated, category |
Attribution and freshness |
discontinued is not an aliveness test — compare lastPeriod with today. Agencies freeze a table and continue the series in a new one without setting the flag: SCB's TAB638 stops at 2024 with discontinued: null while TAB6471 carries 2025 onwards. At Latvia the key is absent from the /tables hit altogether. If lastPeriod is further back than the table's frequency should allow, find the successor before answering. The catalogue can also lag the other way: Latvia's /tables/IRS010m says lastPeriod: 2026M04 while the data run to 2026M07 — take "latest" from a top(1) on the data, not from the hit.
Present the 3–5 most relevant hits with table ID, title, last period, time frequency, and discontinued status.
Step 3: Explore metadata
Use GET {base_url}/tables/{id}/metadata to understand the table structure.
Metadata is returned in json-stat2 format. Focus on:
idarray — variable names;sizegives the number of values per variabledimensionobject — per variable: codes (category.index), labels (category.label), units (category.unit), andextensionwitheliminationand the availablecodelistsroleobject — start the analysis here.role.metricis what is measured (ContentsCodeon all three known installations; readcategory.unitfor unit and decimals).role.timeis the time dimension (TidorTIME).role.geois optional and installation-dependent — SSB and Latvia set it (Region,AREA), SCB does not, even on tables with 290 municipalities. Look for a geographic variable inidfirst; only when none exists does the data cover the whole area. Do not ask the user. Remaining variables inidare breakdown dimensionsnotearray +extension.noteMandatory— table notes, and which of them must be shown, keyed by index intonote. See Step 5extension(root) —noteMandatory,contact, PX metadata underextension.px, anddiscontinuedwhen set.firstPeriod/lastPeriod/timeUnitare not here — they are on the/tableshit andGET /tables/{id}
Key rules:
- Read
eliminationfrom the metadata response only — in a data response the field describes the extract you received, not the table's contract. Omitting an eliminable variable removes it from the response entirely (fromidanddimension, not as a total row); note it yourself. Metadata does not distinguish a dimension with a predefined total code from one summed on the fly — scancategory.labelfor a total, or probe;eliminationValueCodeappears only in data responses that include the total (verified on SSB, SCB and Latvia). Which variables are eliminable is a property of the table, not of the variable name:Alderis optional in SCB's TAB638 and mandatory in TAB6471. Details:references/json-stat2.md - Omitting a variable with
elimination: falsefails with400 — "Missing selection for mandantory variable"(the API's own spelling, identical on all three installations; quote it as-is). This is the sharpest v1/v2 difference: v1 returns all the variable's values instead of failing.
Codelists: group values into higher aggregation levels — agg_ maps many-to-one, vs_ is an alternative value set. The codelist's own codes are what go in valueCodes (read them from GET /codelists/{id}), and the response does not record which codelist produced a figure. See references/codelists-and-filters.md.
Default selection: GET {base_url}/tables/{id}/defaultselection is a starting point for large tables. A GET /data with no selection is not an error and does not return the whole table — it silently returns the default selection, HTTP 200, with eliminable dimensions outside it summed away unannounced. SCB's TAB6471 returns three cells with Region absent from the response entirely; Latvia's default response also orders id differently from the metadata. Always build the selection yourself — figures and verification in references/troubleshooting.md.
Step 4: Build and run query
PxWebApi v2 supports both GET and POST for data retrieval. You can also use the agency's web interface to build queries graphically — look for a "Save" or "API query" option to get ready-made GET URLs and POST bodies.
Always pass outputFormat=json-stat2 explicitly. The default is per installation: json-stat2 at SSB and SCB, PX at Latvia — a body starting CHARSET="ANSI"; is what you get when you forget.
POST (recommended for complex queries)
The example uses Region, ContentsCode and Tid — Nordic names. At Latvia the same query reads AREA, ContentsCode and TIME. Substitute the actual names from role.geo, role.metric and role.time.
POST {base_url}/tables/{id}/data?outputFormat=json-stat2
Content-Type: application/json
{
"selection": [
{ "variableCode": "Region", "valueCodes": ["01"] },
{ "variableCode": "ContentsCode", "valueCodes": ["Population"] },
{ "variableCode": "Tid", "valueCodes": ["top(5)"] }
]
}
GET (simpler queries, shareable URLs)
GET {base_url}/tables/{id}/data?valueCodes[Region]=01&valueCodes[ContentsCode]=Population&valueCodes[Tid]=top(5)&outputFormat=json-stat2
Filter expressions in valueCodes
All of these work, verified on all three installations: top(N) = last N values, bottom(N) = first N, from(code) = from and including, to(code) = up to and including, range(from,to) = closed interval, top(N,offset) / bottom(N,offset) = N values starting offset from the end / start, * and ? wildcards (2025M* = one year of months), and explicit lists.
In a GET URL, any expression that contains a comma must be wrapped in square brackets — valueCodes[Tid]=[range(2024M01,2024M03)], [top(3,2)] — because a bare comma is the list separator (valueCodes[Tid]=2024M01,2024M02). Unwrapped, range(...) returns 400 — "Illegal selection expression" on every installation, which is easy to misread as the function not existing. Single-argument forms (top(3), from(2024M01)) work with or without brackets. In a POST body it is the other way round: write the bare string ("valueCodes": ["range(2024M01,2024M03)"]); a bracketed string is looked up as a code and fails with Non-existent value. Expressions can be mixed with explicit codes (["2015", "top(2)"]), but two function expressions in one array give the union — ["from(2024M01)", "to(2024M03)"] is 319 periods, not the interval. With curl, pass -g: the brackets in valueCodes[Var] are otherwise parsed by curl itself (bad range in URL) — see references/troubleshooting.md.
For the time dimension, prefer top(N) or from(code) over range() and explicit periods — relative filters capture new periods automatically, so shareable URLs and saved queries stay current instead of freezing on whatever was latest when they were written. Use range() when the closed interval is the point. Full syntax: references/codelists-and-filters.md.
Output formats
Default json-stat2; also csv, xlsx, html, px, json-px, and at some installations parquet — dataFormats in /config is authoritative, and its spelling matters (Latvia lists json_stat2 too, which returns 400). Parameters (UseCodesAndTexts, IncludeTitle, separators, heading/stub): references/api-details.md.
Important limits:
maxDataCellsfrom/config— 10 000 to 800 000 across the known installations; exceeding it is400 — "Too many cells selected"- Rate limiting: check both
/configand the response headers — SSB announces only inx-ratelimit-*headers, SCB and Latvia in/configandX-Rate-Limit-*headers (a different spelling; match both). Seereferences/api-details.md - GET URLs cannot exceed ~2,100 characters — use POST for complex queries
- Start narrow — it's easier to expand than to handle too much data
Step 5: Present results
- Display data in a clean markdown table
- Say which dimensions you collapsed and which codelist you used — the response records neither. If the reader cannot reconstruct your query from your answer, the extract is not reproducible. See "What json-stat2 cannot express" in
references/json-stat2.md - Show mandatory notes.
extension.noteMandatoryis keyed by index into the rootnotearray and travels with the data response, so it costs no extra call. A mandatory note may constrain your own arithmetic — SCB's population tables from 2025 carry disclosure-control noise, so published totals are not the sum of their parts; say so when you sum - Always include source attribution listing every table ID used — if multiple tables were combined, list all of them; never omit a source table.
/configsourceReferencesgives the agency's own citation string per language - Explain what the numbers mean in context — in the user's language
- Present units clearly (count, percent, index, currency); for an index, give the base period
- Offer to visualize the data or download in another format. If the data came from SSB and the
ssb-chart-skillskill is available in the environment, use it for the visualization
Step 6: Saved queries (optional)
To create a shareable, reusable query:
POST {base_url}/savedqueries
Content-Type: application/json
{
"tableId": "{id}",
"language": "en",
"selection": {
"selection": [
{ "variableCode": "Region", "valueCodes": ["01"] },
{ "variableCode": "ContentsCode", "valueCodes": ["Population"] },
{ "variableCode": "Tid", "valueCodes": ["top(5)"] }
]
},
"outputFormat": "json-stat2",
"outputFormatParams": []
}
Both outputFormat and outputFormatParams are required in the savedqueries body — pass outputFormatParams: [] if you don't need any. Include all non-eliminable variables in the selection, otherwise the API returns HTTP 400. The endpoint exists on all three installations.
Useful for reports that are updated regularly — top(N) always returns the latest periods.
Pitfalls — never
The integrity rules at the top always apply — in addition:
- Fetch data without filters and assume you got the table — you get the default selection, HTTP 200, with eliminable dimensions summed away unannounced (see Step 3)
- Assume table IDs, variable codes, codelists, limits, default format or period-code letters are the same across installations — re-check
/configand metadata when switching installation - Mix codes from different codelists
- Present data without units
Fallback
Common errors and their causes (400 titles, 403, 404, 429, PX instead of JSON, empty search results): references/troubleshooting.md.
If the API is not available:
- Say plainly that the data could not be fetched. Never fill the gap with numbers from memory — without API access you deliver guidance, not statistics
- Refer the user to the agency's web-based statistical database
- Suggest search terms based on the question