Skip to content

Multi-merchant

PayWarden supports running multiple merchants from a single deployment. Each merchant has their own API key, webhook configuration, and isolated order history.

Use cases

  • SaaS platforms — each of your customers is a merchant
  • Agency setups — manage payments for multiple clients
  • Multi-brand businesses — separate payment streams per brand

How it works

Admin Dashboard

      ├── Merchant A  (api_key_prefix: pw_live_aaaa)
      │     └── Orders: A1, A2, A3...

      ├── Merchant B  (api_key_prefix: pw_live_bbbb)
      │     └── Orders: B1, B2, B3...

      └── Merchant C  (api_key_prefix: pw_live_cccc)
            └── Orders: C1, C2, C3...

Shared:
  - HD wallet (single `vault/seed.enc` encrypted with `VAULT_KEY`)
  - Address index pool (globally sequential)
  - Chain Watcher (monitors all pending addresses)
  - PostgreSQL + Redis

Setting up a new merchant

Via admin dashboard

  1. Log in at /admin
  2. Navigate to MerchantsCreate Merchant
  3. Fill in:
    • Name — internal label
    • Webhook secret — shared secret used to HMAC-sign outgoing webhooks for this merchant
  4. Copy the generated API key (shown once only)

This is the "Full admin path" referenced in the Quick start. The "Quick path" uses the seeded default merchant from API_KEY and skips the admin UI entirely.

Via admin API

Admin endpoints use the httpOnly admin_token cookie — log in first with POST /api/v1/admin/auth/login (see Authentication), then call the merchant endpoint with the saved cookie:

bash
curl -X POST https://pay.yourdomain.com/api/v1/admin/auth/login \
  -c cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"password": "your-admin-password"}'

curl -X POST https://pay.yourdomain.com/api/v1/admin/merchants \
  -b cookies.txt \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Shop B",
    "webhook_secret": "a-long-random-string"
  }'

The response includes {id, name, api_key, api_key_prefix, webhook_secret, is_active, created_at}. The raw api_key is shown only once — store it immediately.

Merchant API key usage

Merchants use their API key to create and query their own orders:

bash
# Merchant A creates an order
curl -X POST https://pay.yourdomain.com/api/v1/payments \
  -H "X-API-Key: pw_live_aaaa_..." \
  -d '{"amount": "50.00", "currency": "USDT", "external_id": "a_order_1"}'

# Merchant B cannot see Merchant A's orders
curl https://pay.yourdomain.com/api/v1/payments/ord_abc \
  -H "X-API-Key: pw_live_bbbb_..."
# → 404 Not Found

Default merchant

On first setup, PayWarden creates a default merchant using the API_KEY from your .env. This merchant is used in single-tenant mode and for backwards compatibility.

Limitations in self-hosted mode

In the self-hosted version, all merchants share:

  • One vault — single seed phrase, single xpub
  • One address pool — globally sequential index, not per-merchant

This means:

  • If the vault is compromised, all merchants' funds are at risk
  • Address indices are not predictably tied to a single merchant

For full merchant isolation (separate vaults per merchant), see PayWarden Cloud.

Webhook routing

Each merchant receives webhooks only for their own orders, signed with their own webhook_secret:

Each order carries its own callback_url (supplied at order creation). The merchant's webhook_secret is used to HMAC-sign the body, and the signature is sent in the X-Signature header:

Order created by Merchant A with callback_url: https://a.com/hook

      ▼ payment.confirmed
PayWarden signs body with Merchant A's webhook_secret


POST https://a.com/hook
X-Signature: sha256=<hmac-sha256(body, merchant_a_secret)>
X-Idempotency-Key: <uuid>
X-Timestamp: <unix-seconds>

Merchant B never receives Merchant A's events.

Released under the BSL 1.1 License.