---
title: "WayForPay Payment Integration"
description: "HPP form POST, returnUrl vs serviceUrl money truth, PaymentConductor handoff, HMAC webhooks, and wallet credit top-up for Ring Platform"
locale: "en"
---
# WayForPay Payment Integration

WayForPay is Ring's primary Ukrainian PSP for hosted card checkout (including Apple Pay / Google Pay where enabled by the merchant). All new card flows go through **PaymentConductor** — store, membership, news promotion, and wallet credit top-up.

> **Info**
> Use **Founder** / **Developer** tabs in the docs sidebar to filter this page.

> **Success**
> Env SSOT: `WAYFORPAY_MERCHANT_ACCOUNT`, `WAYFORPAY_SECRET_KEY`, `WAYFORPAY_MERCHANT_PASSWORD`, `WAYFORPAY_DOMAIN`, `WAYFORPAY_API_URL`. There is **no** `WAYFORPAY_MERCHANT_ID`.

> **Warning**
> **Money truth:** credit and order fulfillment happen only when WayForPay POSTs to **`serviceUrl`** with `transactionStatus === 'Approved'`. The browser **`returnUrl`** is UX only — not settlement.

## What WayForPay covers in Ring

| Purpose | Live entry | Settled by |
|---------|------------|------------|
| `store_order` | `POST /api/store/payments/wayforpay` → `PaymentConductor.createCheckout` | `handlers/store-order.ts` |
| `membership_upgrade` | `initiateMembershipPayment` (card) → `createCheckout` | `handlers/membership-upgrade.ts` |
| `news_promotion` | News promotion submit | `handlers/news-promotion.ts` |
| `wallet_topup` | `WalletConductor.initiateTopUp` / `initiateCreditTopupPayment` → `createCheckout` | `handlers/wallet-topup.ts` → `creditBalanceService.addFiatUsd` |

Recurring membership management uses WayForPay **`regularApi`** (`lib/payments/subscription/wayforpay-regular-api.ts`) with **merchantAccount + merchantPassword** (not the secret key).

### For founders

## Operator value

- **Hosted checkout** — cards never touch Ring servers; PCI scope stays with WayForPay.
- **No cabinet thank-you page required** — Ring sends `returnUrl` on every payment (e.g. back to `/wallet`). WayForPay’s own result page is used only if `returnUrl` is omitted.
- **One public webhook** — register `https://YOUR_HOST/api/payments/wayforpay/webhook` so Approved payments can settle. Localhost cannot receive WayForPay callbacks (use a tunnel or staging for real credit tests).
- **Wallet top-up is live** — Add Credit / `/wallet/topup` Card tab uses PaymentConductor (processor from env: WayForPay or Stripe). PayPal tab on Add Credit sends `processor=paypal` when PayPal is configured.
- **Membership card path is live** — PaymentModal card tab uses PaymentConductor. Membership **PayPal** is live when `NEXT_PUBLIC_PAYMENT_STORE_ALLOW_PAYPAL=true` (Subscriptions v1 via [SubscriptionConductor](/docs/features/subscriptions.md)) — separate from wallet credit PayPal.

### Credentials checklist

| Variable | Required for | Notes |
|----------|--------------|-------|
| `WAYFORPAY_MERCHANT_ACCOUNT` | All checkouts | Merchant login / account id from cabinet |
| `WAYFORPAY_SECRET_KEY` | HMAC signatures | Server-only — never `NEXT_PUBLIC_*` |
| `WAYFORPAY_MERCHANT_PASSWORD` | Recurring / `regularApi` | Required for subscription lifecycle |
| `WAYFORPAY_DOMAIN` | Merchant domain match | This product: `ring-platform.org` |
| `WAYFORPAY_API_URL` | API base | Default `https://api.wayforpay.com/api` |

Templates: `env.local.template`, `docker.env.template`, `docker-compose.template.yml`.

### Cabinet service URL

```text
https://YOUR_HOST/api/payments/wayforpay/webhook
```

Use your clone host in white-label deployments. Do **not** register legacy `/callback`, `/success`, or `/failure` paths as the settlement endpoint.

