GuidesARXIV
arXiv API from JavaScript
Prefix https://proxy.cors.dev/ to an export.arxiv.org API URL: free, no signup, no key, 600 requests per minute, Atom XML up to 1 MiB, which fits about 400 results per call. The arXiv API sends no Access-Control-Allow-Origin header, so fetch() from your page fails with a CORS error. Through the proxy you can search papers by title, author, abstract, category and submission date, then parse the feed with DOMParser in a few lines.
Why the arXiv API fails in the browser
The arXiv API is one endpoint, https://export.arxiv.org/api/query, that answers GET requests with an Atom 1.0 feed, errors included. It needs no key, but its responses carry no Access-Control-Allow-Origin header, so a fetch() from your site is blocked by the browser even though the same URL opens fine in a tab. Through a proxy the request runs on a server and comes back with the header set to your origin.
Search papers and list the results
Build the query with URLSearchParams so spaces, quotes, colons and brackets are encoded correctly, then read the entries with namespace-aware DOM calls. Atom elements live in http://www.w3.org/2005/Atom, the primary category in http://arxiv.org/schemas/atom and the total count in the OpenSearch namespace.
// Markup: <ol id="papers"></ol>
const ATOM = 'http://www.w3.org/2005/Atom';
const ARXIV = 'http://arxiv.org/schemas/atom';
const OPENSEARCH = 'http://a9.com/-/spec/opensearch/1.1/';
async function searchArxiv(query, { start = 0, max = 20, sortBy = 'relevance' } = {}) {
const params = new URLSearchParams({
search_query: query,
start,
max_results: max,
sortBy,
sortOrder: 'descending',
});
const response = await fetch(`https://proxy.cors.dev/https://export.arxiv.org/api/query?${params}`, {
credentials: 'omit',
});
if (!response.ok) {
throw new Error(`arXiv request failed: ${response.headers.get('X-Cors-Error') ?? response.status}`);
}
const feed = new DOMParser().parseFromString(await response.text(), 'application/xml');
if (feed.querySelector('parsererror')) throw new Error('The response is not valid XML');
const text = (node, name) => node.getElementsByTagNameNS(ATOM, name)[0]?.textContent.trim() ?? '';
const papers = [...feed.getElementsByTagNameNS(ATOM, 'entry')].map((entry) => ({
id: text(entry, 'id').split('/abs/')[1],
title: text(entry, 'title').replace(/\s+/g, ' '),
authors: [...entry.getElementsByTagNameNS(ATOM, 'author')].map((author) => text(author, 'name')),
summary: text(entry, 'summary').replace(/\s+/g, ' '),
published: text(entry, 'published').slice(0, 10),
category: entry.getElementsByTagNameNS(ARXIV, 'primary_category')[0]?.getAttribute('term'),
}));
return { total: Number(feed.getElementsByTagNameNS(OPENSEARCH, 'totalResults')[0]?.textContent), papers };
}
const list = document.querySelector('#papers');
searchArxiv('cat:cs.AI AND ti:agent', { max: 10, sortBy: 'submittedDate' })
.then(({ total, papers }) => {
list.replaceChildren(
...papers.map((paper) => {
const item = document.createElement('li');
const link = document.createElement('a');
link.href = `https://arxiv.org/abs/${paper.id}`;
link.textContent = paper.title;
item.append(link, ` (${paper.published}, ${paper.authors.slice(0, 3).join(', ')})`);
return item;
}),
);
console.log(`${total} papers match`);
})
.catch((error) => {
list.textContent = `Could not load papers: ${error.message}`;
});Each entry id is the abstract URL with a version suffix, such as http://arxiv.org/abs/1706.03762v7. Link readers to the abstract or PDF page rather than fetching the PDF: the free tier relays text only and answers 502 unsupported_response for a PDF.
Query syntax
search_query takes field prefixes joined with AND, OR and ANDNOT. Use id_list for papers you already know, and sortBy with sortOrder for newest first; the default order is relevance.
| Goal | Parameter | Note |
|---|---|---|
| Words in the title | search_query=ti:transformer | abs: searches abstracts, co: comments |
| Papers by an author | search_query=au:bengio | Surname; join two-part names with _ |
| An exact phrase | search_query=ti:"large language model" | Quotes are sent as %22 |
| A subject category | search_query=cat:cs.AI | Any arXiv category, such as math.CO or q-bio.NC |
| Combine fields | search_query=au:bengio AND cat:cs.LG | AND, OR and ANDNOT, group with ( ) |
| Leave something out | search_query=cat:cs.CL ANDNOT ti:survey | ANDNOT filters the left side |
| A submission date range | search_query=cat:cs.AI AND submittedDate:[202601010000 TO 202602010000] | YYYYMMDDHHMM in GMT |
| Known papers | id_list=1706.03762,1810.04805 | Latest version unless you add v1, v2 |
| Newest first | sortBy=submittedDate&sortOrder=descending | Or lastUpdatedDate, relevance |
| Paging | start=0&max_results=100 | Default 10, arXiv maximum 2,000 per call |
Paging and arXiv rate limits
start and max_results page through a result set. arXiv allows up to 2,000 results per call and 30,000 in total, but on the free tier 1 MiB is the real ceiling: 400 recent cs.AI entries came to 999,810 bytes, and 500 returned 502 response_too_large. Pages of 100 to 200 are a comfortable size.
arXiv's terms ask for no more than one request every three seconds and one connection at a time, counted across everything you run. Results only change when new papers are announced once a day, so cache them instead of repeating a query.
// Uses searchArxiv() from the example above, in a module script or the DevTools console
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function collect(query, { pageSize = 200, limit = 1000 } = {}) {
const papers = [];
for (let start = 0; start < limit; start += pageSize) {
const page = await searchArxiv(query, { start, max: pageSize, sortBy: 'submittedDate' });
papers.push(...page.papers);
if (page.papers.length < pageSize || start + pageSize >= page.total) break;
await sleep(3000);
}
return papers.slice(0, limit);
}
const recent = await collect('cat:cs.CL', { pageSize: 200, limit: 600 });
console.log(recent.length, recent[0]);Category feeds such as https://rss.arxiv.org/rss/cs.AI also work through the proxy for a daily list of new papers, but they have no new papers on Saturdays and Sundays, when arXiv announces nothing. See fetch an RSS feed in JavaScript for parsing RSS.
- cors.dev free tier: GET and HEAD, 600 requests per minute per client IP, no daily or monthly quota, text responses up to 1 MiB and 10 seconds for arXiv to answer. A 400-result query took about a second.
- [Pro](/pricing): $5 per month for 500,000 requests, with a 7-day trial of 1,000 requests and no card. Responses up to 6 MiB of any content type.
Good questions.
Does the arXiv API need an API key?
No. export.arxiv.org/api/query is open to anyone who follows the arXiv API terms of use. The only obstacle in a browser is the missing CORS header.
How many results can I get per request?
arXiv returns up to 2,000 per call and 30,000 per query in total. Through the cors.dev free tier the 1 MiB response limit comes first, at about 400 entries, so page with start and max_results to get more.
How often may I call the arXiv API?
arXiv asks for at most one request every three seconds, one connection at a time, across all machines you control. New papers appear once a day, so the same query never needs to run more than daily.
How do I get the newest papers first?
Add sortBy=submittedDate&sortOrder=descending. Without them arXiv sorts by relevance, which is why API results look out of order next to the arxiv.org listing pages.
How do I search for an exact phrase?
Wrap it in double quotes inside the field, for example ti:"large language model", and let URLSearchParams encode it. Spaces become + and quotes become %22, which is what arXiv expects.
Can I download PDFs through the proxy?
Not on the free tier, which relays text responses only and returns 502 unsupported_response for a PDF. Link to the arxiv.org abstract or PDF page instead; arXiv also asks that you send readers there rather than serve copies yourself.