Better Payment
Kavramlar

HTTP Handler

Framework'ten bağımsız, varsayılan olarak güvenli HTTP handler.

BetterPaymentHandler, HTTP isteklerini sağlayıcı metotlarına eşler. Her framework'le çalışır: framework'ün isteğini bir BetterPaymentRequest'e çevirirsiniz ve BetterPaymentResponse'u geri gönderirsiniz.

Güvenli varsayılanlar

Hiç seçenek verilmezse handler yalnızca şunları açar:

  • sağlayıcı callback'leri: payment/complete-3ds ve callback
  • kart sorguları: installment ve bin-check
  • GET /health

Geri kalan her şeyi allowedActions ile açıkça etkinleştirin. İade, iptal, ödeme sorgusu ve abonelik yönetimi ayrıca bir authorize hook'u gerektirir; hook yoksa handler oluşturulurken hata fırlatılır.

const payment = betterPayment({
  providers: { /* ... */ },
  handler: {
    basePath: '/api/pay', // default
    allowedActions: ['payment/init-3ds', 'payment/complete-3ds', 'callback', 'refund', 'payment/get'],

    authorize: async (ctx) => {
      // callbacks come from the bank or the customer's browser and are signature-checked
      if (ctx.action === 'callback' || ctx.action === 'payment/complete-3ds') return true;
      if (ctx.action === 'payment/init-3ds') return !!(await getSession(ctx.request.headers.cookie));
      return isAdmin(ctx.request.headers.authorization);
    },

    // Build the provider request on the server; never trust the browser's amounts
    transformRequest: async (ctx) => {
      if (ctx.action !== 'payment/init-3ds') return ctx.body;
      const order = await db.orders.find(ctx.body?.orderId);
      return {
        price: order.total,
        paidPrice: order.total,
        currency: 'TRY',
        conversationId: order.id,
        basketId: order.id,
        callbackUrl: `${process.env.APP_URL}/api/pay/iyzico/payment/complete-3ds`,
        paymentCard: ctx.body?.paymentCard,
        buyer: order.buyer,
        shippingAddress: order.shippingAddress,
        billingAddress: order.billingAddress,
        basketItems: order.items,
      };
    },

    // Called with verified callback results (3D return, PayTR notification)
    onCallback: async (result, ctx) => {
      await db.orders.updatePayment(result.paymentId, result.status);
    },

    // Redirect the customer after 3D Secure (303 See Other)
    callbackRedirect: (result) => `/orders/${result.paymentId}?payment=${result.status}`,

    exposeErrors: false, // default: 500 responses do not include internal error messages
  },
});

Birden fazla handler'a ihtiyacınız olduğunda payment.createHandler(options) kullanın; örneğin herkese açık bir handler ve bir yönetici handler'ı.

Route'lar

Yollar basePath'e göredir (varsayılan /api/pay).

MetotYolİşlemSağlayıcı metoduVarsayılan
POST/:provider/payment/complete-3dspayment/complete-3dscompleteThreeDSPayment()
POST/:provider/callbackcallbackcompleteThreeDSPayment()
POST/:provider/installmentinstallmentinstallmentInfo()
POST/:provider/bin-checkbin-checkbinCheck()
POST/:provider/paymentpaymentcreatePayment()—
POST/:provider/payment/init-3dspayment/init-3dsinitThreeDSPayment()—
POST/paytr/payment/tokenpayment/tokencreatePaymentWithToken()—
GET/:provider/payment/:idpayment/getgetPayment()— 🔒
POST/:provider/refundrefundrefund()— 🔒
POST/:provider/cancelcancelcancel()— 🔒
POST/:provider/authorizeauthorizeauthorize()—
POST/:provider/authorize/init-3dsauthorize/init-3dsinitThreeDSAuthorize()—
POST/:provider/capturecapturecapture()— 🔒
POST/:provider/voidvoidvoidAuthorization()— 🔒
POST/:provider/cards/savecards/savesaveCard()— 🔒
POST/:provider/cards/listcards/listlistCards()— 🔒
POST/:provider/cards/deletecards/deletedeleteCard()— 🔒
POST/iyzico/checkout/initcheckout/initinitCheckoutForm()—
POST/iyzico/checkout/retrievecheckout/retrieveretrieveCheckoutForm()—
POST/iyzico/pwi/initpwi/initinitPWIPayment()—
POST/iyzico/pwi/retrievepwi/retrieveretrievePWIPayment()—
POST/iyzico/subscription/initializesubscription/initializeinitializeSubscription()—
POST/iyzico/subscription/{cancel,upgrade,retrieve,card-update}……— 🔒
POST/iyzico/subscription/{product,pricing-plan}……— 🔒
GET/health——

