Collections

A collection is a named list of members: OpenAlex entities of one type, such as “Papers I’m tracking for this grant”, “Authors at my consortium” or “Journals in our Elsevier package”. A collection can hold any OpenAlex entity type. Its ID drops into the filter parameter anywhere in the API, in place of hundreds of IDs pasted into every request. Besides your own, OpenAlex keeps public collections, such as country groups, that anyone can list and use without an account.

Collections behave like every other OpenAlex entity: an id that is a URL (https://openalex.org/collections/col_8yWKmRNyEr, with the short col_8yWKmRNyEr accepted everywhere), display_name, created_date and updated_date, and a list endpoint that takes filter, search, sort, select and cursor paging. What a collection is, and every attribute, is on the Collections entity page; worked examples are in Working with collections.

Quick start

Create a collection with two works, then search inside it:

curl -X POST https://api.openalex.org/collections \
  -H "Authorization: Bearer $OPENALEX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "display_name": "My altmetrics papers",
    "entity_type": "works",
    "member_ids": ["W2755968057", "https://openalex.org/W4404012345"]
  }'
{
  "id": "https://openalex.org/collections/col_8yWKmRNyEr",
  "display_name": "My altmetrics papers",
  "description": "",
  "entity_type": "works",
  "member_count": 2,
  "access": "private",
  "can_edit": true,
  "created_date": "2026-10-03",
  "updated_date": "2026-10-03T14:00:00.000000"
}
curl "https://api.openalex.org/works?filter=collection:col_8yWKmRNyEr,is_oa:true&api_key=$OPENALEX_API_KEY"

Or use a public collection, no collection of your own needed: works with an author in the European Union.

curl "https://api.openalex.org/collections?filter=access:public&search=european"
curl "https://api.openalex.org/works?filter=authorships.countries:col_LV29j8URoX&api_key=$OPENALEX_API_KEY"

Authentication

Send your OpenAlex API key as ?api_key= or an Authorization: Bearer header: a personal key (organization keys aren’t accepted here). Without one you can read only public collections and collections shared by link. Managing collections costs no credits; a search that filters by a collection costs what any search costs.

The collection object

FieldTypeMeaning
idstringhttps://openalex.org/collections/col_…, the collection’s OpenAlex ID. The short col_… works wherever the URL does: in paths, in filters and in copy_of.
display_namestring1 to 100 characters, unique per owner (ignoring case).
descriptionstring0 to 500 characters; "" when empty.
entity_typestringWhat every member is: works, authors, sources, locations, countries or any other entity type. Fixed at creation.
member_countintegerHow many members it holds, at most 1,000,000.
accessstringprivate (the default), shared_by_link or public. See Who can see a collection.
can_editbooleantrue only for the owner. The object never says who the owner is.
created_datestringThe date it was created, YYYY-MM-DD (UTC).
updated_datestringThe datetime of the last change to it or its members (ISO 8601 in UTC, written without a Z, as on every entity).

The members aren’t in the object: page through them at /collections/{id}/members, or get them as full entities with a filter.

Endpoints

MethodPathWhat it doesWho
GET/collectionsPublic collections, plus yours with a key; filter, search, sort, group_by, select, pagingAnyone
POST/collectionsCreate one, or make a copyAny account
GET/collections/{id}Read one; takes selectPer its access
PATCH/collections/{id}Change display_name, description, accessOwner
DELETE/collections/{id}Delete it (204)Owner
GET/collections/{id}/membersIts members, paged, or all as CSV (format=csv)Per its access
POST/collections/{id}/membersAdd membersOwner
DELETE/collections/{id}/members/{member_id}Remove one member (204)Owner
DELETE/collections/{id}/members?member_ids=…Remove up to 100 membersOwner
POST/collections/{id}/importsAdd a search’s results on our servers (202)Owner
GET/collections/{id}/imports[/{import_id}]Imports and their progressOwner

