BTCPay plugin to accept GNU Taler payments with multi-asset support (CHF, KUDOS).
This is experimental software. GNU Taler is in active development and upstream protocol/API behavior may change. This plugin can break between BTCPay or Taler upgrades and should be deployed with caution in production.
The Taler merchant backend ships with btcpayserver-docker as the opt-add-taler-merchant fragment: it uses the shared BTCPay postgres and is exposed at https://<your-host>/taler-merchant/ automatically (private/management APIs are blocked at the reverse proxy). No manual nginx or fragment copying is needed.
- Make sure your deployment is current:
btcpay-update.sh(the fragment requires a btcpayserver-docker from July 2026 or later) - Run
export BTCPAYGEN_ADDITIONAL_FRAGMENTS="$BTCPAYGEN_ADDITIONAL_FRAGMENTS;opt-add-taler-merchant" - Run
. ./btcpay-setup.sh -i - Install the Taler plugin in
Server settings -> Plugins, restart BTCPay - In
Server settings -> Taler: setMerchant public base URLtohttps://<your-host>/taler-merchant/ - Initialize instance, generate API token (make sure to save it), then
Savethen restart BTCPay. - Fetch/enable assets and add a bank account
- Follow the wire and KYC instructions to enable the bank account
- Add rates
Installations that used opt-add-taler-merchant.custom.yml (dedicated taler-merchant-db container) must move their data to the shared postgres before switching:
docker exec generated-taler-merchant-db-1 pg_dump -U taler taler-merchant > taler.sql
docker exec generated_postgres_1 psql -U postgres -c 'CREATE DATABASE "talermerchantmainnet"'
docker exec -i generated_postgres_1 psql -U postgres talermerchantmainnet < taler.sqlThen replace opt-add-taler-merchant.custom with opt-add-taler-merchant in BTCPAYGEN_ADDITIONAL_FRAGMENTS (export + . ./btcpay-setup.sh -i), remove the copied vhost rules from /var/lib/docker/volumes/generated_nginx_vhost/_data/<your-host> (routing now comes from the official nginx.tmpl; duplicates make nginx reject the config), delete the leftover docker-compose-generator/docker-fragments/opt-add-taler-merchant.custom.yml, and once everything works drop the old container and volume.
Use the following Payto URI format: payto://iban/CH00000000000000000000?receiver-name=My%20Company%SA
Warning: only CHF iban are currently supported by the Taler exchange.
In order to receive CHF from taler-ops.ch you will have to have your iban added and follow the instructions:
If the bank account status is kyc-wire-required you will need to send from the same bank account the smallest amount possible to the payto instructions. It might take 1 or 2 days to complete.
Once the bank account is on status kyc-required you will be requested to validate the Terms of Services of the Taler exchange.
- Plugin code:
BTCPayServer.Plugins.Taler/ - Standalone merchant docker (development):
docker-compose.taler.yml - Merchant image source (
rachyand/taler-merchant):docker/taler-merchant/
The production BTCPay docker fragment and nginx routing live upstream in btcpayserver-docker (opt-add-taler-merchant).
Prereqs:
- .NET 10 SDK
- BTCPay source available at
submodules/btcpayserver
Build:
dotnet publish BTCPayServer.Plugins.Taler/BTCPayServer.Plugins.Taler.csproj -c Release -o /tmp/taler-plugin-publish --no-restore -m:1The output directory contains the plugin payload used to create a .btcpay package for upload.
Go to Server settings -> Taler and configure:
Merchant base URL: internal URL reachable by BTCPay container, typicallyhttp://taler-merchant:9966/Merchant public base URL: public URL used in checkout links, typicallyhttps://<your-host>/taler-merchant/Merchant instance ID: usuallydefaultInstance passwordMerchant API token
Then:
Initialize instanceGenerate API token(usesscope: allandduration: forever)Check bank accountsFetch assets- Save and restart BTCPay when asset set changes
All optional. Set them on the BTCPay container (BTCPAY_... variables are passed through by btcpayserver-docker).
| Variable | Default | Purpose |
|---|---|---|
BTCPAY_TALER_MERCHANT_URL |
http://taler-merchant:9966/ in Docker, http://localhost:9966/ otherwise |
Pre-fills Merchant base URL on a fresh settings page. Only a display default — it is never used as a target for stored credentials. |
BTCPAY_TALER_ALLOW_PRIVATE_MERCHANT |
unset (off) | Lets a public merchant hostname resolve to a private address. Only needed for split-horizon DNS; see below. |
BTCPAY_TALER_ALLOW_PRIVATE_EXCHANGES |
unset (off) | Lets exchange/KYC requests reach private addresses. Needed for regtest setups with a local exchange. |
Requests to the merchant backend never follow redirects, and the connection must stay on the configured host. Two consequences when upgrading:
- If your
Merchant base URLrelies on a redirect (for examplehttp://…that the proxy 301s tohttps://…), requests now fail with an error naming the redirect target. Fix it by configuring that target URL directly — it is the correct URL either way. - If a public hostname deliberately resolves to a private address (split-horizon DNS), set
BTCPAY_TALER_ALLOW_PRIVATE_MERCHANT=true. This is not needed for normal deployments: container names (taler-merchant),localhost, IP literals, and.local/.internal/.home.arpanames are always allowed, because none of them can be public DNS.
Nothing to configure: when the opt-add-taler-merchant fragment is enabled, the official btcpayserver-docker nginx.tmpl serves the merchant's public endpoints at https://<your-host>/taler-merchant/ and returns 403 for the private/webui/management paths (which are additionally Bearer-token protected).
add these custom rates:
KUDOS_CHF = 0.01;
CHF_KUDOS = 100;
BTC_KUDOS = BTC_CHF * CHF_KUDOS;
- Merchant private API calls use
Authorization: Bearer secret-token:... - Token scope must allow required operations.
allis used for provisioning flows. - If you see
401on private endpoints, regenerate token and save it in BTCPay.
There is no runtime "list all endpoints" endpoint in taler-merchant.
For backend 32:0:20 (merchant 1.6.9), these are the relevant endpoints this plugin/deployment uses:
- Public:
GET /configGET /instances/{instance}/orders/{order_id}?token={claim_token}
- Provisioning/management:
POST /management/instances
- Instance private (Bearer token):
POST /instances/{instance}/private/tokenGET /instances/{instance}/private/accountsPOST /instances/{instance}/private/accountsDELETE /instances/{instance}/private/accounts/{h_wire}GET /instances/{instance}/private/kycGET /instances/{instance}/private/ordersPOST /instances/{instance}/private/ordersGET /instances/{instance}/private/orders/{order_id}DELETE /instances/{instance}/private/orders/{order_id}POST /instances/{instance}/private/orders/{order_id}/refundPOST /instances/{instance}/private/orders/{order_id}/abort
Canonical upstream reference:
Run the following commands inside BTCPayServer, replace secret-token:yoursecret with the Merchant API token
- Read Merchant Backend config (public endpoint)
docker run --rm --network generated_default curlimages/curl:8.12.1 -sS http://taler-merchant:9966/config
- Read all accounts in Merchant Backend
docker run --rm --network generated_default curlimages/curl:8.12.1 -i -sS \
-H "Authorization: Bearer secret-token:yoursecret" \
"http://taler-merchant:9966/instances/default/private/accounts"
- check KYC status of all accounts
docker run --rm --network generated_default curlimages/curl:8.12.1 -sS \
-H "Authorization: Bearer secret-token:yoursecret" \
"http://taler-merchant:9966/instances/default/private/kyc"
- list all orders
docker run --rm --network generated_default curlimages/curl:8.12.1 -sS \
-H "Authorization: Bearer secret-token:yoursecret" \
"http://taler-merchant:9966/instances/default/private/orders"
- list all paid and wired orders
docker run --rm --network generated_default curlimages/curl:8.12.1 -sS \
-H "Authorization: Bearer secret-token:yoursecret" \
"http://taler-merchant:9966/instances/default/private/orders?paid=yes&wired=yes&delta=-50"
- never drop a payable or already-paid order when switching backends: an order the backend refuses to delete (because it is claimed or paid) is now checked instead of silently abandoned — a paid one has its payment recorded on the spot, and one that is still payable keeps its prompt so BTCPay keeps monitoring it until the next restart
- fix order cancellation treating a merchant
404as success: only the merchant's explicit unknown-order error counts as already deleted, and the alternative URL layout is tried first, like order creation and status already do - stop credentials left behind by older plugin versions (stored without the backend they were issued for) from reaching a backend they were never bound to, including the default URL the settings page fills in on page load
- refuse to follow redirects on merchant backend requests and re-check that a
public backend hostname still resolves publicly, so a compromised backend —
or an MITM when the backend URL is plain
http— cannot pivot requests toward internal services (see Environment variables if your setup relies on a redirect or on split-horizon DNS) - validate URLs the merchant backend reports before they are used: an order
status URL is only rendered as a link when it is
http(s), and a pay URI must name a payable target before it reaches the checkout page or its QR code - escape order and account identifiers consistently in merchant API requests
- add regression tests for stale-order cleanup, credential migration across every settings page action, and the hardened transport
- credit payments made just before expiry: poll expired invoices for their whole monitoring window instead of only the last 7 days, and settle an on-time payment discovered after expiry so it gets normal credit and fulfillment
- bind merchant credentials to the backend they were issued for: changing the backend URL or instance discards the stored API token, and the stored instance password is no longer replayed against a different host
- cancel pending orders left on a replaced backend, and stop checkout from creating new ones there until BTCPay is restarted
- constrain outbound requests driven by merchant KYC data: exchange URLs and
access tokens can no longer reach internal addresses, escape their URL path,
or be redirected elsewhere (set
BTCPAY_TALER_ALLOW_PRIVATE_EXCHANGES=truefor regtest setups with a local exchange) - fix the KYC portal link accepting a dangerous scheme from backend data
- add regression tests for expiry windows, credential binding, and request blocking
- require BTCPay Server >= 2.4.0
- fix server settings menu item not visible with the new BTCPay 2.4 navigation
- update merchant backend to 1.6.9, protocol v32:0:20
- fix currency divisibility discovery (
num_fractional_normal_digits) - show merchant protocol version badge when no release version is available
- add test suite for merchant client
/configparsing
- upgrade to .net 10
- improve API token one time view
- show
order_status_urlfor each order in the list of orders - show the version of the merchant protocol
- update merchant backend to 1.5.8, protocol v27:0:15 (there is a known but in case of multiple currencies available)
- initial plugin
GPLv3. See LICENSE.