better-payment
Integrations

Next.js Integration

Integrating better-payment with the Next.js App Router.

1. Create the singleton

// lib/payment.ts
import { BetterPayment, ProviderType } from 'better-payment';

let _instance: BetterPayment | null = null;

export function getBetterPayment(): BetterPayment {
  if (!_instance) {
    _instance = new BetterPayment({
      mode: process.env.NODE_ENV === 'production' ? 'production' : 'sandbox',
      defaultProvider: ProviderType.IYZICO,
      providers: {
        iyzico: {
          enabled: true,
          config: {
            apiKey: process.env.IYZICO_API_KEY!,
            secretKey: process.env.IYZICO_SECRET_KEY!,
          },
        },
      },
      handler: {
        allowedActions: ['payment/init-3ds', 'payment/complete-3ds', 'callback'],
        transformRequest: async (ctx) => buildPaymentRequestFromOrder(ctx.body.orderId, ctx.body.paymentCard),
        onCallback: async (result) => markOrder(result.paymentId, result.status),
        callbackRedirect: (result) => `/orders/${result.paymentId}?payment=${result.status}`,
      },
    });
  }
  return _instance;
}

Initialize lazily (in a function, not with a module-level new BetterPayment(...)). Otherwise Next.js static generation can crash when the environment variables are not available.

2. Create the catch-all route handler

// app/api/pay/[...path]/route.ts
import { getBetterPayment } from '@/lib/payment';

export const runtime = 'nodejs'; // better-payment needs node:crypto

async function handler(req: Request): Promise<Response> {
  const contentType = req.headers.get('content-type') ?? '';
  let body: unknown;

  if (req.method !== 'GET' && req.method !== 'HEAD') {
    const text = await req.text();
    body = contentType.includes('application/json') && text ? JSON.parse(text) : text;
  }

  const res = await getBetterPayment().handler.handle({
    method: req.method,
    url: req.url,
    headers: Object.fromEntries(req.headers.entries()),
    body,
  });

  // PayTR notifications must receive plain "OK"; redirects have an empty body
  const payload = typeof res.body === 'string' ? res.body : JSON.stringify(res.body);
  return new Response(res.status === 303 ? null : payload, {
    status: res.status,
    headers: res.headers,
  });
}

export const GET = handler;
export const POST = handler;

Form-urlencoded callbacks (iyzico, Param, Akbank, PayTR) are passed as raw strings; the handler parses them.

3. Start a payment from the frontend

const res = await fetch('/api/pay/iyzico/payment/init-3ds', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ orderId, paymentCard }), // amounts come from transformRequest
});

const result = await res.json();
if (result.status === 'pending' && result.threeDSHtmlContent) {
  document.open();
  document.write(result.threeDSHtmlContent);
  document.close();
}

Or use the typed client:

import { createBetterPaymentClient } from 'better-payment/client';

const client = createBetterPaymentClient({ baseUrl: '/api/pay' });
const result = await client.iyzico.initThreeDSPayment(requestBuiltByTransformRequest);
// 422 (provider failure) resolves with status 'failure' instead of throwing

4. Callback

Set callbackUrl to https://yoursite.com/api/pay/<provider>/payment/complete-3ds. The handler verifies the callback, calls onCallback, and redirects the customer using callbackRedirect.

For PayTR, set the notification URL in the PayTR panel to https://yoursite.com/api/pay/paytr/callback.

On this page