Skip to main content

Stavrophora: Asynchronous Python client for the Crossref API

Samuel Mok -- s.mok@utwente.nl -- 2026

Stavrophora is an async Python client for the Crossref REST API, built on bibliofabric.

License: MIT

Features

  • Async by design -- built on httpx + asyncio with proper connection pooling
  • Typed end-to-end -- Pydantic v2 models with extra="allow" (the Crossref spec is incomplete; extra fields are preserved, never dropped)
  • Cursor pagination -- async for work in session.works.iterate(...) over message.next-cursor
  • Polite pool by default -- one mailto= line triples your rate limit
  • Batch DOI lookup -- comma-separated repeated filter=doi: keys (Crossref's supported OR form)
  • Scoped works -- session.journals.works(issn), session.funders.works(id), session.prefixes.works(prefix)

Installation

uv add stavrophora

Or with pip: pip install stavrophora. Requires Python >=3.12.

Quick Start

import asyncio
from stavrophora import StavrophoraSession


async def main():
    async with StavrophoraSession(mailto="you@example.org") as session:
        # Get a single work by DOI
        work = await session.works.get("10.1038/nature12373")
        print(work.title[0], work.issued.date_parts)

        # Search
        response = await session.works.search(search="crow nesting", page_size=5)
        for item in response.message.items:
            print(item.doi, item.is_referenced_by_count)


asyncio.run(main())

No authentication required. Pass a mailto (parameter or STAVROPHORA_MAILTO env var) to use the polite pool: verified live 2026-09, anonymous requests are capped at 1 request/s and 1 concurrent request, polite at 3/s and 3 concurrent (advertised in the x-rate-limit-* response headers). Both the mailto query parameter and mailto: in the User-Agent are honored; stavrophora uses the query parameter.

Basic Usage

Get a single entity

work = await session.works.get("10.1038/nature12373")  # DOI (bare or doi.org-prefixed)
journal = await session.journals.get("0028-0836")  # ISSN or eISSN
funder = await session.funders.get("10.13039/501100000923")
member = await session.members.get("78")  # e.g. Elsevier
prefix = await session.prefixes.get("10.1016")  # no /prefixes list route exists

Search, sort, filter, select

from stavrophora.endpoints import WorksFilters

response = await session.works.search(
    filters=WorksFilters(type="journal-article", from_pub_date="2026-01-01"),
    sort_by="is-referenced-by-count:desc",
    select=["DOI", "title", "is-referenced-by-count"],
    page_size=20,
)

Crossref has no publisher filter (400, verified live); scope by prefix, member or ror-id instead.

Iterate all results (cursor)

async for work in session.works.iterate(
    filters={"from-index-date": "2026-08-01"}, page_size=1000
):
    process(work)

The last non-empty page still carries a next-cursor; stavrophora follows cursors until the API returns an empty page (one extra request, by design).

Scoped works

nature_works = session.journals.works("0028-0836")
response = await nature_works.search(page_size=5)

ut_works = session.prefixes.works("10.3990")  # UT student theses
async for w in ut_works.iterate(page_size=1000):
    ...

Batch DOI lookup

found = await session.works.batch_get_by_doi(
    ["10.1038/nature12373", "10.1103/PhysRevLett.116.061102"]
)

Keys are normalized bare lowercase DOIs; misses are absent from the dict. Requests are batched 50 DOIs per call via repeated filter=doi: keys (the pipe form doi:a|b returns zero results -- verified live).

Registration agency

agency = await session.works.agency("10.1038/nature12373")
print(agency.agency.label)  # "Crossref"

Configuration

Settings load from env vars prefixed STAVROPHORA_ (or .env / secrets.env):

STAVROPHORA_MAILTO=s.mok@utwente.nl

All bibliofabric settings (REQUEST_TIMEOUT, MAX_RETRIES, ...) are inherited under the same prefix.

Known Crossref API Quirks

  • No pipe-OR in filters. filter=doi:a|b silently returns 0 results (OpenAlex-style). Repeated key:value pairs (filter=doi:a,doi:b) are the supported OR form.
  • Unknown query parameters are rejected with 400 (no silent ignoring). The client therefore sends only rows, offset, query, filter, sort, order, select, cursor, mailto.
  • page is not a parameter -- paging is offset (0-based, capped at 10,000 total) + rows (max 1000, 400 otherwise). Deep paging must use cursor.
  • Cursor exhaustion is implicit: the last non-empty page still returns a next-cursor; only the following request returns an empty items and no cursor.
  • /prefixes has no list route (404) -- only /prefixes/{prefix} and /prefixes/{prefix}/works. /licenses has no single-item route (404) -- only the list.
  • The message envelope is one level deep for everything: single items live in message, lists in message.items with totals in message.total-results.
  • Rate limits changed 2025-12-01: anonymous 1 req/s / 1 concurrent, polite 3 req/s / 3 concurrent (older docs say 5/10 req/s). Every response carries x-rate-limit-* headers and the advertised limit identifies the pool (live 2026-09): anonymous 1, polite 3 on query routes and 10 on cacheable single-record routes (the pre-2025-12 value) -- treat 3 req/s as the polite ceiling.
  • API is served at both https://api.crossref.org and .../v1 with identical payloads (verified 2026-09); stavrophora pins /v1, the versioned form.
  • The Crossref spec is incomplete -- several returned fields are missing from the official schema. All models use extra="allow", same defense as aletheca.
  • Schemas differ between list and single-item routes (verified live): /journals/{issn} returns title as a bare string while /journals returns a list; member prefix entries are {name, value} objects. The models normalize both shapes (SafeStrList, dict-typed prefix entries).
  • /types ignores rows/offset (verified live 2026-09): the route always returns the complete list (~30 types) regardless of paging parameters.
  • /prefixes/{prefix} returns member and prefix as full https://id.crossref.org/member/297-style URIs (verified live 2026-09). The Prefix model normalizes both the URI and bare forms to bare values ("297", "10.1038").

Development

uv sync
uv run pytest                    # mocked tests (live tests excluded by default)
uv run pytest -m live_api        # live API smoke tests
uv run ruff check .
uvx ty check src/

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

stavrophora-0.2.1.tar.gz (19.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stavrophora-0.2.1-py3-none-any.whl (28.9 kB view details)

Uploaded Python 3

File details

Details for the file stavrophora-0.2.1.tar.gz.

File metadata

  • Download URL: stavrophora-0.2.1.tar.gz
  • Upload date:
  • Size: 19.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for stavrophora-0.2.1.tar.gz
Algorithm Hash digest
SHA256 773b351d16723ac971df117c08e6f76ea65aaf448def2e28d99293f1351fea32
MD5 d3ea7229fcdd707911e2afb3cab15a3f
BLAKE2b-256 6d71dd102938fdd02ce5bedc0f0e4f308cbd3bcdd01e13e9e25d8c28d8d74905

See more details on using hashes here.

Provenance

The following attestation bundles were made for stavrophora-0.2.1.tar.gz:

Publisher: python-ci.yml on utsmok/stavrophora

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file stavrophora-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: stavrophora-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 28.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for stavrophora-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 63ca7c0a2f9a3618b8bc3b73b2e201e90492a80bb007d3214df63bff30f305a1
MD5 0ca76a618b05505e2f0574135dd6f6bb
BLAKE2b-256 6e3be69675bcd8efec0adb1ad2fd38986d5553237440c1dbe505f3988d1fc006

See more details on using hashes here.

Provenance

The following attestation bundles were made for stavrophora-0.2.1-py3-none-any.whl:

Publisher: python-ci.yml on utsmok/stavrophora

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page