Querying

OQL API

Note: The OpenAlex Query Language is in alpha. It may change without warning — build against it at your own risk, and tell us what you think.

Everything OQL does is available over plain HTTP — no GUI required. There are two endpoint families, and they divide cleanly:

  • Execute — send a query, get entity results back. Lives at the API root: GET /?oql=…, GET /?oqo=…, or POST / with a JSON body.
  • Translate — convert a query between its three forms (OQL text, OQO JSON, classic URL) and validate it, without running it. Lives at /query.

Every response also carries meta.x_query, which echoes your query back in all three forms — so any result you get is also a lesson in how to write the next query.

Every example on this page runs on production today. Counts are live and will drift.


The 30-second version

# Run an OQL query (the API root, not /works — the query names its own entity):
curl -G "https://api.openalex.org/" --data-urlencode "oql=works where year is (2020)"

# Translate a query between forms without running it:
curl "https://api.openalex.org/query/oql/works%20where%20year%20is%20(2020)"

# Long query? POST it — no length limit:
curl -X POST "https://api.openalex.org/" \
  -H "Content-Type: application/json" \
  -d '{"oql": "works where year is (2020)"}'

The rest of this page is detail: each surface, its parameters, its response shape, and its errors.


The three query formats

Every query can be written three ways. They share one meaning but differ in syntax and in how much they can express:

Format Looks like Reach for it when
Classic URL /works?filter=publication_year:2020 you want backwards compatibility with the API you already know
OQL works where year is (2020) you’re writing most applications — it reads and writes like a sentence
OQO {"get_rows": "works", "filter_rows": […]} you’re building queries from a program or an agent — it’s JSON with a concrete, validatable schema

How they relate:

  • OQL and OQO are two faces of the same thing. Every OQL query has exactly one OQO and vice versa — same semantics, different syntax. OQO is the canonical internal representation: everything you send, in any format, is compiled to an OQO before it runs.
  • The classic URL is less expressive. Every URL maps to a corresponding OQL and OQO, but not the reverse — there are many OQL/OQO queries (rich boolean nesting, proximity, grouping) that no classic URL can express. That’s deliberate: OQL/OQO widen the space of questions you can ask, so you can say exactly what you mean with more power and nuance.

So: use the classic URL for legacy compatibility, OQL for most applications, and OQO when an agent or program needs a JSON schema to validate against. The /query endpoint converts freely between all three, and every result echoes all three back in meta.x_query.


Running a query

GET /?oql=

Put a URL-encoded OQL string in the oql parameter at the API root:

https://api.openalex.org/?oql=works where year is (2020)
curl -G "https://api.openalex.org/" \
  --data-urlencode "oql=works where title has (cancer) and year >= (2020)" \
  --data-urlencode "per-page=5"

Two things to know:

  • The root, not /works. An OQL query names its own entity (works where …, authors where …), so there’s one execution endpoint for all entities. GET /works?oql=… is an error (400, “oql is not a valid parameter”).
  • The response is the standard envelope — the same {meta, results} (or {meta, group_by}) you get from /works?filter=…, with the same entity objects. Existing client code doesn’t need to change.

The paging parameters work alongside oql:

Parameter Works with ?oql= Notes
per-page, page yes basic paging
cursor yes deep paging; start with cursor=*, follow meta.next_cursor
sort yes classic sort=column:direction (comma-separated for tiebreakers), e.g. sort=cited_by_count:desc
select yes classic select=field,field to project a subset of fields
api_key yes or send Authorization: Bearer <key> (see Auth below)

Sorting, field selection, and paging are view parameters, not part of the query (the #661 query/view split): the OQO/OQL describes which rows, and sort, select, page, per-page, and cursor travel as siblings of it — the classic query-string parameters above, on either verb, or top-level body keys on POST (below). A sibling always wins over a value still embedded inside an OQO (the deprecated pre-#661 shape, accepted transitionally).

Grouping is part of the language — write it in the query:

curl -G "https://api.openalex.org/" \
  --data-urlencode "oql=works where year is (2020) group by type"

…and the response comes back with a group_by array instead of results.

GET /?oqo=

OQO (the OpenAlex Query Object) is the JSON form of the same query — what OQL compiles to, and the natural form when a program is building queries. Send it URL-encoded in the oqo parameter, again at the root:

curl -G "https://api.openalex.org/" \
  --data-urlencode 'oqo={"get_rows": "works", "filter_rows": [{"column_id": "publication_year", "value": 2020}]}' \
  --data-urlencode "per-page=1"

The OQO carries the query itself — filters, grouping, and sampling (sample/seed live inside the object; a sampled corpus is a different row set). Sorting, projection, and paging are not in the object: put them in the URL parameters as above. See “The OQO object” below for the shape.

POST /

GET puts the whole query in the request line, which is capped at 8,190 bytes — a big Boolean query can blow past that. POST / takes the same query in a JSON body, with no length limit:

curl -X POST "https://api.openalex.org/?per-page=5" \
  -H "Content-Type: application/json" \
  -d '{"oql": "works where year is (2020)"}'

or with an OQO, view params riding as sibling body keys:

curl -X POST "https://api.openalex.org/" \
  -H "Content-Type: application/json" \
  -d '{"oqo": {"get_rows": "works",
               "filter_rows": [{"column_id": "publication_year", "value": 2020}]},
       "sort": "cited_by_count:desc",
       "select": "id,display_name,cited_by_count",
       "per_page": 5}'

