4.1 KiB
btcpaymailer setup guide
Overview
One mailer instance handles multiple BTCPay stores on the same server (payment.nxtgroup.org). Each store registers the same webhook URL; the app uses storeId from each payload to fetch the correct invoice.
1. Postmark
- Copy Server API token →
POSTMARK_API_KEY - Verify sender →
FROM_EMAIL(e.g.noreply@nxtgroup.org) - Optional display name →
FROM_NAME(e.g.NXT Payments→ sends asNXT Payments <noreply@nxtgroup.org>) - Set
BCC_EMAIL(comma-separated, optional)
2. BTCPay API key (all stores)
Use one account-level API key with btcpay.store.canviewinvoices on every store that will use the mailer.
- BTCPay → Account → API keys → Create
- Enable
canviewinvoicesfor each store - Copy token →
BTCPAY_API_KEY
Per-store-only keys are not supported unless you extend the app.
3. Portainer registry
Registries → Add registry
| Field | Value |
|---|---|
| URL | git.nxtgroup.org |
| Username | erling |
| Password | Gitea PAT (read:package) |
4. Deploy stack (Swarm + Traefik)
Wildcard DNS (*.nxtgroup.org) is enough — add a Traefik Host() label, no extra Cloudflare record required.
version: '3.8'
services:
btcpay-mailer:
image: git.nxtgroup.org/erling/btcpaymailer:1.0.0
environment:
BTCPAY_WEBHOOK_SECRET: PASTE_BTCPAY_WEBHOOK_SECRET
TEST_TOKEN: PASTE_RANDOM_TEST_TOKEN
BTCPAY_URL: https://payment.nxtgroup.org
BTCPAY_API_KEY: PASTE_BTCPAY_API_KEY
POSTMARK_API_KEY: PASTE_POSTMARK_TOKEN
FROM_NAME: NXT Payments
FROM_EMAIL: noreply@nxtgroup.org
BCC_EMAIL: admin@nxtgroup.org
DEBUG: "true"
networks:
- public
deploy:
replicas: 1
placement:
constraints:
- node.role == manager
labels:
- traefik.enable=true
- traefik.http.routers.btcpay-mailer.rule=Host(`mailer.nxtgroup.org`)
- traefik.http.routers.btcpay-mailer.entrypoints=websecure
- traefik.http.routers.btcpay-mailer.tls.certresolver=cloudflare
- traefik.http.services.btcpay-mailer.loadbalancer.server.port=5000
restart_policy:
condition: on-failure
networks:
public:
external: true
Generate TEST_TOKEN:
openssl rand -hex 32
Set DEBUG=false after initial testing.
5. BTCPay webhook (per store)
Repeat for each store:
-
Store → Settings → Webhooks → Create webhook
-
Payload URL:
https://mailer.nxtgroup.org/btcpay-webhook(no?token=) -
Secret: generate or accept BTCPay’s secret → copy into Portainer as
BTCPAY_WEBHOOK_SECRETUse the same secret for all stores or one mailer per secret (current app supports one
BTCPAY_WEBHOOK_SECRETonly). Simplest: use the same webhook secret when creating each store webhook, or recreate webhooks with one shared secret. -
Events:
InvoiceReceivedPaymentonly -
Enabled: on
-
Save
The Secret in BTCPay must exactly match BTCPAY_WEBHOOK_SECRET in Portainer.
6. Test Postmark
Optional: set WEBHOOK_TEST_EMAIL (defaults to first BCC_EMAIL address). BTCPay’s Send test for InvoiceReceivedPayment delivers a Postmark test email to that address.
Portainer → container console:
python app.py test-email you@example.com
Or HTTP:
curl -X POST "https://mailer.nxtgroup.org/test-email?token=YOUR_TEST_TOKEN&to=you@example.com"
7. Test BTCPay webhook
- Create a test invoice with buyer email in metadata
- Pay part of the amount (invoice stays
New, partial paid) - Check Portainer Logs for
[evaluation]lines - BTCPay → Webhooks → Deliveries should show
200
Troubleshooting
| Symptom | Fix |
|---|---|
Webhook 401 |
BTCPAY_WEBHOOK_SECRET ≠ BTCPay webhook Secret |
Webhook 500 fetch invoice |
BTCPAY_API_KEY lacks access to that storeId |
ignored in logs |
Full payment, wrong status, or not partial |
No email, 400 |
Invoice missing metadata.buyerEmail |
Shutdown
- BTCPay: disable/delete webhooks
- Portainer: remove stack