{id} is the URL ID or the short col_…, as with every entity: /collections/col_8yWKmRNyEr and /collections/https://openalex.org/collections/col_8yWKmRNyEr are the same collection. Some HTTP clients fold the // in a path, so percent-encode the URL there (https%3A%2F%2Fopenalex.org%2Fcollections%2Fcol_8yWKmRNyEr), or use the short form. Reads by anyone but the owner are rate limited: 120 a minute per IP logged out, 300 a minute per account.

List collections

# Public collections, which anyone can list, no key needed
GET https://api.openalex.org/collections?filter=access:public&search=income

# Your own collections
GET https://api.openalex.org/collections?filter=can_edit:true&sort=updated_date:desc&api_key=<your-api-key>

Lists the public collections, which OpenAlex makes, plus your own when you send an API key. Nobody else’s private or shared-by-link collections are ever listed. filter=access:public is the public list; filter=can_edit:true is yours. Logged out, the list is the public collections alone, 120 requests a minute per IP. The parameters work as on every list endpoint:

ParameterTakesExample
filterentity_type, access and can_edit (true = yours); | for OR, ! to negate, commas to ANDfilter=entity_type:countries,access:public
searchText in display_name or description (case-insensitive)search=latin america
group_byentity_type, access or can_edit: a count per value, in group_by, with results emptygroup_by=entity_type
sortOne of display_name (the default), created_date, updated_date, member_count; ascending unless you add :desc. Dates sort by full creation and update time, ties by id, so paging is stablesort=member_count:desc
selectAny fields of the object, plus matching_member_ids with member_idsselect=id,display_name,member_count
page, per_pageBasic paging; per_page 1 to 100, default 25page=2&per_page=50
cursorCursor paging: cursor=*, then each meta.next_cursor until it’s nullcursor=*
member_idsUp to 100 member IDs of any type, comma-separated and URL-encoded (location IDs too): only listed collections holding any of them, each with matching_member_ids. Add filter=can_edit:true for yours alonemember_ids=W2755968057,W4404012345
{
  "meta": { "count": 3, "page": 1, "per_page": 25 },
  "results": [
    {
      "id": "https://openalex.org/collections/col_Jr8sWq2LmT",
      "display_name": "UC agreement journals",
      "description": "Journals in the UC transformative agreements",
      "entity_type": "sources",
      "member_count": 412,
      "access": "shared_by_link",
      "can_edit": true,
      "created_date": "2026-09-30",
      "updated_date": "2026-10-03T09:12:44.512000"
    }
  ]
}

With group_by, the answer counts the same collections the list would return:

{
  "meta": { "count": 15, "page": null, "per_page": null, "groups_count": 2 },
  "results": [],
  "group_by": [
    { "key": "public", "key_display_name": "Public", "count": 12 },
    { "key": "private", "key_display_name": "Private", "count": 3 }
  ]
}

In cursor mode meta is {"count": 3, "page": null, "per_page": 25, "next_cursor": "…"}. A cursor belongs to its sort: change the sort and start again with cursor=*.

Get a collection

GET https://api.openalex.org/collections/col_8yWKmRNyEr?select=id,display_name,member_count

Returns the collection, or only the selected fields. A collection you can’t read returns 404 with code collection_not_found, whether it’s missing, deleted or private, so nobody can probe for private ones.

Create a collection

POST https://api.openalex.org/collections
Content-Type: application/json

{
  "display_name": "My altmetrics papers",
  "entity_type": "works",
  "description": "Papers I'm tracking for the altmetrics review",
  "member_ids": ["W2755968057", "https://openalex.org/W4404012345"],
  "access": "private"
}

Only display_name and entity_type are required. member_ids takes up to 10,000 OpenAlex IDs per request (a collection holds up to 1,000,000: add more with further requests, or import a search’s results), short (W2755968057) or as URLs; the API stores the short form. It takes OpenAlex IDs only: turn DOIs, ORCIDs or ISSNs into OpenAlex IDs first (how). Every ID must match the collection’s entity_type: A5023888391 in a works collection returns 400 with code member_wrong_type, naming the ID, and nothing is created. Returns 201, the collection, and a Location header with its URL.