Rules of the road:

  • The body is a JSON object with exactly one of "oql" or "oqo", plus optionally the sibling view params sort / select / page / per_page / cursor. Any other top-level key is a 400 (invalid_body), and so is sending both oql and oqo. Content-Type: application/json is required (without it: 400, invalid_body).
  • Sibling view params use the same classic syntax as the query-string form: sort is a string like "cited_by_count:desc,display_name", select a comma-string (a JSON list of column names also works in the body), page/per_page integers, cursor a string. Body siblings win over query-string siblings, which win over anything still embedded in the OQO.
  • sample/seed belong inside the OQO — they change which rows you get, not how they’re presented; {"oql": "…", "sample": 5} is rejected.

Errors

A query that doesn’t parse or validate returns 400 with a structured validation object — never a silent empty result:

curl -G "https://api.openalex.org/" --data-urlencode "oql=works where yearx is (2020)"
{
  "validation": {
    "valid": false,
    "errors": [
      {
        "type": "parse_error",
        "message": "unknown field \"yearx\"  Fix: check the field name against the properties registry",
        "position": 12,
        "context": "works where yearx is (2020)",
        "location": null
      }
    ],
    "warnings": []
  }
}

type is one of parse_error, bad_request, invalid_entity, invalid_body. Parse errors include the character position and the offending context, and the message usually ends with a concrete Fix:.

Auth and cost

No API key is required — these endpoints follow the same rules as the rest of the API: a free-account key raises your daily credit budget, and a query here costs the same credits as its classic-URL equivalent (the response’s meta.cost_usd shows what each call cost). Authenticate with either form:

curl -G "https://api.openalex.org/" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  --data-urlencode "oql=works where year is (2020)"

or append &api_key=YOUR_API_KEY.


Translating a query: the /query endpoint

The same query has three interchangeable forms (see The three query formats above for when to use which):

Form Looks like Best for
oql works where year is (2020) humans reading and writing
oqo {"get_rows": "works", "filter_rows": […]} programs and agents building queries
oxurl /works?filter=publication_year:2020 the classic API you already know

/query converts any form into all the others, and validates it — without executing anything (it never touches the search index, and costs nothing). It’s the machinery behind the GUI’s OQL tab, and it’s how a tool of your own can offer OQL input or output.

Because OQL and OQO are equally expressive, translating between them always round-trips cleanly. Translating into oxurl can fail, though: the classic URL grammar can’t represent every OQL/OQO query (deeply nested booleans, proximity search, some grouping). When that happens /query returns the oql/oqo/validation fields and sets oxurl to null with a note — the query is still perfectly valid and runnable, it just has no URL spelling.

GET /query/oql, /query/oqo, /query/oxurl

Put the query in the path (URL-encoded or plain):

curl "https://api.openalex.org/query/oql/works%20where%20year%20is%20(2020)"
curl "https://api.openalex.org/query/oxurl/works?filter=publication_year:2020"
curl "https://api.openalex.org/query/oqo/%7B%22get_rows%22%3A%22works%22%7D"

POST /query

Same 8,190-byte request-line cap as above; same escape hatch. The body takes exactly one of the three forms:

curl -X POST "https://api.openalex.org/query" \
  -H "Content-Type: application/json" \
  -d '{"oxurl": "/works?filter=publication_year:2020"}'

The translation response

Every /query translation returns the same six fields:

{
  "oxurl": "/works?filter=publication_year:2020",
  "oql": "works where year is (2020)",
  "oql_oneline": "works where year is (2020)",
  "oql_render_v2": { "…": "…" },
  "oqo": {
    "get_rows": "works",
    "filter_rows": [{"column_id": "publication_year", "value": 2020}]
  },
  "validation": {"valid": true, "errors": [], "warnings": []}
}
Field What it is
oxurl the classic-URL form — null when the query can’t be expressed as one (e.g. nested boolean logic the URL syntax can’t nest)
oql the canonical OQL rendering (line-wrapped for readability on big queries)
oql_oneline the same rendering forced onto one line
oql_render_v2 a rich render tree (every token with its role and metadata) — for building editors and syntax highlighters; most clients can ignore it
oqo the canonical OQO — parse any form, read this one
validation {valid, errors, warnings}, same shape as execution errors above

