Skip to main content

xippios-cashctrl

Typed Python client, MCP server and FastAPI routes for CashCtrl, part of the xippios SDK family. Names and semantics follow the upstream documentation; every response carries release metadata (source, endpoint, retrieved_at, published_at, vintage).

Install

uv add xippios-cashctrl            # client only
uv add "xippios-cashctrl[mcp]"     # + MCP server
uv add "xippios-cashctrl[http]"    # + FastAPI router

Configuration

CashCtrl serves every organisation from its own subdomain (https://<subdomain>.cashctrl.com/api/v1) and gives every organisation its own API user. The package therefore works with named organisations, declared in the environment; nothing is read from files.

Variable Meaning
CASHCTRL_ORGS the organisations, comma-separated, lower-case letters, digits and _: acme,acme_test
CASHCTRL_<NAME>_SUBDOMAIN per organisation, the subdomain: CASHCTRL_ACME_SUBDOMAIN=acme
CASHCTRL_<NAME>_API_KEY per organisation, the API user's key (Settings → Users & Roles)
CASHCTRL_DEMO_SUBDOMAIN, CASHCTRL_DEMO_API_KEY the reserved organisation demo, used by this repository's tests; never listed in CASHCTRL_ORGS

Every task and tool takes org, the name of one declared organisation, as its first parameter, and every result names it as meta.tenant. list_organisations() (the tool cashctrl_list_organisations) returns the declared names, nothing else. Each organisation gets its own client, key and rate limit (60 requests per minute, self-imposed); nothing spans organisations. A deployment reaches exactly the organisations its environment declares. An unknown name is refused with the declared ones; a malformed CASHCTRL_ORGS or a missing or malformed subdomain is a configuration error naming the variable, never its value.

30-second example

from xippios.cashctrl import tasks
from xippios.cashctrl.models import Filter

print(tasks.list_organisations().data)  # the declared organisations, e.g. ["acme", "demo"]

# "demo" stands for any declared organisation; the first argument is always org.
periods = tasks.list_fiscal_periods("demo")
current = next(period for period in periods.data if period.is_current)

accounts = tasks.list_accounts("demo", fiscal_period_id=current.id, only_active=True)
for account in accounts.data:
    print(account.number, account.name, account.end_amount)  # a Decimal, or None

bank = tasks.list_accounts("demo", filter=[Filter(comparison="eq", field="number", value="1020")])
record = tasks.get_account("demo", bank.data[0].id)
print(record.meta.endpoint, record.meta.request, record.meta.retrieved_at)

# The journal, persons and orders page by offset: limit gives one page, and meta.total
# says how many match; without limit every page is collected.
page = tasks.list_journal_entries("demo", fiscal_period_id=current.id, limit=20)
print(len(page.data), "of", page.meta.total)
entry = tasks.get_journal_entry("demo", page.data[0].id)  # with lines and bookings
invoices = tasks.list_orders("demo", type="SALES", fiscal_period_id=current.id)

Every task returns Result[T]: data holds models that mirror CashCtrl's payloads under upstream's names in snake_case (is_current, end_amount), in Python and in every tool and route result alike (the report rows of get_report_data excepted, which come as upstream sends them, and the file tasks' Downloaded, which describes a file; Files below), meta the upstream operation, the parameters as sent under upstream's names (onlyActive), the organisation as tenant, when they were fetched and, for a list, total, upstream's count of the matching records. Async twins are named <task>_async (list_organisations has none). The tasks are listed in the status table below. Persons come back without the personal identifiers ssn and iban (list_persons, get_person, the persons get_journal_entry names; ADR-0017), locations and an order's document without their bank account numbers (iban and qr_iban, list_locations; orgIban, orgQrIban and recipientIban, get_order_document), and report rows without any key named ssn or ending in iban (get_report_data, which refuses payroll and people reports outright). An API user that wrote or changed a record shows as API:REDACTED in created_by and last_updated_by, as in the fixtures; CashCtrl writes API: and the first four characters of its key there.

MCP

{
  "mcpServers": {
    "cashctrl": {
      "command": "uvx",
      "args": ["--from", "xippios-cashctrl[mcp]", "xippios-cashctrl-mcp", "--org", "acme"],
      "env": {
        "CASHCTRL_ORGS": "acme",
        "CASHCTRL_ACME_SUBDOMAIN": "acme",
        "CASHCTRL_ACME_API_KEY": "..."
      }
    }
  }
}

--org acme binds the server to one organisation: it stops at startup unless acme is declared with a well-formed subdomain, its instructions name the organisation, and every tool acts on acme and takes no org, so no call reaches another organisation; cashctrl_list_organisations then lists acme only. Without --org, a server serves every declared organisation and the agent picks one per call, after cashctrl_list_organisations has named them. Its tools are read-only and named cashctrl_<verb>_<noun>. Results are capped at 200 rows or 64 KB with a note on how to narrow the query (cashctrl_list_cost_centers and cashctrl_list_history name their filters, and a paged list says where to resume); cashctrl_list_custom_fields at 500 rows or 128 KB and cashctrl_get_report_data at 128 KB, since an agent needs them whole. --http serves streamable HTTP instead of stdio.

Write tools run in a separate server, xippios-cashctrl-mcp-write (see Write tools below); every write tool previews its change unless called with confirm=true. It takes --org too. It carries no authentication, so --http serves it on a loopback --host (127.0.0.1, localhost, ::1) only; --allow-remote serves it beyond, behind a gateway that authenticates.

{
  "mcpServers": {
    "cashctrl-write": {
      "command": "uvx",
      "args": ["--from", "xippios-cashctrl[mcp]", "xippios-cashctrl-mcp-write", "--org", "acme"],
      "env": {
        "CASHCTRL_ORGS": "acme",
        "CASHCTRL_ACME_SUBDOMAIN": "acme",
        "CASHCTRL_ACME_API_KEY": "..."
      }
    }
  }
}

Write tools

Eleven tools create, change and delete journal entries, persons and orders, set the status of orders and create the payment of orders. They are tasks in xippios.cashctrl.tasks_write (re-exported from xippios.cashctrl), served by the write server only: never by the read server, never as an HTTP route.

Upstream operation Task function MCP tool
POST journal/create.json (Create journal entry) create_journal_entry cashctrl_create_journal_entry
POST journal/update.json (Update journal entry) update_journal_entry cashctrl_update_journal_entry
POST journal/delete.json (Delete journal entries) delete_journal_entry cashctrl_delete_journal_entry
POST person/create.json (Create person) create_person cashctrl_create_person
POST person/update.json (Update person) update_person cashctrl_update_person
POST person/delete.json (Delete people) delete_person cashctrl_delete_person
POST order/create.json (Create order) create_order cashctrl_create_order
POST order/update.json (Update order) update_order cashctrl_update_order
POST order/delete.json (Delete orders) delete_order cashctrl_delete_order
POST order/update_status.json (Update status) update_order_status cashctrl_update_order_status
POST order/payment/create.json (Create payment) create_order_payment cashctrl_create_order_payment
  • The confirm gate. Every write tool takes confirm, last, false by default. Without it nothing is written: the result is a preview, performed: false, with endpoint, payload (the exact form fields CashCtrl would receive, under upstream's names: booleans true/false, dates YYYY-MM-DD, lists as JSON, custom fields as XML; ssn and an iban as REDACTED, as in meta.request after a write), before (for an update or a delete, the record as it is now, as the read tools show it) and changes (for an update). A create's preview sends no request, nor does a status's or a payment's; an update's or a delete's sends one read and never a write. With confirm=true the write is sent and the result is performed: true with insert_id (the new record's id; an update answers the record's own) and upstream's message. Call once without confirm, show the preview, then confirm. Only confirm=True writes: from Python anything else ("false", 1) previews; over MCP the schema turns "true", "yes", "1", 1 into true and "false", "no", "0", 0 into false, and refuses anything else. In Python the result is Result[WritePreview[<record>] | WriteResult]; narrow it with isinstance or if result.data.performed:.
  • Updates send the whole record. CashCtrl empties every parameter an update leaves out. update_journal_entry, update_person and update_order therefore read the record, apply the parameters given, and send every parameter update.json documents, the rest taken from the record. A parameter left out keeps its value: an order's items, too, are sent back as stored, each under the keys the items table documents (account_id, name, unit_price, quantity, tax_id, ...), and items given replace them whole; upstream stores the items it is sent anew, so their ids change (observed). clear names parameters to send empty (clear=["tax_id", "notes"]; a flag such as is_vendor or is_display_item_gross is sent false), and a parameter both given and cleared is refused, as is sequence_number_id, which an update sends only when given. Refused with InvalidRequestError before anything is written, unless the call gives or clears what is missing: a record that gives no value for a mandatory parameter; one holding a list whose shape was never observed (attachments, allocations, bank accounts, children, service periods, insurance contracts, certificate values, an order item's allocations), an address with a titleId, a contact with a purpose or an order item with attachments or marked optional (keys the tables do not document); an insurance whose read shows no insurance numbers or an employee whose read shows no user_id (the read leaves empty fields out, and these were never seen come back); and a journal entry an order or a salary run booked. A confirmed update reads the record again and recomputes the payload, so it can differ from the preview if the record changed in between.
  • Deletes take one id; the preview shows the record that would go. delete_journal_entry reads the entry with confirm=true as well and refuses, with InvalidRequestError before anything is deleted, an entry an order or a salary run booked; an unknown id is NotFoundError there. A person or an order that no longer exists, or an entry deleted between that read and the delete, is refused by upstream (RequestRejected).
  • Statuses and payments. update_order_status sets the status status_id on the orders ids names; create_order_payment creates the payment of the orders order_ids names, dated date, for amount per order or, without it, every open amount. CashCtrl documents a payment as the payment order for e-banking, a pain.001 file (type PAIN, its default, or SEPA_PAIN) or a PDF (WIRE_PDF, CASH_PDF), which its order/payment/download delivers afterwards (not wrapped here). Both send what they are given, nothing read first. A payment creates no book entry, and CashCtrl documents no way to read, list or delete one. PAIN needs the sender's and the recipient's bank accounts and the sender's BIC and refuses a date in the past; CASH_PDF needed none of them (observed on the demo organisation, 2026-10-29). A status id comes from the status of the orders' category in cashctrl_list_order_categories.
  • Custom fields: custom maps upstream's names (customField1, customField<ID>, the ids from cashctrl_list_custom_fields) to text, sent as <values><customField1>...</customField1></values>, values escaped. An update keeps every stored value it is not given.
  • Amounts. A journal amount with a tax_id is the gross amount, VAT included, and order items' unit_price was read as gross too (observed on the demo organisation, although the tax codes' calculation type reads NET; docs/demo-org.md).
  • Ids come from the read tools: account ids from cashctrl_list_accounts (the id, not the account number), tax codes from cashctrl_list_tax_codes, persons from cashctrl_list_persons, order categories and their statuses from cashctrl_list_order_categories, currencies from cashctrl_list_currencies, a person's category_id from cashctrl_list_person_categories, its title_id from cashctrl_list_person_titles and an employee's location_id from cashctrl_list_locations, an allocation's to_cost_center_id from cashctrl_list_cost_centers; the record to update or delete from cashctrl_list_journal_entries, cashctrl_list_persons or cashctrl_list_orders (the id, not the nr). A journal associate_id is a person's id; create a new person with cashctrl_create_person first (upstream would create one from a name, which the tools do not send).
  • CashCtrl checks every write and refuses with the fields it objects to, e.g. error: RequestRejected: CashCtrl rejected the request: debitId: This field cannot be empty.; a refused write changed nothing. A write that meets a server error or a timeout is never repeated (ADR-0011): read back before sending it again.
from datetime import date
from decimal import Decimal

from xippios.cashctrl import tasks_write
from xippios.cashctrl.models import WritePreview, WriteResult

entry = dict(
    date_added=date(2026, 10, 24),
    amount=Decimal("100.00"),  # gross, VAT included, with a tax_id
    debit_id=3,  # account ids from list_accounts, not account numbers
    credit_id=42,
    tax_id=9,  # from list_tax_codes
    title="xippios-live README example",  # marked, should it ever run against demo
)
preview = tasks_write.create_journal_entry("demo", **entry)
assert isinstance(preview.data, WritePreview)
print(preview.data.payload)  # what CashCtrl would receive; nothing was written
done = tasks_write.create_journal_entry("demo", **entry, confirm=True)
assert isinstance(done.data, WriteResult)
print(done.data.insert_id)  # the new entry's id

Files

Five tasks write a file instead of returning records: download_order_document, the documents of orders as one PDF, and the list exports export_journal_entries, export_accounts, export_persons and export_orders, each with format (csv, xlsx or pdf), the filters of the list it mirrors and columns. They are Python only: no MCP tool and no HTTP route serves them, since no tool hands an agent a file path (BULK_TASKS, ADR-0019). Each streams the file to dest, the path the caller gives; nothing is read into the result, kept in memory or cached, and data describes the file (path, bytes, content_type, filename, retrieved_at). A refusal raises and writes nothing, and a file already at dest stays as it was: RequestRejected for one CashCtrl sends as HTTP 200, NotFoundError for an unknown order id, which CashCtrl answers with HTTP 404.

  • Cut at 100. Upstream exports at most limit records, 100 when limit is left out (the account export, like its list, takes no limit): read the list task's meta.total first and pass a limit of at least that.
  • Personal identifiers. Without columns, the person export names the fields list_persons returns as its columns, so ssn and iban stay out of the file; a caller that names them in columns gets them, deliberately (ADR-0017). An order document is what its recipient gets: an invoice's QR-bill may carry the organisation's IBAN or QR-IBAN and the recipient's name and address. Where a file goes is the caller's choice.
from pathlib import Path

from xippios.cashctrl import tasks

count = tasks.list_journal_entries("demo", fiscal_period_id=2, limit=1).meta.total or 1
journal = tasks.export_journal_entries(
    "demo", "csv", Path("journal-2025.csv"), fiscal_period_id=2, limit=count
)
print(journal.data.path, journal.data.bytes, journal.data.content_type)
invoice = tasks.download_order_document("demo", [3], Path("RE-2026-103.pdf"))

HTTP

xippios.cashctrl.http.router is a FastAPI router the consumer mounts. It has no lock: every route but GET /cashctrl/list_organisations takes org, and the consumer passes it explicitly on each request. A deployment that must reach one organisation only declares only that one. A task whose parameters are all scalars is a GET /cashctrl/<task> with query parameters; the list tasks that take a filter, a list (list_accounts, list_cost_centers, list_journal_entries, list_persons, list_orders, list_history), are a POST with a JSON body keyed by parameter name. The MCP entry points quiet httpx's request log; the router does not, so a consumer that logs at INFO logs httpx's request lines, whose URLs carry the organisation's host (its subdomain): set the httpx logger to WARNING.

Status

Upstream operation Task function MCP tool Status
none: the declared organisations list_organisations cashctrl_list_organisations available; names only, no request
GET fiscalperiod/list.json (List fiscal periods) list_fiscal_periods cashctrl_list_fiscal_periods available
GET currency/list.json (List currencies) list_currencies cashctrl_list_currencies available
GET tax/list.json (List tax codes) list_tax_codes cashctrl_list_tax_codes available
GET account/list.json (List accounts) list_accounts cashctrl_list_accounts available; HTTP route is POST
GET account/read.json (Read account) get_account cashctrl_get_account available
GET journal/list.json (List journal entries) list_journal_entries cashctrl_list_journal_entries available; paged; HTTP route is POST
GET journal/read.json (Read journal entry) get_journal_entry cashctrl_get_journal_entry available
GET person/list.json (List people) list_persons cashctrl_list_persons available; paged; HTTP route is POST
GET person/read.json (Read person) get_person cashctrl_get_person available
GET order/list.json (List orders) list_orders cashctrl_list_orders available; paged; HTTP route is POST
GET order/read.json (Read order) get_order cashctrl_get_order available
GET order/document/read.json (Read document) get_order_document cashctrl_get_order_document available; layout, sender, recipient, payment references; without orgIban, orgQrIban and recipientIban; the PDF from download_order_document
GET order/bookentry/list.json (List book entries) list_order_book_entries cashctrl_list_order_book_entries available; one order's (order_id, sent as id)
GET order/category/list.json (List categories) list_order_categories cashctrl_list_order_categories available
GET customfield/list.json (List custom fields) list_custom_fields cashctrl_list_custom_fields available; one module (type) a call
GET person/category/list.json (List categories) list_person_categories cashctrl_list_person_categories available
GET person/title/list.json (List titles) list_person_titles cashctrl_list_person_titles available
GET account/category/list.json (List categories) list_account_categories cashctrl_list_account_categories available; flat, linked by parent_id
GET account/costcenter/list.json (List cost centers) list_cost_centers cashctrl_list_cost_centers available; HTTP route is POST; balances assumed of the current fiscal period
GET location/list.json (List locations) list_locations cashctrl_list_locations available; without iban and qr_iban
GET report/tree.json (Get tree of reports) get_report_tree cashctrl_get_report_tree available
GET report/element/data.json (Read JSON data) get_report_data cashctrl_get_report_data available; rows as upstream sends them; payroll and people reports refused
GET history/list.json (List history) list_history cashctrl_list_history available; paged; HTTP route is POST
GET order/document/read.pdf (Download PDF) download_order_document none, Python only available; one PDF to dest; an invoice's QR-bill may carry the organisation's IBAN or QR-IBAN and the recipient's address
GET journal/list.csv, list.xlsx, list.pdf (Export to CSV, Excel, PDF) export_journal_entries none, Python only available; a file to dest; at most limit, upstream's default 100
GET account/list.csv, list.xlsx, list.pdf (Export to CSV, Excel, PDF) export_accounts none, Python only available; a file to dest; every matching account
GET person/list.csv, list.xlsx, list.pdf (Export to CSV, Excel, PDF) export_persons none, Python only available; a file to dest; at most limit, upstream's default 100; without ssn and iban unless columns names them
GET order/list.csv, list.xlsx, list.pdf (Export to CSV, Excel, PDF) export_orders none, Python only available; a file to dest; at most limit, upstream's default 100
POST journal/create.json, update.json, delete.json create_journal_entry, update_journal_entry, delete_journal_entry cashctrl_create_journal_entry, ... available on the write server only
POST person/create.json, update.json, delete.json create_person, update_person, delete_person cashctrl_create_person, ... available on the write server only
POST order/create.json, update.json, delete.json create_order, update_order, delete_order cashctrl_create_order, ... available on the write server only
POST order/update_status.json (Update status) update_order_status cashctrl_update_order_status available on the write server only
POST order/payment/create.json (Create payment) create_order_payment cashctrl_create_order_payment available on the write server only; no book entry, no delete documented

lang (de, fr, it, en), which upstream takes on every request and which matters mostly for error messages and exports, is not exposed yet. list_accounts checks dir (ASC, DESC) and each filter's comparison (eq, like, gt, lt) before anything is sent, since upstream silently ignores values it does not know; both are typed as these value sets, so the tool schema lists them. A filter is a Filter or, from Python, a mapping with the same keys.

The paged lists (list_journal_entries, list_persons, list_orders, list_history) take start (default 0) and limit. With limit they return one page of at most limit records from start; without it, every record from start on, fetched 100 at a time (upstream's documented page size, about 1 s per 100 at the self-imposed pace). meta.total is upstream's count of every record the request matches. The next page starts at start plus the number of records received: over MCP the result cap (200 rows or 64 KB) may return fewer than limit, so never advance by limit. MCP agents should always pass a limit of about 20. Offset pages over a list that changes meanwhile can repeat or skip records. Every other list returns its whole answer in one request and reports upstream's total too.

The journal and order lists answer for the API user's current fiscal period unless fiscal_period_id is given; that period is server-side state that fiscalperiod/switch.json changes, and meta.request does not show which period answered, so pass fiscal_period_id (an id from list_fiscal_periods) for a reproducible answer. Without type (SALES, PURCHASE), list_orders returns both; list_order_categories lists SALES (upstream's default) unless given PURCHASE.

list_order_book_entries lists one order's book entries (order_id, sent as upstream's id): the journal entries its items booked (type GENERATED) and those booked to it by hand, such as payments, each with its rows; an order in a status that books nothing has none. list_history is the audit log, newest first: who changed what and when (created_by, change_type, message), narrowed with order_id, person_id or statement_id; the history of a deleted order is no longer listed (observed).

