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>
133 lines
3.9 KiB
Markdown
133 lines
3.9 KiB
Markdown
# 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 BTCPay’s 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). BTCPay’s **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
|