Article type: How-to guide (one task: turn a research topic or a list of DOIs into a CSV of papers with citation counts and open access links)
Prerequisites: Python 3.9+ with
requests, and your email address. An OpenAlex API key is optional (free). An Apify account only if you use the hosted version at the end.Tested on: 14 November 2026, against the live API.
If you need a list of papers on a topic for a literature review, a reading list or a dataset, OpenAlex is a good place to start. It's an open index of scholarly works with a free API, and all OpenAlex data is released under CC0 (public domain), so you can keep, share and build on what you pull.
This guide sizes a search before you pull it, writes the most cited open access articles to a CSV, looks up a list of DOIs in one request, and covers the gotchas we hit in the results.
Step 1: Know what the API costs (in budget, not money)
OpenAlex's data is free, and so is casual use of the API. Each call draws from a daily usage budget, measured in dollars but not billed:
| Call | Example | Budget used |
|---|---|---|
| Get one record | /works/doi:10.1038/nature14539 | free |
| List with filters | /works?filter=publication_year:2024 | $0.0001 |
| Search | /works?search=... | $0.001 |
Without a key, you share $0.10 a day with everyone on your IP address, so about 100 searches. Adding your email as mailto doesn't raise that. A free API key gives you your own $1 a day. The budget resets at midnight UTC, and every response tells you what it cost in the x-ratelimit-cost-usd header. Everything in this post used $0.0041.
export OPENALEX_EMAIL="you@example.com" export OPENALEX_API_KEY="your-key" # optional Step 2: Size the search, then pull it
The script counts the matches two ways, then pulls the most cited open access articles in one request (100 rows is the most a page returns). Save as openalex_papers.py:
import csv, os, time, requests API = "https://api.openalex.org/works" # Your email (OpenAlex asks you to identify yourself) and, optionally, your free API key. BASE = {"mailto": os.environ["OPENALEX_EMAIL"]} if os.environ.get("OPENALEX_API_KEY"): BASE["api_key"] = os.environ["OPENALEX_API_KEY"] FIELDS = "id,doi,display_name,publication_year,type,cited_by_count,open_access,primary_location,best_oa_location" spent = 0.0 def get(url, **params): global spent r = requests.get(url, params={**BASE, **params}, timeout=60) r.raise_for_status() spent += float(r.headers.get("x-ratelimit-cost-usd", 0)) time.sleep(0.25) return r.json() def count(**params): return get(API, per_page=1, select="id", **params)["meta"]["count"] def row(w): loc = w.get("primary_location") or {} oa = w.get("best_oa_location") or {} return { "openalex_id": w["id"].rsplit("/", 1)[-1], "doi": (w.get("doi") or "").replace("https://doi.org/", ""), "title": w["display_name"], "year": w["publication_year"], "venue": ((loc.get("source") or {}).get("display_name")) or "", "cited_by": w["cited_by_count"], "oa_status": w["open_access"]["oa_status"], "oa_url": w["open_access"].get("oa_url") or "", "pdf_url": oa.get("pdf_url") or "", "licence": oa.get("license") or "", } if __name__ == "__main__": topic = "microplastics drinking water" years = "publication_year:2023-2026" # 1. Size the search two ways before you pull anything. print("search= (full text too): ", count(search=topic, filter=years)) print("title_and_abstract.search: ", count(filter=f"title_and_abstract.search:{topic},{years}")) print(" ...open access articles: ", count(filter=f"title_and_abstract.search:{topic},{years},is_oa:true,type:article")) # 2. Pull the most cited open access articles into a CSV (one request, up to 100 rows). data = get(API, filter=f"title_and_abstract.search:{topic},{years},is_oa:true,type:article", sort="cited_by_count:desc", per_page=100, select=FIELDS) rows = [row(w) for w in data["results"]] with open("papers.csv", "w", newline="") as f: w = csv.DictWriter(f, fieldnames=list(rows[0])) w.writeheader() w.writerows(rows) print(f"\nwrote {len(rows)} rows to papers.csv; top 3:") for r in rows[:3]: print(f' {r["year"]} {r["cited_by"]:>4} cites {r["oa_status"]:<7} {r["title"][:70]}') # 3. Look up a list of DOIs in one filter request. dois = ["10.1038/nature14539", "10.1126/science.1225829", "10.1038/s41586-020-2649-2", "10.9999/not-a-real-doi"] found = get(API, filter="doi:" + "|".join(dois), per_page=100, select=FIELDS)["results"] print(f"\nDOI lookup: {len(found)} of {len(dois)} found") for r in map(row, found): print(f' {r["doi"]:<28} {r["year"]} {r["cited_by"]:>6} cites {r["oa_status"]:<7} {r["title"][:45]}') missing = set(d.lower() for d in dois) - {r["doi"].lower() for r in map(row, found)} print(" not found:", sorted(missing)) print(f"\nOpenAlex usage for this script: ${spent:.4f}") python openalex_papers.py Output on 14 November 2026:
search= (full text too): 17759 title_and_abstract.search: 1412 ...open access articles: 633 wrote 100 rows to papers.csv; top 3: 2023 160 cites hybrid Identification of Poly(ethylene terephthalate) Nanoplastics in Commerc 2024 117 cites gold Microplastics contamination in food products: Occurrence, analytical t 2023 114 cites hybrid 3D Plasmonic Gold Nanopocket Structure for SERS Machine Learning‐Based DOI lookup: 3 of 4 found 10.1038/nature14539 2015 90226 cites green Deep learning 10.1038/s41586-020-2649-2 2020 30359 cites hybrid Array programming with NumPy 10.1126/science.1225829 2012 17935 cites green A Programmable Dual-RNA–Guided DNA Endonuclea not found: ['10.9999/not-a-real-doi'] OpenAlex usage for this script: $0.0041 The first two rows of papers.csv:
openalex_id,doi,title,year,venue,cited_by,oa_status,oa_url,pdf_url,licence W4377691692,10.1021/acs.est.3c00842,Identification of Poly(ethylene terephthalate) Nanoplastics in Commercially Bottled Drinking Water Using Surface-Enhanced Raman Spectroscopy,2023,Environmental Science & Technology,160,hybrid,https://pubs.acs.org/doi/pdf/10.1021/acs.est.3c00842,https://pubs.acs.org/doi/pdf/10.1021/acs.est.3c00842,cc-by W4392094690,10.1016/j.crbiot.2024.100190,"Microplastics contamination in food products: Occurrence, analytical techniques and potential impacts on human health",2024,Current Research in Biotechnology,117,gold,https://doi.org/10.1016/j.crbiot.2024.100190,,cc-by-nc-nd Why the script is built this way:
-
select=keeps the response small. You get only the fields you put in the CSV. -
per_page=1to count. A count call returnsmeta.countwithout pulling the rows. - One filter request for many DOIs.
filter=doi:a|b|creturns them all at once, for a fraction of the budget of separate searches. (Single lookups such as/works/doi:10.1038/nature14539are free, so they also work for a short list.) - The script adds up
x-ratelimit-cost-usd. You see what a run used before you schedule it.
Gotchas from the results
-
search=also searches full text. The same topic and years matched 17,759 works withsearch=, but 1,412 withtitle_and_abstract.search. If you want papers about the topic, filter on title and abstract. The big number includes papers that mention your words anywhere in the text. - Missing DOIs just disappear. The DOI filter returned 3 rows for 4 DOIs, with no error for the made-up one. Compare what you sent with what came back, as the script does.
- Half the open access rows have no PDF link. 50 of the 100 rows had no
pdf_url. They all have anoa_url(a free-to-read page), so keep both columns. - Open access doesn't mean free to reuse. In the 100 rows: 49 were CC BY, 26 CC BY-NC-ND, 7 CC BY-NC, 1 CC BY-NC-SA, 3 "other-oa" and 14 had no licence recorded. If you plan to copy text or figures, read the
licencecolumn. (The metadata itself is CC0; the papers have their own licences.) - Sorting by citations buries new work. The top 100 by citations had 46 papers from 2023, 40 from 2024, 14 from 2025 and none from 2026. For new papers, sort by
publication_date:descinstead. - Deep paging needs a key. Plain
page=paging stops at 10,000 results, and followingnext_cursorbeyond the first page returns a 401 without a key. For a whole field, look at OpenAlex's free snapshot rather than paging the API. - DOIs come back as URLs. Lookups accept
doi:10.1038/NATURE14539,doi:10.1038/nature14539or the fullhttps://doi.org/10.1038/nature14539, and every one returnshttps://doi.org/10.1038/nature14539. The DOI filter matched the upper-case form too. Normalise to lower case without the prefix before you join to your own list.
To change the search, edit topic, the years and the filter strings.
The hosted version (Disclosure: we built this)
We built an Apify Actor, OpenAlex API: Academic Papers & Crossref Search, for when you want more than one source. It searches OpenAlex and Crossref in one run and merges the same paper found in both into one row (matched by DOI, or exact title and year), with foundIn listing the sources. Papers found only in Crossref get citation counts and open access links from OpenAlex. It has three modes: topic search with year and open access filters, DOI lookup for up to 500 DOIs, and author search by name or ORCID iD. Unknown or invalid DOIs come back as free status rows instead of going missing. Abstracts are included only when an openly available version carries a CC0, CC BY, CC BY-SA or public-domain licence. Monitor mode returns only papers it hasn't returned before. Its topic search uses each source's own search, which for OpenAlex includes full text, as in gotcha 1. It needs no key; you can add your own OpenAlex key for large or frequent runs. Export as CSV, JSON or Excel. For example (not run for this post):
{ "mode": "search", "query": "microplastics drinking water", "yearFrom": 2023, "openAccessOnly": true, "sortBy": "mostCited", "maxResultsPerSource": 100, "maxResults": 100 } It's pay per event: $0.002 per paper returned (base price; higher Apify plans pay less), $0.01 per monitor run that finds new papers, and Apify's $0.00005 Actor start per run (per GB of memory; a default run counts once). Status rows are free. 100 papers would cost 100 × $0.002 + $0.00005 = $0.20005. For one source and one query, the script above is the cheaper choice.
OpenAlex API: Academic Papers & Crossref Search on Apify
Data: OpenAlex (openalex.org), CC0. This post and the Actor are not affiliated with or endorsed by OpenAlex or OurResearch.
Your Evenings
![Embedded Software Engineering — Ch.10: OTA Firmware Update [EN]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm6nak2soahu0o1ww35hy.png)
![Embedded Software Engineering — Ch.11: Embedded Security [EN]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8lj1zci0ucpfvn91aqd5.png)


![Embedded Software Engineering — Ch.12: Embedded Case Studies [EN]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fth5r2cczjsot6ae55lq5.png)