get_report_tree lists the organisation's reports, collections of report elements; get_report_data reads one element (element_id, a balance sheet, a profit and loss statement, a VAT settlement, ...) for fiscal_period or for start_date to end_date. Giving the fiscal period and a date is refused before anything is sent: upstream derives the dates from the period and would ignore the ones given. The report comes back as upstream sends it (data, the rows, nested; properties, the columns shown; total), under upstream's camelCase names and with amounts as JSON numbers: the documentation gives no schema, and the rows differ by report type. Payroll and people reports, which carry personal data under undocumented names, are refused before their data is requested: every report whose type (as get_report_tree shows it) starts with SALARY_ or contains PERSON, or is SETTLEMENT, WITHHOLDING_TAX_SETTLEMENT, PEOPLE or ORG_CHART; WITHHOLDING_TAX_RATES, rates without persons, is read. The type comes from the organisation's report tree, read once per organisation and process and kept; a report the tree does not list is asked of upstream. Every key named ssn or ending in iban (qrIban, orgIban) is dropped from what comes back, in any letter case and at any depth. list_account_categories is the flat account/category/list.json, each category with its parent_id; account/category/tree.json nests the same categories with display fields only. list_custom_fields takes the module (type: JOURNAL, PERSON, ORDER, ...) and refuses any other value; list_cost_centers checks dir and the filters as list_accounts does.