Members of a locations collection are location IDs, stored exactly as given: doi:10.7717/peerj.4375, pmh:oai:arXiv.org:cond-mat/0404022. They are case-sensitive and contain / and :, so copy them verbatim. A work’s copies are locations[].id in GET /works/W2741809807?select=locations.

Make a copy

POST https://api.openalex.org/collections
Content-Type: application/json

{ "copy_of": "https://openalex.org/collections/col_8yWKmRNyEr" }

Copies any collection you can read (your own, or one shared by link) into a new private collection you own, with the same type, description and members. Pass display_name to name it; otherwise it keeps the source’s name, with “(copy 2)” and so on if you already use that name. The copy doesn’t follow later changes to the source. A copy of more than 100,000 members is created at once and fills in the background, as an import; GET /collections/{id}/imports shows its progress.

Change a collection

PATCH https://api.openalex.org/collections/col_8yWKmRNyEr
Content-Type: application/json

{ "display_name": "Renamed", "description": "Updated notes", "access": "shared_by_link" }

Takes display_name, description and access, and returns the collection. entity_type is fixed when a collection is created.

Delete a collection

DELETE https://api.openalex.org/collections/col_8yWKmRNyEr

Returns 204 with no body. Its members go with it; saved searches that filter by it start returning 404.

List its members

GET https://api.openalex.org/collections/col_8yWKmRNyEr/members?per_page=1000
{
  "meta": { "count": 4, "page": 1, "per_page": 1000 },
  "results": [
    { "id": "W2755968057", "added_at": "2026-05-20T16:00:00" }
  ]
}

Each member is its id (stored as described above) and added_at, when it was added (ISO 8601, UTC). Members come oldest first; members added in the same call come in no set order, so compare them as a set. Page with page and per_page (1 to 1,000, default 100), or with cursor=* and meta.next_cursor. For the members as full entities, filter their endpoint instead: /works?filter=collection:col_8yWKmRNyEr.

Every member at once, as CSV (id,added_at), streamed however big the collection:

GET https://api.openalex.org/collections/col_8yWKmRNyEr/members?format=csv

Add and remove members

POST https://api.openalex.org/collections/col_8yWKmRNyEr/members
Content-Type: application/json

{ "member_ids": ["W2755968057", "https://openalex.org/W4404012345"] }

Returns {"added": 1, "already_present": 1, "member_count": 5}. Adding a member that’s already there is not an error, even when the collection is full: only new members count against the limit. If any ID is the wrong type or not an OpenAlex ID, nothing is added and the 400 names it. One request takes up to 10,000 IDs.

Add a search’s results

POST https://api.openalex.org/collections/col_8yWKmRNyEr/imports
Content-Type: application/json

{ "query": "https://api.openalex.org/works?filter=topics.id:T10102,publication_year:2024" }

Adds every result of a search, on our servers: send the search as query (an api.openalex.org URL listing the collection’s type), as oql, or copy_of (another collection you can read). exclude_ids (up to 10,000) leaves some results out. It returns 202 and a Location header for the import:

GET https://api.openalex.org/collections/col_8yWKmRNyEr/imports/imp_3kQ9sXv2Lm
{
  "id": "imp_3kQ9sXv2Lm",
  "status": "running",
  "result_count": 48211,
  "added": 12000,
  "already_present": 0,
  "progress": 0.2489,
  "stopped_at_limit": false,
  "error": null
}

status goes queued, running, then done or failed (with error.code and error.message). The search runs with your API key, one request per 200 results, so it uses your credits like paging through the results yourself. Results go in in the search’s order, and an import stops when the collection is full (stopped_at_limit: true). Paging, select and sort are handled for you; group_by and sample can’t be imported. One import runs per collection at a time (409, code import_in_progress), and up to 3 per account. GET /collections/{id}/imports lists the latest. On the website this is Save results as a collection on any search, and Select all followed by Add to collection.

