Field-tested against api.nobelprize.org on 2026-11-09 using a Browser Use cloud browser (logged out) plus plain HTTP (curl, Python urllib). About 12 browser requests (page loads and in-page fetch()) and 25 plain HTTP calls to api.nobelprize.org, all serial at 1 s or more spacing: every answer was HTTP 200, no 429, no challenge, no key, no login.
robots.txt (checked 2026-11-09, curl and the cloud browser): api.nobelprize.org/robots.txt is HTTP 200 text/html, a Slim framework placeholder page ("Hello robots.txt!") with 0 directives; www.nobelprize.org/robots.txt has only User-agent: * / Disallow: /wp-admin/ / Allow: /wp-admin/admin-ajax.php. No named AI-agent groups on either host. Terms: the API terms (https://www.nobelprize.org/about/terms-of-use-for-api-nobelprize-org-and-data-nobelprize-org/) say "The Services are free to use according to the Creative Commons Zero (CC0) license. In case of conflicting terms or other inconsistencies, these Terms shall prevail", "There is no obligation to register in order to use the API", "Applications shall be constructed in a manner which minimizes the capacity burden of the API. Consider using a local cache", data should not be altered or censored, and apps "may not use Nobel Prize Outreach's name, logos, or other trademarks in their titles or logos, or in any way that implies endorsement" (citing the API as the data source is encouraged). No AI, ML, scraping or bot clause. The www.nobelprize.org site terms (https://www.nobelprize.org/about/terms-of-use/) cover the site's own Content (articles, photos) and require written permission for commercial use; this skill fetches only the API, which the API terms put under CC0, and those site terms also have no AI or scraping clause. This skill is not affiliated with or endorsed by Nobel Prize Outreach.
Quick summary
- No scraping. Official, keyless JSON API,
Access-Control-Allow-Origin: *,cache-control: public, max-age=60:GET https://api.nobelprize.org/2.1/nobelPrizes?nobelPrizeYear=2025&nobelPrizeCategory=phy- the prize:dateAwarded,prizeAmount, laureates withportionandmotivation.GET https://api.nobelprize.org/2.1/laureates?nobelPrizeYear=2025&nobelPrizeCategory=phy- the same laureates as full records (birth, affiliation at the time of the award, Wikidata ID) in one call.- Also
/2.1/nobelPrize/<cat>/<year>,/2.1/laureate/<id>,/2.1/laureates?name=Curie. Docs: https://www.nobelprize.org/about/developer-zone-2/ ; OpenAPI spec JSON: https://api.swaggerhub.com/apis/NobelMedia/NobelMasterData/2.1
- Category codes are lowercase:
phy,che,med,lit,pea,eco. Anything else (physics,Phy) is dropped without an error and you get every category of that year (block 2, part 4). Check thatmetaechoes the filter, as the blocks do. - Amounts are SEK per full prize. The API has no currency field; nobelprize.org's prize money page (https://www.nobelprize.org/prizes/about/the-nobel-prize-money/) gives the amounts in Swedish kronor. 2025: 11,000,000. 2026: 12,000,000.
- "Latest" is not 2025 any more. The 2026 prizes are in the API (Physics 2026-10-06, Francis Halzen). Pass the year you mean, or ask for
sort=desc&limit=1(block 1, part 3). - Keep load low. The terms ask apps to minimize API load and suggest a local cache; responses are cached 60 s. Two calls answer the whole task.
- Browser or plain HTTP. Block 1 opens a small API URL so
fetch()is same-origin; blocks 2 and 3 need no browser.
What works
1. Who won, the motivation, the prize amount, plus each laureate's affiliation (browser)
Self-contained. 1 page load + 3 calls. get_json() returns parsed JSON on 200, backs off on 429/5xx and otherwise raises, so a failure never looks like "no prize". checked() raises when the API ignored a filter.
import json, time
YEAR, CATEGORY = 2025, "phy" # category codes: phy, che, med, lit, pea, eco (lowercase; anything else is ignored, see Gotchas)
API = "https://api.nobelprize.org/2.1/"
PAUSE = 1.0 # no published rate limit; the terms ask apps to minimize load, so keep calls serial and few
def on_api_tab():
"""fetch() runs from a logged-out tab on api.nobelprize.org (a small API URL), so every call is same-origin. Reuse the tab if already there."""
try:
if js("location.origin") == "https://api.nobelprize.org":
return
except Exception:
pass
new_tab(API + "nobelPrizes?limit=1")
wait_for_load()
if js("location.origin") != "https://api.nobelprize.org" or "json" not in (js("document.contentType") or ""):
raise RuntimeError(f"tab landed on {js('location.href')} ({js('document.contentType')}): stop, do not retry around it")
# js() gives up after a few seconds, so start the fetch, then poll for it.
START_JS = """(() => { window.__np = window.__np || {}; const id = String(Math.random()).slice(2); window.__np[id] = null;
fetch(%s, {credentials: 'omit'})
.then(async r => { window.__np[id] = {status: r.status, type: r.headers.get('content-type'), text: await r.text()}; })
.catch(e => { window.__np[id] = {status: 0, type: '', text: String(e)}; });
return id; })()"""
POLL_JS = """(() => { const v = window.__np[%s]; if (v) delete window.__np[%s]; return v; })()"""
def get_json(url, tries=3, timeout=60):
"""GET url from inside the tab -> parsed JSON. Backs off 30 s on 429/5xx, raises on anything else, so a failure never looks like "no prize"."""
for attempt in range(tries):
rid = json.dumps(js(START_JS % json.dumps(url)))
t0 = time.time()
while (r := js(POLL_JS % (rid, rid))) is None:
if time.time() - t0 > timeout:
raise RuntimeError(f"{url}: no answer after {timeout}s")
time.sleep(0.3)
time.sleep(PAUSE)
if r["status"] == 200 and "json" in (r["type"] or ""):
return json.loads(r["text"])
if r["status"] not in (0, 429, 500, 502, 503, 504) or attempt == tries - 1:
raise RuntimeError(f"{url}: HTTP {r['status']} {r['type']} {' '.join(r['text'].split())[:200]!r}; stopping")
time.sleep(30)
def checked(d, **want):
"""The API silently drops a parameter it does not understand (e.g. category 'physics' or 'Phy') and returns everything.
meta echoes only the parameters it applied, so compare."""
for k, v in want.items():
if str(d["meta"].get(k)) != str(v):
raise ValueError(f"API ignored {k}={v!r} (meta: {d['meta']}); use a lowercase code: phy che med lit pea eco")
return d
name = lambda x: (x.get("knownName") or x.get("orgName") or {}).get("en")
on_api_tab()
# 1. The prize: date, amount, laureates, share, motivation
d = checked(get_json(API + f"nobelPrizes?nobelPrizeYear={YEAR}&nobelPrizeCategory={CATEGORY}"), nobelPrizeYear=YEAR, nobelPrizeCategory=CATEGORY)
if d["meta"]["count"] != 1:
raise ValueError(f"expected one prize, got {d['meta']}")
p = d["nobelPrizes"][0]
print(f"{p['categoryFullName']['en']} {p['awardYear']} | announced {p.get('dateAwarded')} | prize amount {p['prizeAmount']:,} SEK "
f"(adjusted {p['prizeAmountAdjusted']:,}) | {len(p.get('laureates', []))} laureates")
if "topMotivation" in p:
print("top motivation:", p["topMotivation"]["en"])
for l in p.get("laureates", []):
print(f" {l['sortOrder']}. {name(l)} (id {l['id']}) share {l['portion']}")
motivations = sorted({l["motivation"]["en"] for l in p.get("laureates", [])})
print("motivation:" if len(motivations) == 1 else "motivations:", *[f'"{m}"' for m in motivations])
# 2. One call for all laureates' full records: affiliation at the time of the award, birth
d = checked(get_json(API + f"laureates?nobelPrizeYear={YEAR}&nobelPrizeCategory={CATEGORY}"), nobelPrizeYear=YEAR, nobelPrizeCategory=CATEGORY)
print(f"\nlaureates endpoint, same filter: count {d['meta']['count']}")
if len(d["laureates"]) != d["meta"]["count"]: # one prize has at most 3 laureates and a page holds 25, so this never pages
raise ValueError(f"got {len(d['laureates'])} of {d['meta']['count']} laureates; page with offset")
for l in d["laureates"]:
prize = next(x for x in l["nobelPrizes"] if x["awardYear"] == str(YEAR) and x["category"]["en"] == p["category"]["en"])
aff = "; ".join(f"{a['name']['en']}, {a.get('city', {}).get('en', '?')}, {a.get('country', {}).get('en', '?')}" for a in prize.get("affiliations", []))
born = l.get("birth", {})
when = born.get("date", "?").replace("-00-00", "") # only the year is known for many recent laureates: "1942-00-00"
where = born.get("place", {}).get("locationString", {}).get("en", "place not listed")
print(f" {name(l)}: born {when}, {where} | {prize['prizeStatus']} | {aff}")
print(f" wikidata {l.get('wikidata', {}).get('id')} | {next((x['href'] for x in l['links'] if x['rel'] == 'external'), '')}")
# 3. Newest prize in the category, to see whether a later year is out
d = checked(get_json(API + f"nobelPrizes?nobelPrizeCategory={CATEGORY}&sort=desc&limit=1"), nobelPrizeCategory=CATEGORY)
n = d["nobelPrizes"][0]
print(f"\nnewest {n['category']['en']} prize in the API: {n['awardYear']} ({n.get('dateAwarded')}): {', '.join(name(l) for l in n.get('laureates', []))} "
f"| {n['prizeAmount']:,} SEK | {d['meta']['count']} {n['category']['en']} prize years listed")Output (from the run on 2026-11-09):
The Nobel Prize in Physics 2025 | announced 2025-10-07 | prize amount 11,000,000 SEK (adjusted 11,000,000) | 3 laureates
1. John Clarke (id 1050) share 1/3
2. Michel H. Devoret (id 1051) share 1/3
3. John M. Martinis (id 1052) share 1/3
motivation: "for the discovery of macroscopic quantum mechanical tunnelling and energy quantisation in an electric circuit"
laureates endpoint, same filter: count 3
John Clarke: born 1942, Cambridge, United Kingdom | received | University of California, Berkeley, CA, USA
wikidata Q555849 | https://www.nobelprize.org/laureate/1050
John M. Martinis: born 1958, place not listed | received | University of California, Santa Barbara, CA, USA; Qolab, Los Angeles, CA, USA
wikidata Q28976188 | https://www.nobelprize.org/laureate/1052
Michel H. Devoret: born 1953, Paris, France | received | Yale University, New Haven, CT, USA; University of California, Santa Barbara, CA, USA; Google Quantum AI, Santa Barbara, CA, USA
wikidata Q26703310 | https://www.nobelprize.org/laureate/1051
newest Physics prize in the API: 2026 (2026-10-06): Francis Halzen | 12,000,000 SEK | 126 Physics prize years listed2. Search laureates by name, every prize of one year, a year with no prize (plain HTTP, no browser)
Python standard library only, truthful User-Agent, 5 calls at 1 s spacing. The name search pages with offset until it has meta.count laureates (25 per page; "John" matched 43, 2 pages); motivations are cut to 90 characters with ... for display.
import json, time, urllib.error, urllib.parse, urllib.request
NAME = "Curie" # part of a person's or organization's name
YEAR = 2025 # every prize of one year
UA = {"User-Agent": "browser-harness-skill/1.0 (Python urllib)"}
API = "https://api.nobelprize.org/2.1/"
def call(path, **params):
"""GET -> parsed JSON. Waits 30 s on 429/5xx, then stops. Serial, one call per second (the terms ask apps to keep load low)."""
url = API + path + ("?" + urllib.parse.urlencode(params) if params else "")
for attempt in range(3):
try:
with urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=60) as r:
status, body, ctype = r.status, r.read(), r.headers.get("content-type", "")
except urllib.error.HTTPError as e:
status, body, ctype = e.code, e.read(), e.headers.get("content-type", "")
time.sleep(1)
if status == 200 and "json" in ctype:
return json.loads(body)
if status not in (429, 500, 502, 503, 504) or attempt == 2:
raise RuntimeError(f"{url}: HTTP {status} {ctype} {body[:200]!r}; stopping")
time.sleep(30)
name = lambda x: (x.get("knownName") or x.get("orgName") or {}).get("en")
short = lambda s, n=90: s if len(s) <= n else s[:n - 3].rstrip() + "..." # preview; the full text is in the record
# 1. Laureates whose name matches, with every prize they won (25 per page by default; page with offset until meta.count)
d = call("laureates", name=NAME)
while len(d["laureates"]) < d["meta"]["count"]:
page = call("laureates", name=NAME, offset=len(d["laureates"]))
if not page["laureates"]:
raise RuntimeError(f"paging stopped at {len(d['laureates'])} of {d['meta']['count']}")
d["laureates"] += page["laureates"]
print(f"laureates matching {NAME!r}: {d['meta']['count']}")
for l in d["laureates"]:
kind = "organization" if "orgName" in l else l.get("gender", "?")
print(f" {name(l)} (id {l['id']}, {kind}, wikidata {l.get('wikidata', {}).get('id')})")
for p in l["nobelPrizes"]:
aff = "; ".join(a["name"]["en"] for a in p.get("affiliations", []))
print(f" {p['awardYear']} {p['category']['en']:<10} share {p['portion']:<4} {p['prizeAmount']:>9,} SEK {short(p['motivation']['en'])}" + (f" | {aff}" if aff else ""))
# 2. Every prize in one year
d = call("nobelPrizes", nobelPrizeYear=YEAR)
print(f"\n{YEAR}: {d['meta']['count']} prizes")
for p in d["nobelPrizes"]:
who = ", ".join(f"{name(l)} ({l['portion']})" for l in p.get("laureates", []))
print(f" {p.get('dateAwarded')} {p['category']['en']:<22} {p['prizeAmount']:,} SEK {who}")
# 3. A year with no prize: no laureates, a topMotivation instead
d = call("nobelPrizes", nobelPrizeYear=1940, nobelPrizeCategory="phy")
p = d["nobelPrizes"][0]
print(f"\n1940 {p['category']['en']}: laureates {len(p.get('laureates', []))} | {p['topMotivation']['en']}")
# 4. Things that look like answers but are not
d = call("nobelPrizes", nobelPrizeYear=YEAR, nobelPrizeCategory="physics") # not a category code
print(f"\ncategory 'physics': count {d['meta']['count']}, meta keeps nobelPrizeCategory? {'nobelPrizeCategory' in d['meta']} "
f"-> first result is {d['nobelPrizes'][0]['category']['en']}")
d = call("laureate/99999") # an ID that does not exist
print("laureate/99999:", "no record, only meta" if d and set(d[0]) == {"meta"} else d)Output (from the run on 2026-11-09):
laureates matching 'Curie': 3
Irène Joliot-Curie (id 194, female, wikidata Q7504)
1935 Chemistry share 1/2 159,917 SEK in recognition of their synthesis of new radioactive elements | Institut du Radium
Marie Curie (id 6, female, wikidata Q7186)
1903 Physics share 1/4 141,358 SEK in recognition of the extraordinary services they have rendered by their joint research...
1911 Chemistry share 1 140,695 SEK in recognition of her services to the advancement of chemistry by the discovery of the... | Sorbonne University
Pierre Curie (id 5, male, wikidata Q37463)
1903 Physics share 1/4 141,358 SEK in recognition of the extraordinary services they have rendered by their joint research... | École municipale de physique et de chimie industrielles (Municipal School of Industrial Physics and Chemistry)
2025: 6 prizes
2025-10-08 Chemistry 11,000,000 SEK Susumu Kitagawa (1/3), Richard Robson (1/3), Omar M. Yaghi (1/3)
2025-10-13 Economic Sciences 11,000,000 SEK Joel Mokyr (1/2), Philippe Aghion (1/4), Peter Howitt (1/4)
2025-10-09 Literature 11,000,000 SEK László Krasznahorkai (1)
2025-10-10 Peace 11,000,000 SEK Maria Corina Machado (1)
2025-10-07 Physics 11,000,000 SEK John Clarke (1/3), Michel H. Devoret (1/3), John M. Martinis (1/3)
2025-10-06 Physiology or Medicine 11,000,000 SEK Mary E. Brunkow (1/3), Fred Ramsdell (1/3), Shimon Sakaguchi (1/3)
1940 Physics: laureates 0 | No Nobel Prize was awarded this year. 1/3 of the prize money was allocated to the main fund and 2/3 was allocated to the special fund of this prize section.
category 'physics': count 6, meta keeps nobelPrizeCategory? False -> first result is Chemistry
laureate/99999: no record, only meta3. Same task with curl, and the CSV format
curl -sS --fail "https://api.nobelprize.org/2.1/nobelPrizes?nobelPrizeYear=2025&nobelPrizeCategory=phy" | python3 -c '
import json, sys
d = json.load(sys.stdin)
if d["meta"].get("nobelPrizeCategory") != "phy": # an unknown category is dropped silently
raise SystemExit("API ignored nobelPrizeCategory=phy: " + json.dumps(d["meta"]))
for p in d["nobelPrizes"]:
print(p["categoryFullName"]["en"], p["awardYear"], "|", p["dateAwarded"], "|", format(p["prizeAmount"], ","), "SEK")
for l in p["laureates"]:
print(" ", l["knownName"]["en"], l["portion"], "|", l["motivation"]["en"])
'
sleep 1
curl -sS -o /dev/null -w 'csv: HTTP %{http_code} %{content_type}\n' "https://api.nobelprize.org/2.1/nobelPrizes?nobelPrizeYear=2025&nobelPrizeCategory=phy&format=csv"
sleep 1
curl -sS "https://api.nobelprize.org/2.1/nobelPrizes?nobelPrizeYear=2025&nobelPrizeCategory=phy&format=csv" | head -3Output (from the run on 2026-11-09):
The Nobel Prize in Physics 2025 | 2025-10-07 | 11,000,000 SEK
John Clarke 1/3 | for the discovery of macroscopic quantum mechanical tunnelling and energy quantisation in an electric circuit
Michel H. Devoret 1/3 | for the discovery of macroscopic quantum mechanical tunnelling and energy quantisation in an electric circuit
John M. Martinis 1/3 | for the discovery of macroscopic quantum mechanical tunnelling and energy quantisation in an electric circuit
csv: HTTP 200 text/csv;charset=UTF-8
category,year,id,name,share,overallmotivation,motivation
Physics,2025,1050,"John Clarke",1/3,,"for the discovery of macroscopic quantum mechanical tunnelling and energy quantisation in an electric circuit",
Physics,2025,1051,"Michel H. Devoret",1/3,,"for the discovery of macroscopic quantum mechanical tunnelling and energy quantisation in an electric circuit",Fields
| Field | Where | Notes |
|---|---|---|
awardYear, category.en, categoryFullName.en | prize | year as a string ("2025"); Physics, The Nobel Prize in Physics; also no/se translations |
dateAwarded | prize | announcement date, 2025-10-07; absent for years with no prize |
prizeAmount | prize | SEK for the full prize in that year; each laureate gets portion of it |
prizeAmountAdjusted | prize | spec: "adjusted according to Index number yearly average"; 1940 Physics: 138,570 -> 4,464,201 |
topMotivation.en | prize | only on some prizes, e.g. 1940 Physics "No Nobel Prize was awarded this year..." |
laureates[].id, knownName.en / orgName.en | prize, laureate | persons have knownName, givenName, familyName, gender, birth; organizations have orgName, founded |
laureates[].portion, sortOrder, motivation.en | prize | share 1, 1/2, 1/3, 1/4; motivation is per laureate and can differ (Economic Sciences 2025: 1/2, 1/4, 1/4) |
nobelPrizes[].affiliations[] | laureate | name, city, country at the time of the award; can be several (Devoret: 3) or none |
nobelPrizes[].prizeStatus | laureate | received, declined, restricted (spec enum) |
birth.date, birth.place.locationString.en | laureate | 1942-00-00 when only the year is known; place can be missing (Martinis) |
wikidata.id, wikipedia.english, links[rel=external] | laureate | Q555849; the laureate's page on www.nobelprize.org |
meta.count, meta.offset, meta.limit | top level | limit defaults to 25; page with offset. meta echoes only the filters that were applied |
Gotchas
- Unknown filter values are ignored, not rejected.
nobelPrizeCategory=physicsorPhyreturns all six 2025 prizes, Chemistry first, HTTP 200. Readmeta.nobelPrizeCategory, or check every returnedcategory. - Unknown laureate ID is HTTP 200 with
[{"meta": {...}}]and no record (/2.1/laureate/99999). links[].hrefpoint at/2/(https://api.nobelprize.org/2/laureate/1050), not/2.1/. Both answered 200; build/2.1/URLs from theidyourself.- Order differs by endpoint.
nobelPrizeslists laureates bysortOrder(Clarke, Devoret, Martinis);laureatessorts alphabetically by known name (Clarke, Martinis, Devoret). Years with several prizes come back alphabetical by category, not by date. - No-prize years are still listed. 126 Physics entries for 1901-2026 include years like 1940 with no
laureatesand onlytopMotivation. Count laureates, not entries. yearToworks withoutnobelPrizeYearalthough the spec says it is required:nobelPrizes?yearTo=2025returned 688 prizes from 1901.- CSV has no amounts.
format=csvgivescategory,year,id,name,share,overallmotivation,motivationwith a trailing comma on each row. Use JSON for amounts and affiliations. - Tab marker. The harness prefixes the tab title with a horse emoji; block 1 checks only origin and content type.
What does not work
- robots.txt on the API host is a placeholder HTML page, not a robots file; there are no rules to read there.
- Not tested:
limitmaximum, rate limits (none documented, none hit),/2.1/nobelPrize/<cat>/<year>beyond one 200 call, the other laureate filters (affiliation,birthCountry,deathDate, ...), declined or restricted prizes.