No subscriptions. NOWPayments has a “recurring billing” feature, but it’s
just an email-resend of new invoices each cycle — not auto-debit, because
on-chain crypto wallets can’t pre-authorize recurring withdrawals. The
provider in vibestrap throws on
type: 'subscription' to fail loudly
rather than silently book bad UX. Use NOWPayments for one-time / lifetime /
credit-pack purchases. Pair with Stripe (USDC) or Helio if you need true
crypto subscriptions.No customer portal. createPortalLink throws — there’s no NOWPayments
equivalent of Stripe’s billing portal.Prerequisites
- A NOWPayments account at nowpayments.io. No KYC required for basic merchant signup — just an email.
- A receiving wallet address registered in the NOWPayments dashboard (Settings → Payment settings → Outcome wallet). This is where your crypto lands. Use a non-custodial wallet you control (Ledger / Trezor / a trusted CEX deposit address).
- An IPN secret generated in Settings → Store settings → IPN Secret.
- Postgres up, schema pushed.
1. Set the active provider
Insrc/config/site.ts:
currency stays 'usd' — you price in USD on your end, and NOWPayments
quotes the equivalent crypto amount at checkout time.
2. Set env vars
In.env.local (variable names match src/env.ts):
NOWPayments has no concept of a “product” or “price ID”. Other providers’
PRICE_* env vars hold a Stripe price_xxx / Creem product id / etc.;
NOWPayments’ just hold the dollar amount as a string. The provider parses
it to a number and POSTs it as price_amount directly.3. Configure the IPN webhook
NOWPayments calls these “IPN” (Instant Payment Notifications). In Settings → IPN settings:- IPN callback URL:
https://your-domain.com/api/webhooks/nowpayments
ipn_callback_url per-invoice so it works even if the
dashboard global is unset — but setting both is harmless and lets failed
invoices retry against the right host.
How the flow works
- User clicks “Pay with crypto” → server action calls
paymentManager.createCheckout({ priceId: '49', type: 'one_time', ... }). - The provider POSTs to
https://api.nowpayments.io/v1/invoicewithprice_amount: 49,price_currency: 'usd', anorder_idpacked asvbs|<userId>|<scene>|<type>so we can recover context from the IPN, andipn_callback_urlset to your/api/webhooks/nowpaymentsroute. - NOWPayments returns a hosted invoice URL like
https://nowpayments.io/payment?iid=4514933743. We redirect the user there. - The user picks a coin (BTC / ETH / USDC / …), sends the funds.
- NOWPayments emits multiple IPNs as the payment progresses:
waiting → confirming → confirmed → sending → finished. The provider only treatsfinishedas paid — the rest are normalized tounknownand ignored. Other terminal states (failed,expired,partially_paid,refunded) are alsounknownfrom the handler’s perspective — those need manual reconciliation in the NOWPayments dashboard. - On
finished, the standardprocessNormalizedEventflow runs: payment row inserted (idempotent onpayment.invoiceId, which we set to the NOWPaymentspayment_id), credits granted, license issued if applicable.
Webhook signature verification
NOWPayments signs the IPN body with HMAC-SHA512 of the alphabetically-sorted JSON serialization of the body, using your IPN secret. The signature arrives in thex-nowpayments-sig header.
The verification is implemented in verifyNowpaymentsWebhook and matches
the algorithm shown in the official Node.js sample
(JSON.stringify(params, Object.keys(params).sort())) and the official PHP
WooCommerce plugin’s check_ipn_request_is_valid() (ksort + hash_hmac).
Tests cover the round-trip plus tampering, malformed JSON, and wrong-length
signatures.
Verify it works
- Use NOWPayments’ sandbox at sandbox.nowpayments.io for test transactions (separate API key + IPN secret).
- Expose
localhost:3000via cloudflared / ngrok, set the resulting URL as your IPN callback in the sandbox dashboard. pnpm dev, open/pricing, click checkout — NOWPayments hosted invoice page opens.- Pay with the sandbox testnet faucet they provide.
- Watch the IPNs arrive — you’ll see several with
payment_statusgoing fromwaitingtofinished. - Check Postgres after
finished:
Common pitfalls
is_fee_paid_by_user: false— vibestrap’s provider always sets this so you eat the (small) network fee instead of surprising the buyer with a larger invoice. Toggle insrc/payment/provider/nowpayments.tsif you’d rather pass it through.- Sandbox vs production keys are completely separate — including the IPN secret. Mixing them silently fails signature verification.
partially_paidrequires manual reconciliation — if a buyer underpays (sent the wrong amount, or the price moved during the confirmation window), NOWPayments emitspartially_paidand holds the funds. The provider ignores it; you’ll need to refund or top-up via the dashboard.- Refunds are out-of-band — there’s no Stripe-style “create refund” API;
you initiate refunds in the NOWPayments dashboard. The handler doesn’t
reverse credit grants automatically. If you need that, listen to the
raw
refundedIPN status and callrefundCredits()yourself. - Order ID format is load-bearing — we encode
vbs|<userId>|<scene>|<type>so the IPN handler can recover context (NOWPayments has no metadata field). If you change the prefix or delimiter, updateparseOrderIdinsrc/payment/provider/nowpayments.tsto match. - Price is fixed at invoice creation — if BTC moves 5% between invoice creation and the buyer paying, NOWPayments locks in the original quote. Buyers occasionally complain they “overpaid” or “underpaid” by a few cents; this is normal crypto-payment behavior, not a bug in vibestrap.
- No customer portal — buyers can’t self-serve refunds, billing history,
or payment method updates. Build your own
/settings/billingpage that reads from thepaymenttable if you want a UI.
Switching from Stripe / Creem
Already running another provider? Three steps:- Flip
siteConfig.payment.providerto'nowpayments'. - Set the four
NOWPAYMENTS_*env vars. - Configure the IPN URL in the NOWPayments dashboard.
event.provider, and the
shared handler is idempotent, so old provider events still hit the same
payment table without conflict.
Official docs
- NOWPayments: nowpayments.io
- API reference: documenter.getpostman.com/view/7907941/S1a32n38
- IPN setup guide: nowpayments.io/help/ipn-callbacks
- Sandbox: sandbox.nowpayments.io
- Official Node SDK: github.com/NowPaymentsIO/nowpayments-api-js