### After a member pays with card

1. Browser returns to Ring (`returnUrl`) — they see the wallet/store page.
2. WayForPay’s servers POST the result to your webhook.
3. Only then is credit added or the order marked paid.

If the member is back on `/wallet` but credit is missing, the webhook did not land (wrong host, localhost, or HMAC failure) — check `payment_transactions` and server logs.

### For developers

## Environment (verified)

{`WAYFORPAY_MERCHANT_ACCOUNT=your_wayforpay_merchant_account
WAYFORPAY_SECRET_KEY=your_wayforpay_secret_key
WAYFORPAY_MERCHANT_PASSWORD=your_wayforpay_merchant_password
WAYFORPAY_DOMAIN=ring-platform.org
WAYFORPAY_API_URL=https://api.wayforpay.com/api
# Optional store-specific overrides (fallback to WAYFORPAY_* above)
# WAYFORPAY_STORE_MERCHANT_ACCOUNT=
# WAYFORPAY_STORE_SECRET_KEY=
# WAYFORPAY_STORE_DOMAIN=

# Card processor for wallet credit (default = PAYMENT_DEFAULT_PROCESSOR)
# PAYMENT_WALLET_TOPUP_PROCESSOR=wayforpay   # or stripe
# PAYMENT_DEFAULT_PROCESSOR=wayforpay`}

## HPP contract — form POST only

WayForPay Hosted Payment Page: `https://secure.wayforpay.com/pay`.

| Rule | Detail |
|------|--------|
| Method | **HTML form POST** (`application/x-www-form-urlencoded`) |
| GET query URLs | **Invalid** — WayForPay responds `Bad Request` / *This page requires only POST data* |
| Ring builder | `lib/payments/wayforpay-hpp.ts` → `form_post` `CheckoutRedirect` |
| Client handoff | `lib/payments/checkout-redirect.ts` → `followCheckoutResult` (unbranded; never import PSP-branded client helpers) |

PaymentConductor returns `CreateCheckoutResult.redirect`:

- WayForPay HPP → `{ mode: 'form_post', url, fields }`
- Stripe Checkout / PayPal approve / WFP invoiceUrl → `{ mode: 'navigate', url }`

`createCheckout` always runs `normalizeCheckoutResult` so processors may set either `redirect` or legacy `paymentUrl` / `paymentFields`.

### returnUrl vs serviceUrl

| Field | Who sets it | Role | Trust for money? |
|-------|-------------|------|------------------|
| `returnUrl` | Ring per request (`WalletConductor.initiateTopUp`, store/membership contexts) | Browser land-back after pay | **No** |
| `serviceUrl` | Ring via `getWebhookUrl('wayforpay')` | Server POST from WayForPay | **Yes** — settlement |

Wallet credit settlement: `handlers/wallet-topup.ts` requires `transactionStatus === 'Approved'`, then `markPaid` + `creditBalanceService.addFiatUsd`.

## Module map

| Concern | Path |
|---------|------|
| Processor (all purposes) | `lib/payments/processors/wayforpay.processor.ts` |
| HPP sign + fields | `lib/payments/wayforpay-hpp.ts`, `wayforpay-hpp-types.ts` |
| Unbranded browser handoff | `lib/payments/checkout-redirect.ts` |
| Signature verify | `lib/payments/processors/wayforpay-verify.ts` |
| Store form helper | `lib/payments/wayforpay-store-service.ts` (`initiateStorePayment` — called from processor only) |
| Webhook dispatcher | `lib/payments/conductor/webhook-dispatcher.ts` |
| Handlers | `lib/payments/conductor/handlers/*.ts` |
| regularApi | `lib/payments/subscription/wayforpay-regular-api.ts` |
| Subscription provider | `lib/payments/subscription/providers/wayforpay-subscription.ts` |
| Canonical webhook | `app/api/payments/wayforpay/webhook/route.ts` |

## Checkout sequence

Caller invokes `PaymentConductor.createCheckout({ purpose, userId, amount, currency, returnUrl, ... })`. Conductor resolves processor (`metadata.processor` or `getPaymentProvider(purpose)`).