Upstream terms and attribution

Data comes from CashCtrl (https://cashctrl.com) under its own terms of use; this package adds no rights to it. Documentation snapshot: docs/upstream/.

Licence

Apache-2.0. See LICENSE.

Metadata

Release files for xippios-cashctrl 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for xippios-cashctrl 0.3.0
File Size Uploaded Provenance Actions
xippios_cashctrl-0.3.0.tar.gz 222.4 kB With provenance Details for xippios_cashctrl-0.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for xippios-cashctrl 0.3.0
File Interpreter ABI Platform Actions
xippios_cashctrl-0.3.0-py3-none-any.whl Python 3 none any Details for xippios_cashctrl-0.3.0-py3-none-any.whl

Total release size: 387.3 kB

Release files / xippios_cashctrl-0.3.0.tar.gz

Download URL xippios_cashctrl-0.3.0.tar.gz
Size 222.4 kB
Tags Source
SHA-256 checksum
How to use checksums
5b994cc29c19be9c128724cb37086bd94eea1a9f90b06a09a70af8a537e0ef6c
BLAKE2b-256 checksum
How to use checksums
afb9e5720dad2df6507f37800871c8ba771a8971c3392b77e7402cb7b342d242
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.

Transparency log

Release files / xippios_cashctrl-0.3.0-py3-none-any.whl

Download URL xippios_cashctrl-0.3.0-py3-none-any.whl
Size 164.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8c7c8488e522b388b523d76f1a3d09528b2333a1721bcb5d7968cdff092a2091
BLAKE2b-256 checksum
How to use checksums
40157656e7244e4aea703564cddc18ef7be734aedfb88904271df55624a019ca
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.

Transparency log

Security for xippios-cashctrl 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for xippios_cashctrl-0.3.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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