# Remove one member
DELETE https://api.openalex.org/collections/col_8yWKmRNyEr/members/W2755968057

# Remove several (up to 100), no request body
DELETE https://api.openalex.org/collections/col_8yWKmRNyEr/members?member_ids=W2755968057,W4404012345

Removing one returns 204, or 404 with code member_not_found if it wasn’t a member. Removing several returns {"removed": 2, "member_count": 3}.

Filtering by a collection

On its own endpoint: collection:

# Every work in a works collection
GET https://api.openalex.org/works?filter=collection:col_8yWKmRNyEr

# Every location in a locations collection
GET https://api.openalex.org/locations?filter=collection:col_Lo7kq2PZab

collection: works on the endpoint of every type a collection can hold, as long as the two match: an authors collection on /works returns 400 naming both. The collection resolves to its members at query time, so it combines with every other filter, sort, group_by, select and paging:

# Open-access papers in this collection, newest first
GET https://api.openalex.org/works?filter=collection:col_8yWKmRNyEr,is_oa:true&sort=publication_date:desc

Any filter whose value is an OpenAlex ID also takes a collection of that type, so a collection of one type filters another. Some common ones (the how-to has more, such as locations.source.id and authorships.countries):

Filter clauseCollection typeMeaning
/works?filter=primary_location.source.id:col_…sourcesWorks published in these journals
/works?filter=authorships.author.id:col_…authorsWorks by any of these authors
/works?filter=authorships.institutions.lineage:col_…institutionsWorks from these institutions or their parts
/works?filter=corresponding_institution_ids:col_…institutionsWorks with a corresponding author at one of these
/works?filter=topics.id:col_…topicsWorks on these topics
/works?filter=funders.id:col_…fundersWorks funded by these funders
/authors?filter=last_known_institutions.id:col_…institutionsAuthors last seen at these institutions

A mismatch returns 400, for example “collection col_… is type ‘sources’, not valid for the authorships.author.id filter (expects ‘authors’).” A field that doesn’t take an entity ID (a date, a boolean) can’t take a collection at all.

Excluding a collection

Prepend !: /works?filter=primary_location.source.id:!col_8yWKmRNyEr is every work not published in those journals.

Limits

  • One collection per filter field. field:col_a|col_b, a second clause on the same field with another collection, or a collection mixed with literal IDs (field:col_a|S123) return 400. Different fields can each carry one.
  • Size. A collection filters live up to 300,000 members, and an author collection up to 100,000 (author filters pull whole careers, so a bigger roster gives wrong answers, not slow ones). A bigger one still holds, lists and exports its members, but as a filter returns 400 with code collection_too_big_to_filter. For a whole country or institution, use the authorships.institutions.country_code or authorships.institutions.lineage filter instead: it counts by affiliation and is exact. Filters by collections of more than about 150,000 members can take a few seconds.
  • At most 5 collections per request, and 300,000 members in all.
  • Access. A private collection filters only for its owner’s key; one shared by link or public filters for anyone. One you can’t read returns 404 “Collection col_… not found.” (code collection_not_found), never a silent zero.

Who can see a collection

accessWho can view it and filter by itListed?
privateOnly its owner. The default for every new collection.Only to its owner
shared_by_linkAnyone with its link or ID, logged in or not.Only to its owner
publicAnyone, logged in or not.To everyone, in GET /collections and on openalex.org/collections

Public collections

Public collections are lists OpenAlex makes and keeps up to date, starting with country groups: the European Union (EU27), the UN M49 regions and Latin America and the Caribbean, the World Bank income groups and OECD members. The full list, with IDs and sources, is on the Collections entity page; each one’s description names its source and date. List them, then use one like any other collection:

