Metadata-Version: 2.4
Name: pretix-swissbitcoinpay
Version: 0.2.0
Summary: Bitcoin payments (Lightning and on-chain) for pretix with Swiss Bitcoin Pay: paid straight to the merchant's wallet.
Author-email: Swiss Bitcoin Pay Sàrl <hello@swiss-bitcoin-pay.ch>
Maintainer-email: Swiss Bitcoin Pay Sàrl <hello@swiss-bitcoin-pay.ch>
License-Expression: MIT
Project-URL: homepage, https://github.com/SwissBitcoinPay/checkout-pretix
Project-URL: repository, https://github.com/SwissBitcoinPay/checkout-pretix
Project-URL: changelog, https://github.com/SwissBitcoinPay/checkout-pretix/blob/main/CHANGELOG.md
Project-URL: issues, https://github.com/SwissBitcoinPay/checkout-pretix/issues
Keywords: pretix,bitcoin,lightning,payments,swiss-bitcoin-pay
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Intended Audience :: Other Audience
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Office/Business :: Financial
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: swissbitcoinpay-checkout<0.2,>=0.1.0
Requires-Dist: qrcode>=7.4
Dynamic: license-file

# Swiss Bitcoin Pay for pretix

A payment plugin for [pretix](https://pretix.eu): customers pay their orders in bitcoin, in
Lightning or on-chain, and the bitcoin goes straight to the organizer's
[Swiss Bitcoin Pay](https://swiss-bitcoin-pay.ch) wallet. It is built on the
[Swiss Bitcoin Pay checkout SDK](https://pypi.org/project/swissbitcoinpay-checkout/)
([source](https://github.com/SwissBitcoinPay/checkout-python)): 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](#how-it-works)
- [Requirements](#requirements)
- [Installation](#installation)
- [Configuration](#configuration)
- [Payments and orders](#payments-and-orders)
- [Background checks](#background-checks)
- [Security](#security)
- [Privacy](#privacy)
- [Known limits](#known-limits)
- [Development](#development)
- [Releases](#releases)
- [License](#license)

## 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](https://github.com/SwissBitcoinPay/checkout-python#on-chain-payments)).
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](https://docs.pretix.eu/guides/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](#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](https://github.com/SwissBitcoinPay/checkout-python#what-goes-where).

## Installation

Into the Python environment of your pretix installation:

```shell
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](https://docs.pretix.eu/self-hosting/installation/docker_smallscale/)):

```dockerfile
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](https://github.com/SwissBitcoinPay/checkout-python#what-goes-where)).
- Report a vulnerability privately: see [SECURITY.md](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](https://github.com/SwissBitcoinPay/checkout-python#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](https://docs.pretix.eu/dev/development/setup.html),
or simply:

```shell
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](https://docs.astral.sh/uv/) 0.13.0
  (`.github/ci-requirements.txt`) and the tools of `uv.lock` (in `.venv`, apart from the tests'):

  ```shell
  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`](https://github.com/SwissBitcoinPay/checkout-pretix/blob/main/.github/workflows/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](LICENSE), © 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.
