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 + RedisSetting up a new merchant
Via admin dashboard
- Log in at
/admin - Navigate to Merchants → Create Merchant
- Fill in:
- Name — internal label
- Webhook secret — shared secret used to HMAC-sign outgoing webhooks for this merchant
- 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:
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:
# 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 FoundDefault 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.