btcpaymailer/SETUP.md
Erling c2b5d23477 Validate BTCPay webhooks with BTCPAY-SIG HMAC-SHA256
Replace URL token auth with the official webhook secret signature check.
Add SETUP.md and separate TEST_TOKEN for the Postmark test endpoint.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 12:20:00 +02:00

131 lines
3.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
1. Copy **Server API token**`POSTMARK_API_KEY`
2. Verify sender → `FROM_EMAIL` (e.g. `billing@nxtgroup.org`)
3. 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.
1. BTCPay → **Account****API keys** → Create
2. Enable `canviewinvoices` for each store
3. 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.
```yaml
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_EMAIL: billing@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`:
```bash
openssl rand -hex 32
```
Set `DEBUG=false` after initial testing.
## 5. BTCPay webhook (per store)
Repeat for each store:
1. Store → **Settings****Webhooks****Create webhook**
2. **Payload URL:** `https://mailer.nxtgroup.org/btcpay-webhook` (no `?token=`)
3. **Secret:** generate or accept BTCPays secret → copy into Portainer as `BTCPAY_WEBHOOK_SECRET`
Use the **same secret** for all stores **or** one mailer per secret (current app supports one `BTCPAY_WEBHOOK_SECRET` only). Simplest: use the **same webhook secret** when creating each store webhook, or recreate webhooks with one shared secret.
4. **Events:** `InvoiceReceivedPayment` only
5. **Enabled:** on
6. Save
The Secret in BTCPay must **exactly match** `BTCPAY_WEBHOOK_SECRET` in Portainer.
## 6. Test Postmark
Portainer → container console:
```bash
python app.py test-email you@example.com
```
Or HTTP:
```bash
curl -X POST "https://mailer.nxtgroup.org/test-email?token=YOUR_TEST_TOKEN&to=you@example.com"
```
## 7. Test BTCPay webhook
1. Create a test invoice with **buyer email** in metadata
2. Pay **part** of the amount (invoice stays `New`, partial paid)
3. Check Portainer **Logs** for `[evaluation]` lines
4. 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