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.1.0.tar.gz (18.8 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.1.0-py3-none-any.whl (28.1 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for stavrophora-0.1.0.tar.gz
Algorithm Hash digest
SHA256 77a16b21e2fd50e425c68c1b2af00dc2ef9e387acb42f51991cb23788e901196
MD5 135624019b2aa08a086bd8c320cff9c0
BLAKE2b-256 caaa72bcfd987c6c8a828d43a3488e6271a046cc3207fbc6e45c4c1163822ca7

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: stavrophora-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 28.1 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.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c47199b4619e2729d6ffd6ee43a7a0aa995fb418dee856387696a02820de29fa
MD5 9efe07525ab46e642eaaae4fa8a2b61f
BLAKE2b-256 aa96cbfad2ebeea4f9947dff9c6b6ed16ba63ac6d5a19ee6f74088a1c19ddbae

See more details on using hashes here.

Provenance

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

0.2.0

2 files

This release

0.1.0 This release

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