Base URL
https://statorigin.org/api/v1
All endpoints are GET, return JSON, and send
Access-Control-Allow-Origin: *, so you can call them straight from a
browser without a proxy.
Quick start
curl "https://statorigin.org/api/v1/statistics?industry=electric-vehicles®ion=CN&year=2020..2026&limit=5" import requests
r = requests.get("https://statorigin.org/api/v1/statistics",
params={"industry": "electric-vehicles", "region": "CN", "limit": 100})
for row in r.json()["data"]:
print(row["period"]["start"][:4], row["value"], row["scale"],
"-", row["source"]["org"], row["source"]["locator"]) const res = await fetch(
"https://statorigin.org/api/v1/statistics?industry=electric-vehicles&limit=100"
);
const { data, meta } = await res.json();
// Every row carries its own citation, ready to use.
console.log(data[0].citation.apa);
console.log(meta.attribution); Endpoints
12 endpoints. Every one returns the same envelope and the same licence and attribution metadata.
/{indicators|industries|regions}/{slug}/export.{csv|xlsx} Spreadsheet downloads
CSV and Excel workbooks include every source field, original source terms and the StatOrigin CC BY 4.0 compilation license. Excel preserves typed numbers and adds a license sheet. Use CSV if a workbook exceeds 16 MiB or a source field exceeds Excel’s cell limit; exports never silently truncate records. No legacy binary XLS is generated.
| Parameter | Type | Notes |
|---|---|---|
slug | string | Indicator, industry or region slug. |
/statistics Query data points
The main endpoint. Filters compose, so you can ask for one indicator in one region over a year range.
| Parameter | Type | Notes |
|---|---|---|
industry | string | Industry slug. Comma-separate for several. |
region | string | Region code or slug. Comma-separate for several. |
indicator | string | Indicator slug. |
year | string | A year, or an inclusive range like 2020..2026. |
value_type | enum | actual · estimate · forecast |
source_org | string | Source organisation slug. |
limit | integer | 1–500. Default 50. |
cursor | string | Opaque cursor from meta.next_cursor. |
/statistics/{id} One data point
Returns the record plus any sources that disagree with it, so a consumer sees the same conflict the page shows.
| Parameter | Type | Notes |
|---|---|---|
id | string | A stat_… identifier. |
/industries Industry tree
The full ISIC Rev.4 classification: 21 sections, 88 divisions.
| Parameter | Type | Notes |
|---|---|---|
level | integer | 1 = section, 2 = division. |
parent | string | Parent ISIC code, e.g. C. |
/industries/{slug} One industry
Detail plus a coverage summary, its indicators, regions and sources.
| Parameter | Type | Notes |
|---|---|---|
slug | string | e.g. electric-vehicles |
/indicators List indicators
Filter by industry or by a substring of the name and definition.
| Parameter | Type | Notes |
|---|---|---|
industry | string | Industry slug. |
q | string | Substring match. |
/indicators/{slug} One indicator
Definition, units, methodology note, and the regions and sources that carry it.
| Parameter | Type | Notes |
|---|---|---|
slug | string | Indicator slug. |
/indicators/{slug}/series Time series
One entry per observation, each with its own source: a series here is a set of individually sourced points, not one dataset with one citation.
| Parameter | Type | Notes |
|---|---|---|
region | string | Region code or slug. Defaults to GLOBAL. |
from | integer | Earliest year. |
to | integer | Latest year. |
/sources Source organisations
Every publisher in the database, with its tier and contribution.
| Parameter | Type | Notes |
|---|---|---|
tier | integer | 1–4. |
/sources/{slug} One organisation
Its documents and the data taken from them.
| Parameter | Type | Notes |
|---|---|---|
slug | string | Organisation slug. |
limit | integer | 1–500. Default 100. |
/regions Regions
Countries and supranational groupings.
| Parameter | Type | Notes |
|---|---|---|
type | string | world · country · bloc · subnational |
/search Full-text search
Searches indicator names, regions, industries, source organisations and the verbatim source quotes.
| Parameter | Type | Notes |
|---|---|---|
q | string | 2–120 characters. |
limit | integer | 1–100. Default 25. |
Response shape
Every list endpoint returns data plus meta. Each element
of data is a full record; provenance is never abbreviated.
| Field | Meaning |
|---|---|
id | The permanent identifier. Also the last path segment of the record's URL. |
indicator | Slug, name and definition of what is being measured. |
industry | Array: a figure can belong to more than one industry, with the ISIC code. |
region | Code, name and type (world, country, bloc). |
period | Start, end and granularity. Always explicit, never inferred from context. |
value + scale | Kept separate on purpose, so the source's own magnitude survives. 17.2 + million. |
value_type | actual, estimate or forecast, never conflated. |
source | Publisher, report, publication date, locator, the verbatim quote, access date and link status. |
citation | Pre-formatted APA and BibTeX, crediting the original publisher and linking to the record. |
is_secondhand | True when the figure was relayed rather than read from the original publication. |
Live example: /api/v1/statistics/stat_C3GXCWCGPPPPWY1K0743MWVCXJ
Reproduce a chart
Every hub chart is drawn from one series call. This is the electric vehicles hub's first chart, electric cars as a share of new car sales: one request, the response, and a plot.
curl "https://statorigin.org/api/v1/indicators/global-ev-sales-share-of-new-cars/series?region=GLOBAL" {
"data": {
"indicator": { "slug": "global-ev-sales-share-of-new-cars", "name": "…", "unit": "percent", "scale": "1" },
"unit": "percent",
"scale": "1",
"region": { "code": "GLOBAL", "name": "Global", "type": "world" },
"points": [
{
"period_start": "2024-01-01",
"period_label": "2024",
"value": 20,
"value_type": "actual",
"statistic_id": "stat_…",
"url": "https://statorigin.org/stat/stat_…",
"source_org": "IEA",
"source": { "org": "…", "document_title": "…", "document_url": "…", "locator": "…", "published_date": "…" }
}
],
"available_regions": ["GLOBAL", "…"]
},
"meta": { "total": 6, "attribution": "…", "license": "…", "docs": "…" }
} import requests, matplotlib.pyplot as plt
d = requests.get("https://statorigin.org/api/v1/indicators/global-ev-sales-share-of-new-cars/series?region=GLOBAL").json()["data"]
pts = d["points"]
years = [int(p["period_start"][:4]) for p in pts]
vals = [p["value"] for p in pts]
actual = [p["value_type"] == "actual" for p in pts]
plt.plot(years, vals, marker="o") # every point, one line
plt.scatter([y for y, a in zip(years, actual) if not a],
[v for v, a in zip(vals, actual) if not a], facecolors="white") # hollow = forecast
plt.title(d["indicator"]["name"]); plt.ylabel(d["unit"])
plt.figtext(0.01, 0.01, "Source: " + ", ".join(sorted({p["source_org"] for p in pts})) + " · statorigin.org")
plt.show() region takes a code or a slug (GLOBAL, CN, china) and defaults to GLOBAL; from and to bound the years. Every value is expressed in the
series-level unit and scale, whatever scale the publisher
used for that year. Each point is its own sourced record: url is the
permanent page for that observation, and source is what it was taken
from. If an indicator is renamed, the old slug is kept in slug_history
and keeps resolving, so a saved script does not break.
Live: /api/v1/indicators/global-ev-sales-share-of-new-cars/series?region=GLOBAL · the chart as drawn on the hub: electric vehicles charts
Authentication and quotas
| Tier | Requests per day | How |
|---|---|---|
| Anonymous | 1,000 | No key. Counted per IP. |
| Free key | 10,000 | Authorization: Bearer so_live_… |
| Higher quotas | 100,000 | Available on request for research and public-interest projects. |
Responses include X-RateLimit-Limit, X-RateLimit-Remaining
and X-RateLimit-Reset. Exceeding the quota returns 429 with Retry-After.
Quotas exist to stop abuse, not to sell access. If you need more than the free tier for something useful, ask: the answer is usually yes.
Errors
Errors return { "error": { "code", "message", "docs" } } with an
HTTP status that matches the code.
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request | A parameter was missing or malformed. The message says which. |
| 401 | invalid_api_key | The bearer token is not recognised or is deactivated. |
| 404 | not_found | No record matches that identifier. |
| 429 | rate_limited | Daily quota exhausted. Retry-After gives the seconds remaining. |
| 500 | internal_error | Something failed on our side. Retrying is reasonable. |
| 503 | database_unavailable | A deployment problem on our side, not yours. |
A malformed filter is rejected rather than ignored. If you pass
year=20204 you get a 400, not a result set containing
every year. A silently dropped filter is how wrong numbers get into other
people's work.
Licence and attribution
Data is published under CC BY 4.0. Attribution is required and commercial use is permitted.
Attribute the original publisher (they did the work) and link to
the permanent record so the trail stays checkable. Every response includes an
attribution string in meta and a formatted citation on
each record, so you never have to assemble one by hand.
Machine-readable resources
- OpenAPI 3.1 specification · generate a client, or import into Postman or Insomnia
- llms.txt · a plain-language brief for language models
- Sitemap index · hubs, indicators, topics, regions, rankings, reports, sources and static pages, with truthful lastmod values
- Methodology · how figures are sourced and verified
Currently serving 232938 published data points across 3138 indicators and 171 source organisations.