Skip to content
Better Payment

PayTR

PayTR integration reference — iFrame payments, notifications, refunds, status, BIN and installments.

PayTR uses form-urlencoded requests. Every token is signed as base64(HMAC-SHA256(data + merchant_salt, merchant_key)).

Configuration

import { betterPayment, paytr } from 'better-payment';

const payment = betterPayment({
  providers: {
    paytr: paytr({
      merchantId: process.env.PAYTR_MERCHANT_ID!,
      merchantKey: process.env.PAYTR_MERCHANT_KEY!,
      merchantSalt: process.env.PAYTR_MERCHANT_SALT!,
      // testMode: defaults to mode === 'sandbox' (sends test_mode=1)
      // timeoutLimit: 30 (minutes)
      // threeDSMode: 'iframe' | 'direct' (default 'iframe', see Direct API 3D Secure)
    }),
  },
});

Order ids

PayTR's merchant_oid may contain only letters and digits. Pass your order id as conversationId. If you leave it out, an id such as BP1727180000000A1B2C3D4 is generated and returned as paymentId / conversationId. Store it: refund, cancel and getPayment use it.

iFrame payment (3D Secure)

const init = await payment.paytr.initThreeDSPayment({
  ...paymentRequest,            // buyer.gsmNumber and billingAddress.address are sent to PayTR
  conversationId: 'ORDER123',
  callbackUrl: 'https://yoursite.com/orders/ORDER123', // where the customer returns
  installment: undefined,       // undefined: any; 1: single payment only; n: up to n
});

// init.threeDSHtmlContent — a page with the PayTR iframe
// init.redirectUrl        — https://www.paytr.com/odeme/guvenli/<token>

payment_amount is paidPrice (in kuruş). Basket items are sent with TL prices.

Direct API 3D Secure (your own card form)

Added in 0.12.0

With threeDSMode: 'direct', initThreeDSPayment() takes the card in the request instead of showing PayTR's iFrame, so you keep your own checkout design. It returns a form signed with the Direct API token (non_3d=0) that the customer's browser posts to PayTR, as PayTR requires for card data. PayTR runs 3D Secure, redirects to callbackUrl (or failUrl) and sends the result to the notification URL, as with the iFrame.

const payment = betterPayment({
  providers: {
    paytr: paytr({ merchantId, merchantKey, merchantSalt, threeDSMode: 'direct' }),
  },
});

const init = await payment.paytr.initThreeDSPayment({
  ...paymentRequest,            // paymentCard comes from your own checkout form
  conversationId: 'ORDER123',
  callbackUrl: 'https://yoursite.com/orders/ORDER123',
  // failUrl?: string (defaults to callbackUrl)
  // saveCard?: true (PayTR stores the card; the utoken comes in the notification)
});
// init.threeDSHtmlContent auto-submits the signed card form to https://www.paytr.com/odeme

The Direct API must be enabled on your PayTR account. Stored cards always use this path, whatever threeDSMode is.

Notification (Bildirim URL)

callbackUrl does not carry the payment result. PayTR sends the result server-to-server to the notification URL set in the PayTR merchant panel.

  1. Set the notification URL to https://yoursite.com/api/pay/paytr/callback.
  2. With the HTTP handler, the notification is verified, passed to onCallback, and answered with plain-text OK:
handler: {
  onCallback: async (result) => {
    // result.paymentId === merchant_oid
    // result.status is 'success' or 'failure'
    await db.orders.updatePayment(result.paymentId, result.status);
  },
}

Without the handler:

const result = await payment.paytr.completeThreeDSPayment(req.body);
if (result.errorCode === 'INVALID_HASH') return res.status(400).send('bad hash');
await db.orders.updatePayment(result.paymentId, result.status); // make this idempotent
res.type('text/plain').send('OK');

Direct payment (non-3D)

const result = await payment.paytr.createPayment(paymentRequest);

This uses the Direct API with non_3d=1 and sync_mode=1. Your PayTR account must be approved for non-3D payments.

Stored cards (utoken / ctoken)

const init = await payment.paytr.createPaymentWithToken({
  utoken, ctoken, cvv, price: '100.00',
  callbackUrl: 'https://yoursite.com/orders/ORDER123',
  conversationId: 'ORDER123',
  buyer: { email, name, surname, ip, gsmNumber },
  basketItems: [{ name: 'Item', price: '100.00', quantity: 1 }],
});
// init.threeDSHtmlContent auto-submits a Direct API form from the browser

Refund, Cancel & Status

await payment.paytr.refund({ paymentId: 'ORDER123', price: '50.00', currency: 'TRY', ip: '1.2.3.4' });

// PayTR has no void; cancel() is a full refund. Without price, the paid amount is looked up.
await payment.paytr.cancel({ paymentId: 'ORDER123', ip: '1.2.3.4' });

const status = await payment.paytr.getPayment('ORDER123'); // /odeme/durum-sorgu
// 'success', or 'cancelled' when fully refunded

BIN & Installments

const bin = await payment.paytr.binCheck('552879');
// bin.bankName, bin.cardFamily (brand), bin.cardAssociation (schema), bin.cardType

const info = await payment.paytr.installmentInfo({ binNumber: '552879', price: '100.00' });
// Totals use the commission rates defined on your PayTR account (/odeme/taksit-oranlari)