Docs

Payments (iyzico, PayTR)

@vamos-ui/react/payments talks to iyzico's hosted Checkout Form and PayTR's iFrame API from your server: it signs requests, checks response and notification signatures, and never handles card data. Sandbox / test mode unless you switch it off.

Ground rules

  • Server only: route handlers, server actions, a Node or edge function. Web Crypto and fetch, no SDK dependency.
  • Keys come only from the config you pass — read them from environment variables. They are never logged and never appear in error messages.
  • iyzico defaults to mode: "sandbox" (sandbox-api.iyzipay.com); PayTR defaults to testMode: true (test_mode=1). Real payments need an explicit mode: "live" / testMode: false.
  • Card details are entered on the provider's hosted page or iframe, never in your form.
  • A redirect back to your site is not proof of payment. Only a verified result (paid: true) is.

iyzico Checkout Form

initializeCheckoutForm starts a session and returns paymentPageUrl (redirect) and checkoutFormContent (inline form). The basket total must equal the sum of the items; prices are decimal strings. After payment iyzico posts a token to your callbackUrl; retrieveCheckoutForm(token) asks iyzico for the result and checks its signature.

// lib/iyzico.ts
import { createIyzicoClient } from "@vamos-ui/react/payments";

export const iyzico = createIyzicoClient({
  apiKey: process.env.IYZICO_API_KEY!,
  secretKey: process.env.IYZICO_SECRET_KEY!,
  mode: process.env.IYZICO_MODE === "live" ? "live" : "sandbox",
});

// app/odeme/actions.ts
"use server";
export async function startCheckout(orderId: string) {
  const order = await orders.get(orderId);
  const { paymentPageUrl } = await iyzico.initializeCheckoutForm({
    price: order.total,                       // "349.90" = sum of basketItems[].price
    basketId: order.id,
    conversationId: order.id,
    callbackUrl: "https://ornek.com.tr/odeme/sonuc",
    buyer: { id: order.customerId, name: "Deniz", surname: "Kaya", identityNumber: "10000000146", email: "deniz@ornek.com.tr",
             gsmNumber: "+902160000000", registrationAddress: "Rasimpaşa Mah. Rıhtım Cad. No: 14", city: "İstanbul", country: "Turkey" },
    billingAddress: { contactName: "Deniz Kaya", city: "İstanbul", country: "Turkey", address: "Rasimpaşa Mah. Rıhtım Cad. No: 14" },
    basketItems: [{ id: "W1", name: "Seramik atölyesi", category1: "Atölye", itemType: "VIRTUAL", price: "349.90" }],
  });
  redirect(paymentPageUrl!);
}

// app/odeme/sonuc/route.ts — iyzico posts the token here
export async function POST(req: Request) {
  const token = (await req.formData()).get("token");
  const result = await iyzico.retrieveCheckoutForm(token);
  if (result.paid) await orders.markPaid(result.basketId!, { paymentId: result.paymentId, amount: result.paidPrice });
  return Response.redirect(new URL(result.paid ? "/odeme/tamam" : "/odeme/hata", req.url), 303);
}
Result fieldMeaning
paidstatus success, signature verified and paymentStatus SUCCESS — the only field to act on.
verifiedThe response signature (paymentStatus, paymentId, currency, basketId, conversationId, paidPrice, price, token) matched.
paidPrice / priceAs iyzico returned them; compare with your order before fulfilling it.
errorCode / errorMessageiyzico's own error, for failures.
rawThe full response (keep it out of client responses).

iyzico notifications

If merchant notifications are enabled for your account, iyzico posts JSON to your notification URL with an X-IYZ-SIGNATURE-V3 header. verifyIyzicoWebhook supports the direct and the HPP (checkout form) formats; answer with a 2xx once handled.

import { verifyIyzicoWebhook } from "@vamos-ui/react/payments";

export async function POST(req: Request) {
  const body = await req.json();
  if (!(await verifyIyzicoWebhook(process.env.IYZICO_SECRET_KEY!, req.headers.get("x-iyz-signature-v3"), body))) {
    return new Response("bad signature", { status: 400 });
  }
  // body.status, body.paymentConversationId … — re-check with retrieveCheckoutForm(body.token) before fulfilling
  return new Response(null, { status: 204 });
}

PayTR iFrame API

Step 1: createPaytrIframeToken computes paytr_token, asks PayTR for an iframe token and returns the iframe URL. Amounts are decimal strings, sent as kuruş. Step 2: PayTR posts the result to the notification URL set in the merchant panel; verifyPaytrCallback checks its hash, and the route answers with the plain text OK — also for repeated notifications of an order already handled.

import { createPaytrIframeToken, verifyPaytrCallback, PAYTR_CALLBACK_OK, PAYTR_IFRAME_RESIZER } from "@vamos-ui/react/payments";

const paytr = {
  merchantId: process.env.PAYTR_MERCHANT_ID!,
  merchantKey: process.env.PAYTR_MERCHANT_KEY!,
  merchantSalt: process.env.PAYTR_MERCHANT_SALT!,
  testMode: process.env.PAYTR_TEST_MODE !== "0",
};

// Step 1 (server component or action)
const { iframeUrl } = await createPaytrIframeToken(paytr, {
  merchantOid: "YA20261115001",           // letters and digits, unique per order
  email: "deniz@ornek.com.tr",
  amount: "349.90",
  userIp,                                  // the customer's public IP
  basket: [{ name: "Seramik atölyesi", price: "349.90", quantity: 1 }],
  userName: "Deniz Kaya", userAddress: "Rasimpaşa Mah. Rıhtım Cad. No: 14, Kadıköy/İstanbul", userPhone: "+902160000000",
  okUrl: "https://ornek.com.tr/odeme/tamam", failUrl: "https://ornek.com.tr/odeme/hata",
});
// <iframe src={iframeUrl} id="paytriframe" className="w-full" /> + <Script src={PAYTR_IFRAME_RESIZER} />

// Step 2: app/api/paytr/bildirim/route.ts
export async function POST(req: Request) {
  const result = await verifyPaytrCallback(paytr, await req.formData());
  if (!result.verified) return new Response("bad hash", { status: 400 });
  if (result.paid) await orders.markPaidOnce(result.merchantOid, result.totalAmount); // idempotent
  return new Response(PAYTR_CALLBACK_OK, { headers: { "Content-Type": "text/plain" } });
}
The notification URL
PayTR's notification URL must be reachable without a login and must answer with nothing but OK. Customers never visit it; session data is not available there — find the order by merchantOid.

Reference

ExportDoes
createIyzicoClient(config){ mode, initializeCheckoutForm(req), retrieveCheckoutForm(token, opts?) }. config: apiKey, secretKey, mode?, baseUrl?, fetch?.
verifyIyzicoWebhook(secretKey, signature, body)X-IYZ-SIGNATURE-V3 check (direct and HPP formats).
iyzicoSignature · iyzicoAuthorization · iyzicoTrimPrice · iyzicoCheckoutBodyThe building blocks: response signature, IYZWSv2 header, trailing-zero rule, validated request body.
createPaytrIframeToken(config, req)Step 1: { token, iframeUrl }. config: merchantId, merchantKey, merchantSalt, testMode? (default true), fetch?.
paytrTokenFields(config, req)The form fields of the token request, paytr_token included, without sending them.
verifyPaytrCallback(config, form)Step 2: { verified, paid, merchantOid, status, totalAmount, paymentAmount, testMode, failedReasonCode? … }.

Tests cover iyzico's published response-signature sample and its trailing-zero table; the other signatures are checked against an independent node:crypto implementation of the documented formulas.