API

A REST API over 232938 sourced industry statistics from 171 primary sources. Free, CORS-open, and every response carries the complete provenance and a ready-made citation for each data point.

For ChatGPT and other MCP clients, connect to https://statorigin.org/mcp. No key.

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
curl "https://statorigin.org/api/v1/statistics?industry=electric-vehicles&region=CN&year=2020..2026&limit=5"
Python
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"])
JavaScript
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.

GET /{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.
GET /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.
GET /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.
GET /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.
GET /industries/{slug}

One industry

Detail plus a coverage summary, its indicators, regions and sources.

Parameter Type Notes
slug string e.g. electric-vehicles
GET /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.
GET /indicators/{slug}

One indicator

Definition, units, methodology note, and the regions and sources that carry it.

Parameter Type Notes
slug string Indicator slug.
GET /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.
GET /sources

Source organisations

Every publisher in the database, with its tier and contribution.

Parameter Type Notes
tier integer 1–4.
GET /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.
GET /regions

Regions

Countries and supranational groupings.

Parameter Type Notes
type string world · country · bloc · subnational
GET /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
idThe permanent identifier. Also the last path segment of the record's URL.
indicatorSlug, name and definition of what is being measured.
industryArray: a figure can belong to more than one industry, with the ISIC code.
regionCode, name and type (world, country, bloc).
periodStart, end and granularity. Always explicit, never inferred from context.
value + scaleKept separate on purpose, so the source's own magnitude survives. 17.2 + million.
value_typeactual, estimate or forecast, never conflated.
sourcePublisher, report, publication date, locator, the verbatim quote, access date and link status.
citationPre-formatted APA and BibTeX, crediting the original publisher and linking to the record.
is_secondhandTrue 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.

Request
curl "https://statorigin.org/api/v1/indicators/global-ev-sales-share-of-new-cars/series?region=GLOBAL"
Response (abridged)
{
  "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": "…" }
}
Python, matplotlib
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.

Building something with this? Tell us and we will link to it from the project's showcase. Public-interest projects, teaching material and open-source tools get higher quotas for free.

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.