FPX ialah rangkaian pembayaran perbankan dalam talian Malaysia. Kedai dalam talian biasanya sampai kepadanya melalui gateway pembayaran: halaman bayaran gateway memaparkan senarai bank, dan kod anda hanya bercakap dengan gateway.

Contoh di bawah memakai bayarkit, pustaka sumber terbuka yang kami selenggara, yang meletakkan empat belas gateway Malaysia di sebalik satu antara muka. Langkahnya sama jika anda memanggil API gateway sendiri.

Bagaimana bayaran FPX bergerak

  • Pelayan anda meminta gateway mencipta bayaran untuk satu jumlah dan satu rujukan.
  • Pelanggan pergi ke halaman gateway, memilih bank dan meluluskan bayaran dalam perbankan dalam talian.
  • Gateway menghantar pelanggan kembali ke URL return anda dan, secara berasingan, menghantar callback ke pelayan anda.
  • Pelayan anda menyemak callback, bertanya status bayaran kepada gateway, dan barulah menanda pesanan dibayar.

1. Pasang

Shell
npm install @bayarkit/core @bayarkit/toyyibpay

bayarkit memerlukan Node.js 22.12 atau lebih baharu. Adapter memakai node:crypto, jadi Cloudflare Workers dan Vercel Edge tidak disokong; pelayan Node.js biasa, atau route handler Next.js pada runtime Node.js, boleh.

2. Cipta bayaran

Jumlah ialah sen integer: 1050 ialah RM10.50, jadi ringgit titik terapung tidak pernah sampai ke perbandingan. Beri setiap cubaan bayaran rujukannya sendiri, supaya callback lewat dari cubaan lama tidak boleh sepadan dengan yang baharu.

TypeScript
// lib/payment.ts
import { toyyibpay } from '@bayarkit/toyyibpay'

export const gateway = toyyibpay({
  secretKey: process.env.TOYYIBPAY_SECRET_KEY ?? '',
  categoryCode: process.env.TOYYIBPAY_CATEGORY_CODE ?? '',
  environment: 'sandbox', // 'production' bila sudah hidup
})
TypeScript
// app/api/checkout/route.ts
import { checkoutResponse } from '@bayarkit/core'
import { gateway } from '@/lib/payment'
import { orders } from '@/lib/orders' // pangkalan data anda

export async function POST(request: Request) {
  const { orderId } = (await request.json()) as { orderId: string }
  const order = await orders.get(orderId)
  const reference = `ORD${order.id}T${Date.now()}` // satu rujukan bagi setiap cubaan
  const payment = await gateway.createPayment({
    reference,
    amount: order.amount, // sen: 1050 = RM10.50
    description: `Order ${order.id}`,
    customer: { name: order.name, email: order.email },
    returnUrl: 'https://shop.example/thank-you',
    callbackUrl: 'https://shop.example/api/payment/callback',
  })
  await orders.update(order.id, { reference, paymentId: payment.id })
  return checkoutResponse(payment, 'Teruskan ke pembayaran') // ubah hala 303 ke senarai bank gateway
}

3. Sahkan bayaran dalam callback

Callback ialah dakwaan, bukan bukti. Ia boleh dipalsukan, diulang dari bayaran lama, atau benar tetapi untuk pesanan atau jumlah lain. confirmPayment membuat tiga semakan setiap kali: tandatangan callback, pertanyaan status baharu kepada gateway, dan padanan tepat pada rujukan, jumlah dan mata wang.

TypeScript
// app/api/payment/callback/route.ts
import { confirmPayment, readCallback } from '@bayarkit/core'
import { gateway } from '@/lib/payment'
import { orders } from '@/lib/orders'

export async function POST(request: Request) {
  const callback = await readCallback(request) // toyyibPay menghantar multipart/form-data
  const verified = gateway.verifyCallback(callback) // null bila tandatangan salah
  const order = verified ? await orders.findByReference(verified.reference) : undefined
  if (!order) return new Response('unknown order', { status: 404 })
  if (order.status === 'paid') return new Response('OK') // gateway menghantar semula callback: jawab dengan cara yang sama
  const result = await confirmPayment(gateway, callback, { reference: order.reference, amount: order.amount })
  if (result.ok) await orders.update(order.id, { status: 'paid' })
  // Bila !result.ok, log result.reason: invalid_signature, not_paid, mismatch atau gateway_error.
  return new Response('OK')
}

Jangan sekali-kali menanda pesanan dibayar dari URL return. Pelayar pelanggan yang membawanya, jadi sesiapa pun boleh menaipnya.

Apa yang ditunjukkan sandbox kepada kami

Kami menjalankan bayaran berjaya dan gagal melalui empat sandbox gateway pada Oktober 2026. Setiap dapatan di bawah merosakkan kod yang ditulis berdasarkan dokumen sahaja.

  • toyyibPay menghantar callback sebagai multipart/form-data. Kod yang hanya menghurai badan URL-encoded nampak callback kosong.
  • toyyibPay menjawab No data found! untuk bil tanpa transaksi, dan memberi jawapan sama kepada secret key yang salah. Kunci salah kelihatan seperti bayaran yang tidak pernah disahkan, bukan seperti ralat.
  • Billplz menghantar callback untuk bayaran gagal juga, dengan paid=false, dan callbacknya tidak membawa rujukan anda. Cari pesanan ikut id bil yang disimpan semasa checkout.
  • Stripe tidak menghantar sebarang event checkout.session bila bayaran FPX gagal: sesi kekal terbuka dan belum dibayar sehingga tamat tempoh.
  • Bayarcash memakai satu secret key untuk setiap portal dalam satu akaun, jadi callback yang ditandatangani dengan betul boleh milik portal lain. Adapter membandingkan nama portal dengan yang dikonfigurasi.

Menguji tanpa wang sebenar

Setiap gateway ini ada sandbox atau mod ujian. Untuk ujian automatik, @bayarkit/testing menjalankan gateway palsu bagi setiap gateway di localhost, jadi dua route handler di atas boleh berjalan dalam CI tanpa rangkaian atau kunci.

Menukar gateway kemudian

Dua route itu kekal sama; hanya adapter dan kuncinya berubah, contohnya bayarcash({ token, secretKey, portalKey, environment: 'sandbox' }) menggantikan toyyibpay(...). Beza antara gateway bagi pembangun ada dalam Gateway pembayaran Malaysia dibanding.