Skip to main content

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

  1. 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).
  2. The customer lands on a payment page of the shop, in its design and language: one QR code (BIP-21 with lightning=, or lightning:), a button to open their wallet, the invoice and the address to copy, and the time left to pay.
  3. 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.

In pretix's widget, on the organizer's own site, it is the same: the payment page shows in the widget's frame (or in a new tab, where the widget opens the checkout so), and its copy buttons work there too.

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. To update the plugin, the same commands, with pip install -U pretix-swissbitcoinpay.

With pretix's Docker image, build your own image with the plugin, as pretix's documentation shows for any plugin (Install a plugin):

FROM pretix/standalone:stable
USER root
RUN pip3 install pretix-swissbitcoinpay
USER pretixuser
RUN cd /pretix/src && make production

then run that image instead of pretix/standalone: it migrates the database on start. Build it again to update the plugin, or pretix.

Configuration

  1. In the Swiss Bitcoin Pay app, create an integration for your shop and copy its secret key (sbp_ck_…).
  2. In pretix, open your event, Settings → Plugins, and enable Swiss Bitcoin Pay.
  3. 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.
  4. 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 paid status 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: a Lightning payment can only be refunded to an invoice of the customer's, an on-chain one to an address of theirs, which pretix never asks. Refund the customer from the wallet, then record the refund in pretix as a manual refund.
  • 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 noindex and 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.

Privacy

  • What the plugin stores, in pretix's database: each payment's invoice as the SDK made it (its reference, amounts in sats and in the event's currency, exchange rate, Lightning invoice and, from 1,000 sats, a deposit address of the merchant's wallet) and what its checks found (its state, what came, the confirmations, the transactions, the proof of payment). No name and no email address: the customer's are pretix's own.

  • What the pretix server sends, and to whom (the SDK's What goes where):

    • the merchant's Nostr relays, encrypted for the merchant (NIP-44): each invoice, with its price and its note (the event's name and the order's code), which the merchant's app shows;
    • the merchant's Lightning address (breez.tips for a Swiss Bitcoin Pay account): asked for each invoice, its amount and, when the address accepts comments (LUD-12), its note, then for its payment;
    • the SDK's rate sources, one at a time until one answers (CoinGecko, then mempool.space, Coinbase, Kraken, OpenNode): asked for the price of bitcoin;
    • mempool.space and blockstream.info, for an invoice with an address: the address and its transactions; api.blockcypher.com: a transaction, only for one no check saw in time.

    Each of them sees the pretix server's IP address, never the customer's: the customer's browser talks to the shop only (the payment page loads nothing from elsewhere).

  • Removing personal data. pretix's data shredder « Payment information » (once the event is over and its shop offline) also removes what leads to the customer's coins on the blockchain: the deposit address and the transactions, from the payments' information, from pretix's log entries of them and from their invoices. Their references, amounts, exchange rates, dates and states stay, for the organizer's accounting; so do their Lightning invoices, which name the merchant's node, never the customer's. An invoice still being checked then (an on-chain payment being confirmed) loses them once final. What the merchant's relays received stays theirs: the merchant's app keeps its sales.

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, then PRETIX_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 of uv.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 localegen updates the .po files from the code and the templates, make localecompile compiles 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. The Swiss Bitcoin Pay name and logo (pretix_swissbitcoinpay/static/pretix_swissbitcoinpay/swissbitcoinpay.svg, shown in pretix's list of plugins) are Swiss Bitcoin Pay Sàrl's brand: not under that licence.

Metadata

Release files for pretix-swissbitcoinpay 0.2.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 pretix-swissbitcoinpay 0.2.0
File Size Uploaded
pretix_swissbitcoinpay-0.2.0.tar.gz 77.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pretix-swissbitcoinpay 0.2.0
File Interpreter ABI Platform
pretix_swissbitcoinpay-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 171.8 kB

Release files / pretix_swissbitcoinpay-0.2.0.tar.gz

Download URL pretix_swissbitcoinpay-0.2.0.tar.gz
Size 77.3 kB
Tags Source
SHA-256 checksum
How to use checksums
06d875b3641c38a118f870b580be8264c0a5a00bf549fa109da028f6d886d7f9
BLAKE2b-256 checksum
How to use checksums
cdb99edf0b55008e3b4a9d25056674db5c525c9465fa072191c77455b2b7fa39
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 19, 2026.

Transparency log

Release files / pretix_swissbitcoinpay-0.2.0-py3-none-any.whl

Download URL pretix_swissbitcoinpay-0.2.0-py3-none-any.whl
Size 94.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
55429d632dd1187442fb026a4754f72904032594c7e507007cd479a34c593eeb
BLAKE2b-256 checksum
How to use checksums
1ef3fb7e09bcb6ba2aa65d1f394d3d82c348fad69c497d6fc7f6603689928f91
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 19, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release 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