BTCPayServer GNU Taler Plugin
August 5, 2026 · View on GitHub
BTCPay plugin to accept GNU Taler payments with multi-asset support (CHF, KUDOS).
Warning
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.
Quick production checklist
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
Upgrading from the pre-merge custom fragment
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.sql
Then 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.
Add a bank account
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.
Repository layout
- 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).
Build plugin
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:1
The output directory contains the plugin payload used to create a .btcpay package for upload.
BTCPay server settings
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
Environment variables
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. |
Merchant backend connection rules
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.
Reverse proxy (public merchant path)
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).
Rates
add these custom rates:
KUDOS_CHF = 0.01;
CHF_KUDOS = 100;
BTC_KUDOS = BTC_CHF * CHF_KUDOS;
API/token notes
- 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.
Merchant API endpoints (used in this project)
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:
Some CLI commands
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"
Changelog
v0.0.2.2
- 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
v0.0.2.1
- 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
v0.0.2.0
- 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
v0.0.1.1
- 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)
v0.0.1.0
- initial plugin
License
GPLv3. See LICENSE.