swissbitcoinpay-checkout
Take Lightning and on-chain payments for a Swiss Bitcoin Pay merchant from an online shop's server, in Python.
It implements the Swiss Bitcoin Pay checkout specification
0.1.0 (protocol 1), pinned in spec/
(spec/PIN.json): with the
integration's secret key alone, your server makes the invoice of each order (a Lightning invoice,
a deposit address of the merchant's wallet, or both), declares it in the merchant's registry on
the merchant's Nostr relays, and checks its payment. Swiss Bitcoin Pay runs no server and no API:
payments go straight to the merchant's wallet. The key can make invoices that pay the merchant;
it can never spend funds.
- Install
- The key is a secret
- Quick start
- Synchronous or asynchronous
- Orders
- Prices in a currency
- Payments and their states
- On-chain payments
- Checkout links
- Errors
- Options
- What goes where
- Dependencies
- Known limits
- The specification
- Development
- Releases
Install
python -m pip install swissbitcoinpay-checkout
Python 3.11 to 3.15. Every dependency ships wheels for Linux (x86_64 and aarch64), Windows
(x86_64) and macOS (Apple silicon): nothing is compiled at install time there. Elsewhere (an Intel
Mac, Windows on ARM), cryptography publishes no wheel of the locked version and builds from its
sources, which needs a Rust toolchain. The package is typed (py.typed).
The key is a secret
The merchant creates the integration in the Swiss Bitcoin Pay app and gives you its secret key,
sbp_ck_…. Whoever holds it can make invoices and declare sales for the merchant, and read the
deposit addresses the app gives the integration.
- Keep it in your server's secrets: an environment variable, a vault, your platform's secret store. Never in code, a repository, a log, an error report, or anything sent to the browser.
- The package never puts it, or any key derived from it, in a message, a log, an exception or a
repr; a client cannot be pickled; an invalid key or option leaves no frame of the package holding the key's text (error trackers record the local variables of each frame). - If it leaks, ask the merchant to remove the integration in the app (the next invoice of any
client then fails with
RevokedError) and to create a new one.
One key, one shop. The merchant creates an integration for each shop (each system with its own orders' references), and two shops never share a key: an order's records in the merchant's registry are derived from the key and the order's reference, so two shops with one key would share their references, and an order of one would be found paid, or pending, with an invoice of the other. The servers of one shop share its key.
Quick start
A synchronous server (Django, Flask, any WSGI server, a task queue):
import os
from swissbitcoinpay_checkout import Client, Price
client = Client(os.environ["SBP_CHECKOUT_KEY"])
# Make the order's invoice, store it with the order, then show it.
invoice = client.create_invoice(ref="order-1042", fiat=Price("24.50", "CHF"), note="Order 1042")
order.invoice = invoice.to_json() # the JSON of the specification: store it as it is
order.save()
qr_code(invoice.payment_uri) # one QR code: Lightning, on-chain, or both
# Later (a page refresh, a scheduled job): check the payment of the invoice the order holds.
status = client.get_status(order.invoice)
if status.state == "paid":
order.mark_paid(proof=status.preimage or status.txids)
An asynchronous server (FastAPI, any ASGI server):
import asyncio
import os
from swissbitcoinpay_checkout import AsyncClient
client = AsyncClient(os.environ["SBP_CHECKOUT_KEY"])
async def pay(order_ref: str) -> None:
invoice = await client.create_invoice(ref=order_ref, sats=49_000)
... # store invoice.to_json(), show invoice.payment_uri
async with asyncio.timeout(15 * 60):
status = await client.wait_for_payment(invoice, on_status=print)
Mark an order paid only on a paid status of the invoice that order holds: never because the
customer's browser says so.
Synchronous or asynchronous
The protocol talks to every relay at once and goes on as soon as a majority answered, so the package is written once, on asyncio, and offered two ways, with the same options, methods and errors:
AsyncClient:awaitits methods in your event loop. Cancel a call as any asyncio task (asyncio.timeout,task.cancel()): it stops its requests and raises the cancellation.Client: its methods block until they are done. Each call runs in an event loop of its own, in the calling thread, and takes atimeoutin seconds (none by default; past it, the call stops its requests and raisesTimeoutError). It never starts an event loop inside one that is already running: called from a thread that runs one, it raisesRuntimeErrorrather than block that loop. There, useAsyncClient, or callClientfrom a worker thread (asyncio.to_thread).
A client keeps nothing between calls but its options: make one per process and share it between threads (or tasks). Each call reads the merchant's channel again, opens one connection per relay and closes them when it is done.
Orders
invoice = client.create_invoice(
ref="order-1042", # 1 to 64 of A-Z a-z 0-9 . _ : -; one order, one ref (below)
sats=49_000, # or fiat=Price("24.50", "CHF"): exactly one of the two
duration_seconds=900, # 60 to 3600: how long the invoice can be paid
note="Order 1042", # what the merchant's app shows (the ref when absent)
checkout=False, # True: a checkout link too (below)
redirect_url=None, # with checkout=True: where its page sends the customer once paid
)
sats is an int or a string of digits. Every invalid parameter raises InvalidOrderError
naming it (param), before any request.
One order, one reference. A ref is never reused for another order by anything that holds
the key (a test copy of the shop, a restored database included), and never taken from what the
customer sends: take your order's own identifier.
One order, one invoice at a time. The invoices of an order are read in the merchant's registry, so the rule holds even when your server lost what it stored:
- while an earlier invoice of the order may still be paid (until its end and 120 s), its payment
is checked first, so that an invoice already paid is never handed out again (underpaid
on-chain, its rest invoice is asked then, as
get_statuswould); thencreate_invoiceraisesInvoicePendingErrorwith that invoice (error.invoice, and withcheckout=Truea new link to it,error.checkout_url, whose page shows the rest): show it again. A check that fails is the error (the invoice may have been paid): call again; - past that time, while an on-chain payment of it is being confirmed, it raises
InvoicePendingErrorwith that invoice and no new link (checkout_urlisNone: the link you got goes on following the payment); - once an earlier invoice was paid, it raises
OrderPaidErrorwith that invoice and its status.
Several invoices of an order may still be paid when an earlier call failed once some of the
relays took its invoice, and a later one, which did not read it, made another one: their
payments are checked, and the order's records read again on every relay, at the same time. One
of them paid: OrderPaidError, whatever the relays answer; a check failing: its error; fewer
than a majority of the relays answering: NetworkError; else the one the merchant's relays hold
on a majority, the one shown, is handed out.
Within one client, calls for the same ref run one after the other. Across processes, lock the
order in your database around create_invoice.
Store the invoice as invoice.to_json(): the JSON of the specification, the same in every
language, which get_status and wait_for_payment take back as it is (an Invoice, a
DeclaredInvoice or that JSON). Invoice.from_json(...) reads it back.
find_invoices(ref) lists the invoices declared for an order, oldest first, to find an order's
invoice again after a failure. It publishes nothing: to show an order's invoice again, call
create_invoice, whose InvoicePendingError carries it once its records are on a majority of
the relays again. merchant() gives the business's name, the Lightning address's
bounds in sats and the number of deposit addresses the integration's lot holds (never the
merchant's Lightning address).
Prices in a currency
from swissbitcoinpay_checkout import Price
invoice = client.create_invoice(ref="order-1043", fiat=Price("24.50", "CHF"))
invoice.sats # 35502 at 69 012 CHF/BTC: rounded up, the merchant never receives less
invoice.fiat # Fiat(amount='24.50', currency='CHF', minor=2450, digits=2)
invoice.rate # RateInfo(source='coingecko', currency='CHF', price='69012', ...)
Price takes strings, never a float: an ISO 4217 code in upper case and an amount with at most
the currency's decimals (the specification's table, currencies.json).
The price of one bitcoin comes from a rate source: "coingecko", "mempool" (USD, EUR, GBP,
CAD, CHF, AUD, JPY), "coinbase", "kraken" or "opennode", or a function of your own. A list
is asked in its order, the next source only when the previous one gives no usable price.
Without rate_source, the client asks DEFAULT_RATE_SOURCES:
coingecko, mempool, coinbase, kraken, opennode. A price its source dates must be less than an
hour old.
from swissbitcoinpay_checkout import Client, RatePrice, UnsupportedCurrencyError
def my_rates(currency: str) -> RatePrice: # or an async function
if currency != "CHF":
raise UnsupportedCurrencyError("no price", currency=currency)
return RatePrice(price="69012.50", time=my_feed.updated_at) # or just the decimal string
client = Client(key, rate_source=[my_rates, "kraken"], coingecko_api_key=os.environ.get("CG_KEY"))
invoice = client.create_invoice(ref="order-1044", fiat=Price("10", "CHF"), rate_source="mempool")
A custom source returns a decimal string or a RatePrice (never a float); a plain function runs
in a worker thread, so it may block. An error of this package it raises goes through as it is;
any other becomes RateError (its __cause__ kept). coingecko_api_key (CoinGecko's Demo key)
is sent to CoinGecko only.
Payments and their states
get_status(invoice) checks the payment now; wait_for_payment(invoice, interval_ms=2000, on_status=...) checks it until it is paid or expired, handing each status to on_status (a
function, or an async one), retrying NetworkErrors.
status.state |
Means |
|---|---|
open |
Not paid yet; it may still be, until expires_at (reported up to 120 s later). |
unconfirmed |
Enough came on-chain in time; fewer than 3 confirmations yet. Not paid yet. |
paid |
Paid: method is lightning (preimage), onchain (txids) or mixed (both). |
expired |
Never paid, for good. received above 0: an underpayment the merchant received. |
status.to_json() gives the specification's JSON. received counts what came in time, in sats.
A preimage (paid in Lightning) is the proof of payment: keep it.
On-chain payments
From 1 000 sats, an order may also be paid on-chain: the merchant's app gives the integration a lot of deposit addresses of its wallet (filled again whenever the merchant opens the app), and each order leases one for the time of its invoice. By amount:
- within the Lightning address's bounds: a Lightning invoice, and from 1 000 sats an address too
(
invoice.payment_uriis then one BIP-21 QR code,bitcoin:…?amount=…&lightning=…); - above its bounds: on-chain only (
OnchainUnavailableErrorwhen no address could be leased: retryable, once the merchant opened the app); - below 1 000 sats: Lightning only.
An address is the order's once its lease is confirmed: published, read again on every relay (no other order's lease of it, the read complete), then marked confirmed. A lease that is not confirmed (another server leased the address at once, or the read could not be complete) is withdrawn, and the next address tried; one that was confirmed is never withdrawn, whatever fails after (the checkout link, the declaration): it holds its address until its end. An address a witness of the integration names (a transaction it saw paying it) is never leased again.
An on-chain payment is unconfirmed, then paid at 3 confirmations seen by two explorers
(mempool.space and Blockstream's), which takes about 30 minutes: wait while the customer pays,
then check again later with get_status. A transaction replaced by another (RBF) keeps its
order; one broadcast after the invoice's end (and its 120 s of grace) is late and pays no order.
A transaction counts for one invoice at most: one that the witness of another invoice names
counts for nothing.
Underpayments. When less than the amount came on-chain in time, the status stays open with
status.rest: what is left (rest.sats), a Lightning invoice of exactly that (rest.bolt11, asked
of the merchant's Lightning address and declared by the check itself) and one QR code for it
(rest.payment_uri, on-chain too from 1 000 sats). Paid in time, the order is paid with
method: "mixed". The merchant may accept an on-chain payment a little short of the amount (its
tolerance, set in its app): read from the channel when the invoice is made, kept in the
order's lease, never written where your page or the customer reads it.
wait_for_payment of an invoice with an address listens to mempool.space's WebSocket and checks
the on-chain part when it says something changed (and every 60 s at least); get_status never
opens it. A failure that each check of a wait meets again (an explorer or the Lightning address
that does not answer, a rest invoice that cannot be asked, a witness that cannot be published) is
logged once, and again only once a check went without it.
Servers of one shop run implementations of one version of the specification.
Checkout links
invoice = client.create_invoice(
ref="order-1045", sats=49_000, checkout=True, redirect_url="https://shop.example/orders/1045"
)
redirect(invoice.checkout_url) # the merchant's web app shows and checks the invoice
The customer pays on the merchant's web app, which sends them to redirect_url once paid (an
https URL with a lowercase host, or an app's link such as myshop://orders/1045).
CheckoutUnavailableError when the merchant's app gives no web app yet. Being sent to the
redirect URL proves nothing (anyone can open it): mark the order paid only after get_status
says paid. Give checkout_url to the order's customer only: it shows the invoice, the
business's name and the note.
Errors
Every error is a SwissBitcoinPayError with a stable code and retryable (whether the same
call may succeed later, unchanged); a message never holds a key. A cancellation is not one: the
asyncio cancellation, or TimeoutError past a Client call's timeout.
| Exception | code |
Retryable | What to do |
|---|---|---|---|
InvalidKeyError |
invalid_key |
no | The key is not an integration key: check the secret you loaded. |
InvalidOptionError |
invalid_option |
no | param names the client's or the call's option. |
InvalidOrderError |
invalid_order |
no | param names the order's parameter, or invoice (stored). |
RevokedError |
revoked |
no | The merchant removed the integration. |
ChannelNotFoundError |
channel_not_found |
unless newer | newer_format: update this package. |
NetworkError |
network |
yes | A relay or a service did not answer usably: try again. |
InvalidInvoiceError |
invalid_invoice |
no | The Lightning address's answer failed a check (check). |
InvoicePendingError |
invoice_pending |
no | Show invoice again (or checkout_url). |
OrderPaidError |
order_paid |
no | The order was paid: invoice, status, preimage. |
UnsupportedCurrencyError |
unsupported_currency |
no | No source has a price in currency. |
RateError |
rate |
yes | No usable price (invalid, too old, a custom source failed). |
CheckoutUnavailableError |
checkout_unavailable |
no | No checkout link for this merchant yet. |
OnchainUnavailableError |
onchain_unavailable |
yes | Only payable on-chain, and no address could be leased for now. |
create_invoice returns an invoice only once it is declared on a majority of the merchant's
relays (and its address leased, its checkout link published): after any error there is nothing to
show. Call it again: the order gets its invoice, or InvoicePendingError with the earlier one.
Options
Client(key, ...) and AsyncClient(key, ...) take the same keyword options; an invalid one
raises InvalidOptionError naming it, before any request.
| Option | Default | Rule |
|---|---|---|
rate_source |
DEFAULT_RATE_SOURCES |
A source, or a list of them (each name once). |
coingecko_api_key |
none | CoinGecko's Demo key: visible ASCII (U+0021 to U+007E). |
relay_timeout_ms |
10 000 | Per relay request, connection included: 100 to 60 000. |
http_timeout_ms |
10 000 | Per HTTP request, body included: 100 to 60 000. |
http |
the standard library's http.client |
An HttpTransport (tests, proxies). Never follow redirects. |
websocket |
websockets, 1 MiB a message, no redirect, no compression |
async (url) -> connection (tests, proxies). |
logger |
logging.getLogger("swissbitcoinpay_checkout") |
Warnings only, never a secret. |
now, random |
time.time, os.urandom |
For tests only. |
Neither default transport uses the environment's proxy settings: to go through a proxy, inject
your own http and websocket.
What goes where
- The merchant's Nostr relays (the key's, or those the merchant's app names): the channel, the declarations, leases, witnesses and checkout links, all encrypted (NIP-44) and signed.
- The merchant's Lightning address (breez.tips): asked for each invoice and its payment.
- The rate sources you use: asked for a price, with your server's IP address.
- mempool.space and blockstream.info (orders with an address): the address and its transactions; api.blockcypher.com: a transaction, only for one no check saw in time.
Never the key: every one of them sees your server's IP address and what it asks. Every HTTP
request, and the opening handshake of each WebSocket, sends User-Agent: SwissBitcoinPay-Checkout/1, the same in every implementation and version of the specification.
Outbound requests. Besides fixed hosts (the rate sources, the explorers), your server asks
hosts that others choose: the merchant's Lightning address and relays, and that address's
provider (its callback, its verify URLs). No redirect is followed, and their URLs are checked
first: no IP address, no localhost nor a name under it, TLS before any request. But a name may
still lead to an address of your own network (an internal name, a public name resolving to a
private address). If your server holds the keys of several merchants, or sits in a network with
services of its own, restrict what it can reach to public addresses: in your network, or in the
http and websocket you inject. The package does not resolve names itself to refuse private
addresses: a name can resolve again to another address (DNS rebinding).
Dependencies
The standard library first: SHA-256 and HMAC (hashlib, hmac), the HTTP client
(http.client, which never follows a redirect), JSON, base64, big integers. Three packages for
the rest, each because the standard library has no such thing:
| Package | Why | Rejected alternatives | License |
|---|---|---|---|
btclib-secp256k1 |
secp256k1: BIP-340 Schnorr signatures, x-only keys, ECDH. A binding (cffi) to libsecp256k1 v0.8.0. | coincurve (its last release has no wheels for Python 3.14 and 3.15, and its sources did not build on 3.14), secp256k1 (no wheels after 3.13), pure-Python curves (never the curve written by hand: not constant time). |
MIT |
cryptography |
HKDF and ChaCha20 (NIP-44 v2), on OpenSSL; maintained by the Python Cryptographic Authority, and already on most Python servers. | pycryptodome (a second C cryptography library beside the one most servers carry), a ChaCha20 written by hand. |
Apache-2.0 OR BSD-3-Clause |
websockets |
The Nostr relays and mempool.space's WebSocket, on asyncio, with a message limit. | aiohttp (a whole HTTP stack the package does not need), wsproto (the I/O to write ourselves). |
BSD-3-Clause |
Their own dependencies: cffi (MIT-0) and pycparser (BSD-3-Clause).
What "audited" rests on. No published audit covers btclib-secp256k1 itself: what it rests on
is that it binds libsecp256k1, the library of
Bitcoin Core, developed and reviewed in the open for Bitcoin, whose signing and key generation
are constant time (its README). The release this package locks (0.8.0.10) builds libsecp256k1
v0.8.0: its sources in the package are identical to the upstream tag (commit 6e2c8bc), and
every one of its 66 files on PyPI carries a PEP 740 attestation of its build by the
btclib-org/btclib-secp256k1 repository's release workflow. The binding's own code (some 8 000
lines of Python around cffi) is that project's, far less used than libsecp256k1 itself: its pin is
exact, and an update is read before it is taken. The package tests it with the official BIP-340 vectors and the official NIP-44
vectors (tests/vectors/, spec/vectors/nip44/).
Versions. uv.lock pins every package exactly, with its hashes, and the CI installs it as it
is; no version less than 7 days old is ever locked (exclude-newer), and Dependabot waits as
long. What the package declares to pip leaves the integrator's own resolver room where it is
safe: btclib-secp256k1 exactly (few projects share it, and its fourth version number may break
its interface), cryptography from 50.0.2 on (shared by many packages, and its security fixes
must reach the merchant's server without waiting for a release of this one), websockets from
17.2 and below 18.
Known limits
- Constant time. The curve operations (libsecp256k1) and the ciphers and MACs (OpenSSL) are
constant time; MACs are compared with
hmac.compare_digest. The Python code around them is not, and Python cannot erase a key from memory (itsbytesare immutable): the key lives in the server's memory as long as the client does. Keep it in a server you trust. - Platforms. The CI runs every supported Python version on Linux (x86_64) and Windows (x86_64). macOS, ARM machines, PyPy and the free-threaded builds of Python are not tested by the CI (see Install for the wheels).
- Waiting. An on-chain payment takes about 30 minutes to be
paid: a server that cannot wait that long checks again later withget_status.wait_for_paymentkeeps one WebSocket open and opens new relay connections at each check. - Timeouts. Each relay request and each HTTP request is bounded by its timeout. A request
that times out or is cancelled shuts its socket at once, wherever it is (connecting, TLS,
reading); a name resolution under way in the operating system cannot be interrupted: its thread
ends when the resolution returns, and makes no connection. A
create_invoicecancelled (or past aClientcall'stimeout) once it published a lease, before that lease was confirmed, publishes nothing more: the lease is not withdrawn, never gives its invoice an address, and holds the address until its end (4 320 s at most). - Reads on every relay. Three reads decide that a record is nobody else's: a lease's
confirmation, the witnesses read again after a check published one, and the choice among
several invoices of an order. They wait for every relay of the merchant's list, one
relay_timeout_msafter a majority answered at most, so that a relay that hides a record cannot hide it while another one that holds it answers in time; a relay that ran out of time, or lost its connection, earlier in the call is not waited for again. Every other read goes on at a majority. - Leases anybody can write. Anybody may write events where a lease is (its place is public):
the leases of the lot are read page after page of 100 events, newest first, and a read they
fill (one second holding more than a page, 20 full pages) is not complete; the order then goes
without that address, never with one another order holds. It relies on relays that answer a
filter's
limitwith their newest events, as many as the limit when they hold as many (strfry, nostr-rs-relay): the merchant's relays are such relays. - What third parties send. Every answer is checked before it is used, within the bounds the specification sets (1 MiB a message or an HTTP body, 1000 events a query, 20 pages). A read keeps of each event only what it needs, as the event comes (its id and date, and the record it opens to; of a lease, which anybody can copy under another id, a few bytes): what a relay sends never piles up, whatever its size, and an answer is never cut at a size (events anybody wrote where a lease is end a read of leases incomplete, never failed). The relays' WebSocket offers no compression, whose few bytes could unpack into many messages held at once. Parsing what is received runs on the event loop: a BOLT11 invoice of 1 MiB takes some 0.6 s of CPU, an explorer's answer of 1 MiB made for it about 1 s. mempool.space lists 75 transactions of an address at most (50 unconfirmed, 25 confirmed); a check publishes a witness, and may ask BlockCypher, for each one it has not seen yet, one after the other.
The specification
spec/ holds the files of
the specification, unchanged, at commit d5dd0e9491423d03083662f2a621e7a9c8eea14a (version 0.1.0)
with their SHA-256 in
spec/PIN.json;
tests/test_spec_pin.py fails on any difference, and every
vector file is read by a test. To follow a new version:
python scripts/pin_spec.py /path/to/a/checkout-spec/clone <commit>
then update the code until the new vectors pass, in one pull request. A disagreement between the specification and this package is reported to checkout-spec (privately, by its SECURITY.md, for anything that could cost a merchant money or leak a key).
Development
With uv 0.13.0 (.github/ci-requirements.txt):
uv sync --locked
uv run --locked pytest # never the network: local relays and fakes
uv run --locked ruff format --check . && uv run --locked ruff check .
uv run --locked mypy
Releases
Releases are the maintainers' only: a maintainer pushes a tag vX.Y.Z (the version of
pyproject.toml) on a commit of main;
release.yml runs the
CI again, checks the tag, builds the package from the lockfile, and publishes it to PyPI by
trusted publishing, in the pypi environment, once a maintainer approves.
License
MIT, © 2026 Swiss Bitcoin Pay Sàrl.
Metadata
Release files for swissbitcoinpay-checkout 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| swissbitcoinpay_checkout-0.1.0.tar.gz | 462.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| swissbitcoinpay_checkout-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 590.5 kB
Release files / swissbitcoinpay_checkout-0.1.0.tar.gz
| Download URL | swissbitcoinpay_checkout-0.1.0.tar.gz |
|---|---|
| Size | 462.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d254097a14bf331bfb03539c3adc6a670b1c8cf3be327907fa053034de08d169
|
|
BLAKE2b-256 checksum How to use checksums |
f79161a9899b57911f56a331adf05bbb1f45407f0ef357879bc4f1348640697c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 17, 2026.
Transparency logRelease files / swissbitcoinpay_checkout-0.1.0-py3-none-any.whl
| Download URL | swissbitcoinpay_checkout-0.1.0-py3-none-any.whl |
|---|---|
| Size | 127.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a32850934e2187452108dac8b2d560ec0628a22e97b1fcd340dd52a72c7b4bc9
|
|
BLAKE2b-256 checksum How to use checksums |
f3ed5531d8d5a60071b3a6ab6cdbbfc925ea6353d7e6e9fb7c374b4d110cccab
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 17, 2026.
Transparency log