Bad input gets a 400 with the same validation object as the execution endpoints. A query that parses but fails validation returns 400 with the oqo included, so you can see what it understood.


Reading results: meta.x_query

Every list or group response — including from classic /works?filter=… calls — carries an x_query object in meta, echoing the query in all three forms:

curl "https://api.openalex.org/works?filter=publication_year:2020&per-page=1"
"meta": {
  "count": 11446647,
  "x_query": {
    "oql": "works where year is (2020)",
    "oqo": {"get_rows": "works", "filter_rows": [{"column_id": "publication_year", "value": 2020}], "per_page": 1},
    "url": "/works?filter=publication_year:2020&per_page=1"
  }
}

What it’s for:

  • Learn OQL from queries you already have — hit any classic URL and read the oql field back.
  • Round-trip — feed x_query.oqo back to POST / (or x_query.oql to ?oql=) and get the same results; modify it programmatically along the way.
  • Interoperate — a tool that stores x_query.oqo can rebuild the query in whichever surface it wants: OQL text, a classic URL, or the GUI.

url is null when the query has no classic-URL equivalent. On classic calls the translation is best-effort — a handful of exotic parameter combinations don’t round-trip.

The x_ prefix means what it usually does: this field is young and its details may still shift. The three-forms concept is stable; treat exact field-level details as alpha, like the rest of OQL.


The OQO object

The full, formal shape is the JSON Schema on the OQO schema page (served at /query/spec/oqo). The short version — top level:

Key Type Meaning
get_rows string, required the entity to return: works, authors, institutions, …
filter_rows array of filter nodes the filters; top-level entries are AND-ed
corpus core | expansion | all which works corpus (works only; default core)
group_by array of {column_id} group results into buckets
sample int random sample of n results (optional seed, a string, for reproducibility)

That’s the whole object (#661): sorting, projection, and paging are sibling request params (sort, select, page, per_page, cursor — see “Rules of the road” above), not OQO keys.

A filter node is either a leaf or a branch:

{"column_id": "publication_year", "value": 2020, "operator": ">="}

{"join": "or", "filters": [
  {"column_id": "open_access.is_oa", "value": true},
  {"column_id": "type", "value": "review"}
]}
  • A leaf is {column_id, value} plus optional operator (default is; also >, >=, <, <=, has for text search, in collection) and is_negated: true for negation.
  • A branch is {join: "and"|"or", filters: […]}, nesting to any depth — this is where OQO exceeds the classic URL syntax, which can’t nest.

Putting it together — “highly-cited recent works about cancer that are OA or reviews, top-cited first”:

curl -X POST "https://api.openalex.org/" \
  -H "Content-Type: application/json" \
  -d '{"oqo": {
        "get_rows": "works",
        "filter_rows": [
          {"column_id": "default.search", "value": "cancer", "operator": "has"},
          {"column_id": "publication_year", "value": 2020, "operator": ">="},
          {"join": "or", "filters": [
            {"column_id": "open_access.is_oa", "value": true},
            {"column_id": "type", "value": "review"}
          ]}
        ],
        "sort_by": [{"column_id": "cited_by_count", "direction": "desc"}],
        "select": ["id", "display_name", "cited_by_count"],
        "per_page": 5
      }}'

Column ids are the machine names from the properties registry — list them at https://api.openalex.org/properties (or per entity: /properties/works). The OQL name and the column id often differ (yearpublication_year); when in doubt, write the query in OQL, translate it, and read the oqo.


Endpoint What it does
GET /validate?q=<oql> lint an OQL string; always 200; returns rich diagnostics with stable error codes, spans, and fix-its — built for editors
GET /parse-context?q=<oql>&pos=<n> what the grammar expects at a cursor position — built for autocomplete
GET /properties, /properties/<entity> the registry of every queryable column, its operators, and its OQL name
GET /query/spec index of the spec artifacts (cheat sheet, guide, spec, OQO schema, grammar) that power these docs pages

And the rest of the story lives in the sibling pages here under /query: the Cheat sheet and Guide teach the OQL language itself, the Spec is the normative reference, and the OQO schema page renders the full JSON Schema.

OQL and its API are in alpha and under active development — tell us what’s confusing, what’s missing, and what you wish you could do.

View as Markdown