# All public collections, or those matching a word
GET https://api.openalex.org/collections?filter=access:public
GET https://api.openalex.org/collections?filter=access:public&search=income

# Works with an author in a low-income country (World Bank)
GET https://api.openalex.org/works?filter=authorships.countries:col_WXiZS2Kp4u

# Works in journals based in the EU, and works with no EU author
GET https://api.openalex.org/works?filter=primary_location.source.country_code:col_LV29j8URoX
GET https://api.openalex.org/works?filter=authorships.countries:!col_LV29j8URoX

A country group works on every country filter: on /works, authorships.countries, institutions.country_code, primary_location.source.country_code and funders.country_code; country_code on /institutions and /sources; last_known_institutions.country_code on /authors; funder.country_code on /awards; and collection: on /countries. Not yet on /funders or /publishers.

Only OpenAlex can make a collection public for now: {"access": "public"} from anyone else returns 403 public_needs_review. To suggest a list for the public collections, write to [email protected]. Anyone can make a copy of a public collection and edit their own.

A collection shared by link is never listed or searchable: people find it only through a link or ID you give them. Only the owner can change it; anyone else with an account can make a copy. Share with PATCH /collections/{id} and {"access": "shared_by_link"}; {"access": "private"} unshares it at once, for every link and saved search that uses it. On the website, use Share on the collection’s page. Lists of people say something about them: don’t share lists drawn from HR records.

Alerts and exports

Alerts (full API). Save a works search that filters by a collection of authors, institutions, sources or another type, turn on its alert, and OpenAlex emails you new works that match. A search limited to a works collection (collection:col_…) can’t alert: a fixed list of works never gains new ones (400, code works_collection_cannot_alert). An alert runs only while you can still read every collection in its search; if one is deleted or made private, the alert turns off and you get an email saying which collection.

Exports. An export of a search that filters by a collection reads it with your key. If you can’t read a collection in the search, the export is refused with 404 (code collection_not_found) rather than producing an empty file.

Limits

LimitValue
Members per collection1,000,000
Live filter, author collections100,000 members
Live filter, every other type300,000 members
Collections per account500
display_name1 to 100 characters, unique per owner (ignoring case)
description0 to 500 characters
IDs in one member_ids body10,000
IDs in one member_ids query string100
Imports running at once1 per collection, 3 per account

Errors

Errors look like the rest of the API, with a stable code to branch on:

{ "error": "Bad Request", "code": "invalid_sort", "message": "Can't sort collections by `cited_by_count`. Sort by: display_name, created_date, updated_date, member_count." }
StatuscodeCause
400invalid_filterA filter other than entity_type or access, or a value it doesn’t take; the message lists the valid ones
400invalid_sortNot a sortable field, a direction other than asc or desc, or more than one field
400invalid_selectA field the collection object doesn’t have; the message lists them
400invalid_searchsearch over 200 characters
400invalid_paging, invalid_cursorpage, per_page or cursor out of range, both page and cursor, or a cursor from a different sort
400invalid_bodyThe body isn’t a JSON object (send Content-Type: application/json)
400unknown_fieldA field this endpoint doesn’t take; the message names the right one (member_ids, not entity_ids)
400field_not_editable, no_fieldsentity_type in a PATCH, or a PATCH with nothing to change
400entity_type_invalidentity_type missing or not an entity type
400display_name_blank, display_name_too_long, display_name_duplicate, display_name_url, display_name_reserved, display_name_whitespace, display_name_invalid_characterThe name is empty, over 100 characters, already yours, a URL, reserved, or has control characters
400description_invalid, description_too_long, description_urlThe description isn’t a string, is over 500 characters, or is a URL
400access_invalidaccess isn’t private, shared_by_link or public
400invalid_group_bygroup_by isn’t entity_type, access or can_edit
400invalid_copy_ofcopy_of isn’t a collection ID
400member_id_invalid, member_wrong_typeAn ID isn’t an OpenAlex ID, or isn’t the collection’s type
400member_limit_reachedCreating or copying more than 1,000,000 members (on an add, it’s 403)
400member_ids_required, too_many_member_idsBulk remove without member_ids, or over 100; or over 10,000 IDs in one add
400collection_too_big_to_filterFiltering by a collection over its live filter limit (see Limits)
400query_required, invalid_query, query_wrong_type, invalid_oql, invalid_exclude_ids, copy_of_wrong_typeAn import without exactly one of query, oql, copy_of; a search that isn’t an api.openalex.org list of the collection’s type; or bad exclude_ids
400invalid_formatformat other than csv on the members list
401unauthorizedNo valid API key, on a call that needs one, or an invalid key on any call (a bad key never gets the logged-out answer)
403not_collection_ownerOnly the owner can change it; make a copy instead
403public_needs_reviewOnly OpenAlex can make a collection public; write to [email protected] to suggest one
403collection_limit_reachedYou already own 500 collections
403member_limit_reachedAdding new members would pass 1,000,000; re-sent members never count
404collection_not_foundMissing, deleted, or private to someone else
404member_not_foundRemoving an ID that isn’t a member
404import_not_foundNo such import on this collection
409import_in_progress, too_many_importsThe collection is already importing, or you have 3 imports running
429rate_limitedToo many reads; wait for Retry-After seconds

