Swiss Bitcoin Pay for pretix
A payment plugin for pretix: customers pay their orders in bitcoin, in Lightning or on-chain, and the bitcoin goes straight to the organizer's Swiss Bitcoin Pay wallet. It is built on the Swiss Bitcoin Pay checkout SDK (source): no Swiss Bitcoin Pay server, no API key of a third party, and the integration key can make invoices that pay the organizer, never spend the funds.
- How it works
- Requirements
- Installation
- Configuration
- Payments and orders
- Background checks
- Security
- Known limits
- Development
- Releases
- License
How it works
- At checkout, the customer chooses Bitcoin. Once they confirm their order, the plugin asks the SDK for the invoice of the payment: a Lightning invoice, a deposit address of the organizer's wallet, or both, depending on the amount (see the SDK).
- The customer lands on a payment page of the shop, in its design and language: one QR code
(BIP-21 with
lightning=, orlightning:), a button to open their wallet, the invoice and the address to copy, and the time left to pay. - The page follows the payment on its own. Once the SDK reports it paid, the plugin confirms the pretix payment: the order is paid, the tickets are sent as usual.
The page is rendered on the server (the QR code is an inline SVG); its small script only counts down, copies, and reloads the page when the payment moved on. Without JavaScript, the page offers a button to check the payment.
Requirements
- pretix 2026.3 or later (tested with 2026.3 and 2026.8), on Python 3.11 or later.
- pretix's task workers (Celery) and periodic tasks (
runperiodic, from cron) running, as in any production installation of pretix (see Background checks). - A merchant account of Swiss Bitcoin Pay, and an integration created in its app.
- Outbound HTTPS and WebSocket connections from the pretix server to the merchant's Nostr relays, its Lightning address (breez.tips), the rate sources and the explorers (mempool.space, blockstream.info, api.blockcypher.com): see the SDK's What goes where.
Installation
Into the Python environment of your pretix installation:
pip install pretix-swissbitcoinpay
python -m pretix migrate
python -m pretix rebuild
then restart pretix's web and task workers. With pretix's Docker image, add
RUN pip3 install pretix-swissbitcoinpay to your own Dockerfile based on pretix/standalone,
as for any plugin; the image migrates on start.
Configuration
- In the Swiss Bitcoin Pay app, create an integration for your shop and copy its secret key
(
sbp_ck_…). - In pretix, open your event, Settings → Plugins, and enable Swiss Bitcoin Pay.
- Open Settings → Payment → Bitcoin (Swiss Bitcoin Pay), enable the payment method, and enter the key. It is checked when you save it (an integration the merchant removed is refused), and never shown again.
- Time to pay: how long the customer has to pay an invoice, 1 to 60 minutes (15 by default).
Change the key only while no Bitcoin payment is pending: payments are checked with the key set at the time of the check, and an on-chain payment made under another integration's key would be judged without the records of its invoice.
pretix's usual settings of a payment method apply as well: fees, availability dates, minimum and maximum totals, sales channels, countries.
Test mode. Swiss Bitcoin Pay has no test network: in pretix's test mode too, payments are real bitcoin payments, to the organizer's wallet. The checkout says so.
Payments and orders
| What the SDK reports | What the plugin does |
|---|---|
| An invoice was made | The pretix payment is pending; the customer goes to the payment page. |
open |
Waits. Another payment method only once the invoice ended unpaid. |
open with received (on-chain, short) |
The page shows what came and a QR code for the rest (the SDK asks for it). |
unconfirmed |
Waits for 3 confirmations (about 30 minutes), keeping the order from expiring. |
paid |
Confirms the pretix payment: the order is paid. |
expired |
The pretix payment fails (no email): the customer can pay again. |
expired with received above 0 |
Fails too, and logs on the order what reached the wallet, to settle. |
- Only a
paidstatus confirms a payment, from the SDK's own check of the invoice that payment holds: never because the customer's browser says so. - One payment, one invoice. Each invoice is made for a random reference, never used for another one: no deleted test order, restored backup or other installation can share it. Two requests for one payment at once (a double click) get the same invoice: the payment is locked in the database while its invoice is made.
- One payable invoice per order. A Lightning invoice cannot be revoked: the customer can choose another payment method only once the invoice has ended unpaid (the order's page says when), so that the order is never paid twice. When the organizer cancels or changes the order meanwhile, the invoice stays payable until its end and its checks go on: a payment that comes all the same is confirmed, and pretix shows an overpaid or canceled order to settle.
- Deadlines. An invoice never outlives the moment pretix would refuse its payment or free the order's products: the order's last date of payments and its expiry (but for an order past its expiry when late payments are accepted: that is a late payment, as pretix allows). The plugin checks that each invoice ends in time, the SDK's 120 s of grace and the clock of the merchant's Lightning address included. A payment made in time is confirmed however long its confirmations took, and while an on-chain payment is being confirmed, its order is kept from expiring.
- Refunds are not automatic: refund the customer from the wallet, then record the refund in pretix.
- The organizer sees in the payment's details the invoice's reference, amount and exchange rate, the address, the transactions (linked to mempool.space), the proof of payment (preimage) and the last check.
Background checks
The SDK has no webhooks: payments are checked, by the plugin, on the pretix server.
- While the customer's payment page is open, every few seconds.
- In the background, each invoice by a chain of Celery tasks: every minute while it can be paid, once just before the end of its grace period (an on-chain payment a check lists by then came in time, without asking BlockCypher) and once just after, then every five minutes until its final status is known (an on-chain payment being confirmed); after failed checks, less and less often (up to every 30 minutes).
- pretix's periodic tasks restart the checks of an invoice whose chain stopped (a lost task). When pretix runs without Celery workers, they run the checks themselves, as often as cron runs them, for about a minute per run (the invoices due the longest first): payments are then confirmed later.
An invoice whose final status is still unknown after 90 days (an on-chain payment made in time, still unconfirmed on a congested network) is no longer checked; it is logged on its order, whose payment stays pending: check it in the Swiss Bitcoin Pay app and settle it in pretix by hand.
If pretix confirms a payment but fails to mark its order paid (a plugin reacting to the payment fails, the database goes away), the next check marks it paid, as pretix's backend would.
Security
- The integration key is stored in pretix's settings, like the keys of pretix's other payment providers, and never shown again once saved. The plugin never puts it in a log, an exception or a page; the SDK never puts it, or a key derived from it, in a message or a representation. Note that pretix itself records the new values of a payment method's settings in its audit log, which pretix's administrators can inspect, and in its database.
- One key, one shop. Create an integration for your pretix shop, and use its key nowhere else. If it leaks, remove the integration in the Swiss Bitcoin Pay app and create a new one: the key can make invoices that pay you, never spend your funds.
- Payment links carry a secret of the order tagged for this plugin (as pretix's own payment
plugins do): an order's code, or a link of another plugin, never opens them. Like the order's
own link, a payment link opens the order: it is the customer's. The pages are
noindexand never cached, and use no inline script (pretix's Content Security Policy). - Outbound requests. The SDK connects to hosts the merchant chooses (its relays, its Lightning
address). It refuses IP addresses and
localhost, follows no redirects and checks TLS, but a name may still lead to an address of your own network: if your pretix server sits in a network with services of its own, restrict what it can reach to public addresses (see the SDK's Outbound requests). - Report a vulnerability privately: see SECURITY.md.
Known limits
- No proxy. The SDK's transports do not use the environment's proxy settings: the pretix server needs direct outbound connections.
- Currencies. The event's currency must be one the rate sources price (the SDK's table of currencies). An amount the SDK cannot convert, or out of the merchant's limits, is refused at checkout with a message, and the customer chooses another method.
- Confirmations. An on-chain payment is paid at 3 confirmations, about 30 minutes: the order stays pending until then.
- Gunicorn's timeout. Making an invoice runs in a web request, bounded at 20 seconds (gunicorn stops a worker after 30 seconds by default). Checks run in Celery, but for one a customer without JavaScript asks for, and all of them on an installation without Celery workers: those run in the request, or in pretix's periodic tasks, bounded at 20 seconds each (and at about a minute in all per run of the periodic tasks).
- Deposit addresses. Each invoice of 1,000 sats or more may lease an address of the integration's lot for its time: many orders left unpaid at once can empty it, the next ones are then payable in Lightning only until the merchant opens the app (see the SDK).
Development
The tests, with a pretix development setup, or simply:
python -m venv venv && . venv/bin/activate
pip install "pretix==2026.8.*" pytest pytest-django
pip install -e .
make localecompile # needs GNU gettext
python -m pytest tests # SQLite, migrations off; never the network (the SDK is faked)
-
On PostgreSQL, with pretix's migrations: write a pretix configuration file with a
[database]section, thenPRETIX_CONFIG_FILE=/path/to/pretix.cfg GITHUB_WORKFLOW=local python -m pytest tests(the concurrency test needs PostgreSQL's row locks). -
The checks and the build, as the CI runs them, with uv 0.13.0 (
.github/ci-requirements.txt) and the tools ofuv.lock(in.venv, apart from the tests'):uv sync --locked --no-install-project uv run --locked --no-sync black --check . && uv run --locked --no-sync isort -c . && uv run --locked --no-sync flake8 . uv run --locked --no-sync check-manifest --no-build-isolation uv run --locked --no-sync python -m build --no-isolation --outdir dist . && uv run --locked --no-sync python scripts/check_dist.py dist
-
Translations (German, informal German, French, Italian):
make localegenupdates the.pofiles from the code and the templates,make localecompilecompiles them.
Releases
Releases are the maintainers' only: a maintainer pushes a tag vX.Y.Z (the version of
pretix_swissbitcoinpay/__init__.py) 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. Release the SDK
first when this plugin needs a version of it that PyPI does not have yet.
License
MIT, © 2026 Swiss Bitcoin Pay Sàrl.
Metadata
Release files for pretix-swissbitcoinpay 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 | |
|---|---|---|---|
| pretix_swissbitcoinpay-0.1.0.tar.gz | 65.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pretix_swissbitcoinpay-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 149.8 kB
Release files / pretix_swissbitcoinpay-0.1.0.tar.gz
| Download URL | pretix_swissbitcoinpay-0.1.0.tar.gz |
|---|---|
| Size | 65.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a1da2c40254859ca6a1c5fed9691fc7d5d0257bab8c1facbca6c6d8979c0ae93
|
|
BLAKE2b-256 checksum How to use checksums |
f83bcc1226ab5724f39abffc3a82f4985eba0897058b3b3e60afda417d1fb349
|
| 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 18, 2026.
Transparency logRelease files / pretix_swissbitcoinpay-0.1.0-py3-none-any.whl
| Download URL | pretix_swissbitcoinpay-0.1.0-py3-none-any.whl |
|---|---|
| Size | 84.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e04e4f69d5f415e773cd095dbdc05ae4c88e997ea96a745c9c14dfcd336013c2
|
|
BLAKE2b-256 checksum How to use checksums |
1a56f0825f0e4c64542c20096abe67390da06a8f1fd4428e9c82123519f5678c
|
| 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 18, 2026.
Transparency log