btcpaymailer/SETUP.md
Erling 6edd208245 Handle BTCPay __test__ webhooks with Postmark test email
Send a bannered test transactional email when BTCPay UI test events
arrive instead of failing on a missing invoice API lookup.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 13:18:21 +02:00

133 lines
3.9 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
Optional: set `WEBHOOK_TEST_EMAIL` (defaults to first `BCC_EMAIL` address). BTCPays **Send test** for `InvoiceReceivedPayment` delivers a Postmark test email to that address.
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