btcpaymailer/SETUP.md
Erling a0e04b5d81 Fix SETUP.md list numbering
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 13:27:56 +02:00

4.1 KiB
Raw Blame History

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 tokenPOSTMARK_API_KEY
  2. Verify sender → FROM_EMAIL (e.g. noreply@nxtgroup.org)
  3. Optional display name → FROM_NAME (e.g. NXT Payments → sends as NXT Payments <noreply@nxtgroup.org>)
  4. 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 → AccountAPI 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.

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:

  1. Store → SettingsWebhooksCreate 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:

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

  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