Skip to content
Better Payment

Next.js

Integrating Better Payment with the Next.js App Router.

1. Create the singleton

// lib/payment.ts
import { betterPayment, iyzico } from 'better-payment';

const createPayment = () =>
  betterPayment({
    mode: process.env.NODE_ENV === 'production' ? 'production' : 'sandbox',
    providers: {
      iyzico: iyzico({
        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}`,
    },
  });

let instance: ReturnType<typeof createPayment> | undefined;

export const getBetterPayment = () => (instance ??= createPayment());

2. Create the catch-all route handler

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

export const { GET, POST } = toNextJsHandler(getBetterPayment);

Passing the getBetterPayment function (not its result) keeps the initialization lazy: the instance is created on the first request, so next build works without the environment variables.

The adapter passes JSON and form-urlencoded bodies (the iyzico, Param, Akbank and PayTR callbacks) to the handler as raw text, and writes the handler's response unchanged:

  • PayTR's plain-text OK is not JSON-encoded.
  • Redirects (callbackRedirect) keep their Location header and have no body.

The route works on both runtimes. Add export const runtime = 'edge' to run it on the edge runtime.

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.

Complete example

examples/next-shop is a runnable App Router shop built with this setup: Prisma orders, amounts from transformRequest, a verified 3D Secure callback, payment events and a privileged refund route. It runs on MockProvider without credentials.