`wayforpay.processor.ts` builds `orderReference`, writes `createPending`, builds HPP fields (`returnUrl` + `serviceUrl`), returns `redirect: { mode: 'form_post', ... }`. Conductor runs `normalizeCheckoutResult`.

UI calls `followCheckoutResult` — hidden form POST to `secure.wayforpay.com/pay` (not `window.location` with a query string).

User pays; browser may hit `returnUrl`. WayForPay POSTs to `/api/payments/wayforpay/webhook`.

`dispatchWayForPayWebhook` verifies HMAC, parses purpose from `orderReference`, marks paid, runs the purpose handler (e.g. wallet credit).

### Order reference prefixes

| Purpose | Prefix pattern |
|---------|----------------|
| `store_order` | `store_{orderId}_{ts}` |
| `membership_upgrade` | `membership_{userId}_{ts}` (legacy `ring_` still parsed) |
| `news_promotion` | `news-promo-{base64url(articleId)}-{ts}` |
| `wallet_topup` | `wallettopup_{userId}_{ts}` |

### Wallet top-up

{`// Card tab: omit processor → PAYMENT_WALLET_TOPUP_PROCESSOR / PAYMENT_DEFAULT_PROCESSOR
// PayPal tab: formData processor=paypal
await PaymentConductor.createCheckout({
  purpose: 'wallet_topup',
  userId: session.user.id,
  amount, // 25–2000
  currency: getClientCreditFiatCurrency(),
  returnUrl, // e.g. /{locale}/wallet — UX only
  locale,
  metadata: { source, ...(processor ? { processor } : {}) },
})
// Result.redirect → followCheckoutResult
// Webhook Approved → handleWalletTopupWayForPayWebhook → creditBalanceService.addFiatUsd`}

UI: `features/wallet/components/credit-add-fs-modal.tsx` (Card | PayPal tabs) and `/wallet/topup`.

### Membership card

`initiateMembershipPayment` with card / wayforpay / stripe → `createCheckout({ purpose: 'membership_upgrade' })`. UI: `components/membership/payment-modal.tsx` — native-token + card live; PayPal tab when `NEXT_PUBLIC_PAYMENT_STORE_ALLOW_PAYPAL=true` → `POST /api/membership/payment/paypal`.

### Store

`POST /api/store/payments/wayforpay` → `PaymentConductor.createCheckout({ purpose: 'store_order' })`. No route-level bypass of the ledger. Client uses `followCheckoutResult` with conductor `redirect`.

### regularApi (subscriptions)

`WAYFORPAY_MERCHANT_PASSWORD` + `merchantAccount` authenticate JSON calls to `https://api.wayforpay.com/regularApi` (STATUS, CHANGE, SUSPEND, RESUME, REMOVE). See `wayforpay-regular-api.ts`.

## Security

- Generate and verify HMAC **server-side only** with `WAYFORPAY_SECRET_KEY`.
- Never embed production secrets in MDX, client bundles, or git.
- Idempotent ledger: duplicate Approved webhooks return success without double-crediting (`markPaid` / handler guards).
- Never treat `returnUrl` query params as payment confirmation.

## Smoke / ops

- Staging: migration `004` applied → store + membership card + wallet top-up → inspect `payment_transactions`.
- Webhook must be **public HTTPS** — localhost `serviceUrl` will not settle credit.
- Admin: `GET /api/admin/users/[id]/payments` (UI: `admin-user-detail-sheet.tsx` Payments tab).

## Related

  
- **[PaymentConductor](/docs/features/payment-conductor.md)** — Purposes and rails

  
- **[Architecture](/docs/architecture/payment-conductor.md)** — CheckoutRedirect, dispatcher, ledger

  
- **[Payment integration (ops)](/docs/customization/payment-integration.md)** — Cabinet URLs and checklist

  
- **[Wallet](/docs/features/wallet.md)** — Add Credit Card / PayPal

  
- **[Subscriptions](/docs/features/subscriptions.md)** — SubscriptionConductor + WayForPay recurring
