# 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