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.0.tar.gz (19.0 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.0-py3-none-any.whl (28.3 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: stavrophora-0.2.0.tar.gz
  • Upload date:
  • Size: 19.0 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.0.tar.gz
Algorithm Hash digest
SHA256 0b02926476b2d386462bd57319ce7a1482c1c1b6f1c493ce359cc98fd7bf220f
MD5 3f3c9f2d90d51e51659030265e600dfd
BLAKE2b-256 5424930973b168101cc7ce97803cd44b1f0d2a057390b576a3651f5cd2694769

See more details on using hashes here.

Provenance

The following attestation bundles were made for stavrophora-0.2.0.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.0-py3-none-any.whl.

File metadata

  • Download URL: stavrophora-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 28.3 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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0eb32954eeac49b6e377e1c2b1f8055e78d632288a2848a2da5885dc0736cbcf
MD5 ea64fe7ed9356adcb932c89d41fd8745
BLAKE2b-256 10d24e00a2865e3b363d2f921d799f535b7cf8debcf55db205a82a5b897f527a

See more details on using hashes here.

Provenance

The following attestation bundles were made for stavrophora-0.2.0-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

0.2.1

2 files

This release

0.2.0 This release

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