🔒 = authorize gerektirir.

Yanıtlar

DurumHTTP durum koduGövde
success / pending sonucu200sağlayıcı sonucu
failure sonucu422sağlayıcı sonucu (errorCode, errorMessage, rawResponse)
PayTR bildirimi (/paytr/callback)200 text/plainOK (hash geçersizse 400)
callbackRedirect bir URL döndürdü303Location başlığı
Geçersiz girdi / yanlış metot / etkin değil400 / 405 / 404{ error, message }
authorize reddetti403{ error, message }
Beklenmeyen hata500{ error, message: 'Internal server error' }

PayTR, tam olarak OK alana kadar bildirimi yeniden gönderir. res.headers['Content-Type'] text/plain olduğunda res.body'yi düz metin olarak döndürün; JSON'a çevirmeyin. onCallback hata fırlatırsa handler 500 döner ve PayTR bildirimi tekrar gönderir.

Tekrar eden callback'ler ve Idempotency-Key

Sağlayıcılar callback'leri birden fazla kez iletir: PayTR OK alana kadar bildirimi yeniden gönderir, müşteriler de 3D Secure dönüş sayfasını yeniler ya da geri döner. Handler her callback'i bir kez işler:

  • İlk iletim sağlayıcıyı çağırır (doğrulama; Parampos'ta ayrıca TP_WMD_Pay ile tamamlama), onCallback'i çalıştırır ve yanıtı saklar.
  • Aynısı tekrar geldiğinde saklanan yanıtı alır (PayTR: OK, tarayıcı: aynı yönlendirme). onCallback tekrar çalışmaz, sağlayıcı da tekrar çağrılmaz.
  • onCallback hata fırlatırsa handler 500 döner (PayTR tekrar dener). Sonraki iletim, sağlayıcıyı yeniden çağırmadan saklanan sağlayıcı sonucuyla onCallback'i tekrar çalıştırır.
  • İlki hâlâ işlenirken gelen bir tekrar 409 alır; sağlayıcı daha sonra yeniden dener.
  • Sahte callback'ler (INVALID_HASH) ve sonucu bilinmeyenler (NETWORK_ERROR) asla saklanmaz.

Tekrarlar, callback gövdesinin SHA-256 hash'iyle tanınır. Sahte bir gövde, saklanan bir yanıtla asla eşleşemez.

Veri değiştiren route'lar (payment, payment/init-3ds, refund, cancel, authorize, capture, void, cards/save, cards/delete, iyzico checkout/PWI/abonelik yönetimi) ayrıca bir Idempotency-Key başlığını da dikkate alır. Her işlem için benzersiz bir anahtar gönderin; örneğin bir UUID ya da refund-<orderId>:

  • Aynı anahtar ve aynı gövde, sağlayıcıyı yeniden çağırmadan ilk yanıtı Idempotent-Replayed: true işaretiyle döner.
  • Aynı anahtar farklı bir gövdeyle gelirse 422 döner.
  • Sonuç üretilmeden başarısız olan bir istek (doğrulama hatası, beklenmeyen hata) anahtarını serbest bırakır.

Sonuçlar varsayılan olarak 24 saat saklanır. Varsayılan depo bellek içidir, yani yalnızca tek bir süreç içinde tekilleştirir. Birden fazla örnek ya da serverless fonksiyon kullanıyorsanız paylaşımlı bir depo verin:

import Redis from 'ioredis';
import type { IdempotencyStore } from 'better-payment';

const redis = new Redis(process.env.REDIS_URL!);

const redisStore: IdempotencyStore = {
  get: async (key) => (await redis.get(key)) ?? undefined,
  set: async (key, value, ttl) => void (await redis.set(key, value, 'EX', ttl)),
  setIfAbsent: async (key, value, ttl) => (await redis.set(key, value, 'EX', ttl, 'NX')) === 'OK',
  delete: async (key) => void (await redis.del(key)),
};

betterPayment({
  providers: { ... },
  handler: {
    idempotency: { store: redisStore, ttlSeconds: 7 * 24 * 3600 }, // or `false` to turn it off
  },
});

setIfAbsent atomik olmalıdır: Redis'te SET ... NX, SQL'de ise benzersiz anahtarlı bir tabloya ekleme (INSERT ... ON CONFLICT DO NOTHING ve etkilenen satır sayısını kontrol edin).

Arayüzler

interface BetterPaymentRequest {
  method: string;
  url: string; // absolute or path-only
  headers: Record<string, string>;
  body?: unknown; // parsed object, or a raw string (JSON / x-www-form-urlencoded)
}

interface BetterPaymentResponse {
  status: number;
  headers: Record<string, string>;
  body: unknown;  // JSON object, or a string for text/plain responses
}

interface HandlerContext {
  provider: string | undefined; // the provider id; undefined for plugin endpoints
  action: HandlerAction;        // 'payment/init-3ds', ... or 'plugin:<plugin id>/<endpoint name>'
  params: Record<string, string>; // { paymentId } for payment/get
  request: BetterPaymentRequest;
  body: Record<string, unknown> | undefined; // undefined when there is no object body
}

ctx.body client'tan gelir, bu yüzden alanları unknown tipindedir: kullanmadan önce doğrulayın (ya da doğruladıktan sonra cast edin). onCallback ve callbackRedirect bir PaymentResponse alır.

Plugin'ler handler'a yeni adresler ekleyebilir (plugin endpoint'leri). authorize hook'u bunlar için de çalışır; provider: undefined ve action: 'plugin:<plugin id>/<endpoint adı>' olur. Callback'lerde ödeme olayları onCallback'ten hemen önce yayınlanır; bir dinleyici hata verirse onCallback ile birlikte yeniden denenir.

Framework'ler

Adapter'lar handler'ı framework'ünüze bağlar ve yanlış yapılması kolay ayrıntıları halleder:

  • form-urlencoded banka callback'leri için ham gövdeler
  • PayTR'nin düz metin OK yanıtı
  • gövdesiz yönlendirmeler
FrameworkImportKullanım
Next.js (App Router)better-payment/nextexport const { GET, POST } = toNextJsHandler(getBetterPayment)
Expressbetter-payment/expressapp.all('/api/pay/*path', toExpressHandler(payment))
Fastifybetter-payment/fastifyapp.register(toFastifyPlugin(payment), { prefix: '/api/pay' })
Honobetter-payment/honoapp.all('/api/pay/*', toHonoHandler(payment))
Elysiabetter-payment/elysiaapp.all('/api/pay/*', toElysiaHandler(payment), { parse: 'none' })
node:httpbetter-payment/expresscreateServer(toNodeHandler(payment))
Workers, Deno, Bunbetter-paymenttoFetchHandler(payment)

Next.js, diğer framework'ler ve edge ortamları sayfalarına bakın. Başka bir framework için isteğini bir BetterPaymentRequest'e çevirin (gövdeyi ham metin olarak geçirin), payment.handler.handle()'ı çağırın ve BetterPaymentResponse'u geri yazın.

Bu sayfada