For agents

  • Build a collection from a pasted list by resolving each line to an OpenAlex ID first (Finding OpenAlex IDs), then POST the member_ids, up to 10,000 per request.
  • To save a search’s results, don’t page them yourself: POST /collections/{id}/imports with the search as query, then poll its Location.
  • Use the id the API returns; both its forms work everywhere. Find a collection by name with GET /collections?search=…&select=id,display_name.
  • To answer “which of my collections hold X”, use GET /collections?member_ids=X. For a work, add its location IDs too, to catch locations collections holding its copies.
  • A work’s location IDs are locations[].id from GET /works/{id}?select=locations.
  • Before filtering by a collection, match its entity_type to the filter: collection: on its own endpoint, or the ID field of that type elsewhere.
  • For a country group (EU, Latin America, income groups, OECD), look for a public collection before building one: GET /collections?filter=access:public&search=….
  • Branch on code, never on message.

Older routes (deprecated)

The first version of this API lives on user.openalex.org and keeps working, with its old response shape (bare col_… IDs, created_at and updated_at), for scripts already built on it. New code should use the routes above.

DeprecatedUse instead
GET user.openalex.org/me/collectionsGET /collections
POST user.openalex.org/me/collections (entity_ids, source_collection_id)POST /collections (member_ids, copy_of)
GET user.openalex.org/collections/{id}GET /collections/{id}
GET user.openalex.org/collections/{id}/entitiesGET /collections/{id}/members
PATCH, DELETE user.openalex.org/me/collections/{id}PATCH, DELETE /collections/{id}
POST user.openalex.org/me/collections/{id}/entitiesPOST /collections/{id}/members
DELETE user.openalex.org/me/collections/{id}/entities (body)DELETE /collections/{id}/members?member_ids=
DELETE user.openalex.org/me/collections/{id}/entities/{id}DELETE /collections/{id}/members/{id}

The old routes take the key only as Authorization: Bearer, call members entity_ids and entity_count, and answer errors as {"HTTP_status_code", "error": true, "message", "code"}.

Admin endpoints

OpenAlex admins can list, read, edit or delete any user’s collection on https://user.openalex.org:

MethodPathNotes
GET/admin/collections?q=&owner_id=&entity_type=Cross-user search, paged
GET/admin/collections/{collection_id}Read any collection
PATCH/admin/collections/{collection_id}Same body as the user PATCH, plus user_id to transfer ownership; {"access": "public"} makes it public
DELETE/admin/collections/{collection_id}Hard-delete with cascade

Non-admin callers get 403.

Last updated

View as Markdown