Direct API’de kart formu sitenizin bir parçasıdır ve kart bilgilerini Payniko’ya sunucunuz iletir. Müşterinin tarayıcısı Payniko’ya yalnızca 3-D Secure doğrulaması ve hosted BIN’ler için yönlendirilir. Kart formunun kendi sayfanızda yer alması gerekiyorsa Direct API’yi kullanın.
Entegrasyon adımları
BIN sorgulama
Müşteri ilk altı haneyi girdiğinde GET /api/payments/bin/{bin6} çağrısını yapın. payment_mode, kart bilgilerinin tamamını kendi formunuzda mı alacağınızı yoksa müşteriyi ödeme sayfasına mı yönlendireceğinizi belirtir.
Ödemeyi oluşturma
POST /api/payments/direct çağrısını kart bilgileriyle veya hosted BIN için yalnızca bin6 ile yapın.
3-D Secure adımını tamamlama
Durum requires_action ise tarayıcıyı next_action.url adresine yönlendirin. Tarayıcı ardından imzalı parametrelerle success_url veya fail_url adresinize geri döner.
Sonucu doğrulama
payment.succeeded veya payment.failed webhook’unu bekleyin ya da GET /api/payments/{payment_id} çağrısını yapın. Siparişi yalnızca succeeded durumunda teslim edin.
Ödeme akışı
Your server Payniko Payer's browser
1 GET /api/payments/bin/{bin6} ───► payment_mode: direct | hosted
2 POST /api/payments/direct ─────► 200 requires_action + next_action.url
(or processing / succeeded / failed)
3 redirect to next_action.url ──────────────────────────────────► 3-D Secure page
───────────────────────────► success_url / fail_url
4 webhook ◄─────────────────────── payment.succeeded / payment.failed
GET /api/payments/{payment_id} ──► current status
Ön koşullar
Direct API hesap bazında etkinleştirilir. Bu erişim olmadan yapılan istekler 4030055502 ile reddedilir.
BIN sorgusu, ödeme oluşturulmadan önce ödemenin kendi formunuzda alınıp alınamayacağını (direct) veya Payniko ödeme sayfasında tamamlanması gerektiğini (hosted) belirtir. Sorgu, karttan herhangi bir tahsilat yapılmasına yol açmaz.
Parametreler
Alan
Tür
Zorunlu
Açıklama
Örnek
bin6
string · path
Zorunlu
Kart numarasının ilk altı hanesi.
540061
amount
string · query
Zorunlu
İki ondalık basamaklı tutar, örneğin 100.00. Hesabınızın tutar sınırlarına göre kontrol edilir.
100.00
currency
string · query
İsteğe bağlı
USD, EUR, AED veya TRY. Varsayılan değer sistemin ana para birimidir. Ödemeyi oluştururken aynı para birimini kullanın.
USD
Diğer sorgu parametreleri reddedilir. Sorgu dizesi imzaya dahil değildir.
./payniko.sh GET '/api/payments/bin/540061?amount=100.00¤cy=USD'
Aşağıdaki alanları içeren { "data": ... } nesnesini döndürür:
Alan
Tür
Açıklama
bin6
string
Gönderilen altı hane.
bank
string | null
Kartı çıkaran banka (biliniyorsa).
network
string | null
Kart markası (biliniyorsa).
visamastercardamextroy
card_type
string | null
Kart türü (biliniyorsa).
creditdebitprepaid
card_program
string | null
Kart programı. Şu anda her zaman null.
country
string | null
Kartı çıkaran ülke (ISO 3166-1 alpha-2; biliniyorsa).
domestic
boolean
Yurt içi kartlarda true; diğer durumlarda veya bilinmiyorsa false.
payment_mode
string | null
direct: kart bilgilerinin tamamını alın. hosted: müşteriyi ödeme sayfasına yönlendirin. null dahil diğer tüm değerler: ödeme oluşturmayın.
directhosted
Yanıtın değerlendirilmesi
direct: kart bilgilerinin tamamını alın ve ödemeyi card ile oluşturun.
hosted: ödemeyi yalnızca bin6 ile oluşturun. Müşteri kart bilgilerini Payniko ödeme sayfasında girer.
Diğer payment_mode değerleri veya 409 kart hataları (0055230, 0055231, 0055233): ödeme oluşturmayın; bu bir banka reddi olmadığından müşteriden nötr bir mesajla farklı bir kart isteyin. 4220055235 bir tutar sorununa işaret eder. 429 veya 5xx durumunda aynı sorguyu daha sonra yineleyin.
Örnek
import {api} from './payniko.mjs';
let mode = null;
try {
const lookup = await api('GET', '/api/payments/bin/411111?amount=100.00¤cy=USD');
mode = lookup.data.payment_mode;
} catch (error) {
// No answer, 429 or 5xx: temporary. Try the same lookup again later.
if (error.status === undefined || error.status === 429 || error.status >= 500) throw error;
// Other 4xx: error.problem.code says why, for example 0055233.
}
if (mode !== 'direct' && mode !== 'hosted') {
// Also null: do not create a payment. Ask for another card with a neutral message.
}
Aynı istek yeniden gönderildiğinde de dahil olmak üzere her zaman güncel ödemeyle birlikte 200 döndürür. Kartın reddedilmesi bir HTTP hatası değildir; yanıt status: failed içeren 200 olur.
İstek gövdesi
Alan
Tür
Zorunlu
Açıklama
Örnek
merchant_order_id
string
Zorunlu
Sipariş referansınız: A-Z a-z 0-9 . _ - karakterlerinden oluşan 1–64 karakter. Her referans ve ortam için yalnızca bir ödeme oluşturulabilir; benzersizlik denetiminde büyük/küçük harf ayrımı yapılmaz.
ORDER-2026-002
amount
string
Zorunlu
İki ondalık basamaklı tutar, örneğin "100.00"; hesabınızın tutar sınırları içinde olmalıdır.
"100.00"
currency
string
İsteğe bağlı
Varsayılan değer sistemin ana para birimidir. BIN sorgusunda kullandığınız para birimini kullanın.
USDEURAEDTRY
USD
webhook_url
string · uri
Zorunlu
Bu ödemenin webhook'larını alan, 443 portunda herkese açık HTTPS adresi; en fazla 500 karakter. Yerel adresler sandbox dahil kabul edilmez; alan adı istek anında herkese açık bir IP adresine çözülmelidir, aksi halde 4229000008.
https://merchant.example/webhooks/payments
success_url
string · uri
Zorunlu
Ödeme başarılı olduğunda tarayıcının yönlendirileceği adres. HTTPS, en fazla 2048 karakter; kullanıcı bilgisi (user info) ve parça (fragment) içermemelidir. Sandbox ortamı http://localhost, 127.0.0.1 ve [::1] adreslerini de kabul eder.
https://merchant.example/payment/success
fail_url
string · uri
Zorunlu
Diğer tüm durumlarda tarayıcının yönlendirileceği adres. success_url için geçerli kurallar burada da uygulanır.
https://merchant.example/payment/failure
customer
object
Zorunlu
Ödeyenin iletişim bilgileri; ödeme sonucu, makbuz ve bankanın doğrulama adımları için kullanılır. Yalnızca email ve phone kabul edilir; customer içindeki başka bir alan 4221000001 ile reddedilir.
{ … }
customer.email
string
Zorunlu
Ödeyenin gerçek e-posta adresi: RFC 5322, en fazla 254 karakter, noktalı alan adı. Küçük harfe çevrilerek saklanır.
Ödeyenin gerçek telefon numarası, uluslararası biçimde (+ ülke kodu, E.164), örneğin +37061234567. Türkiye'ye özgü kural: +90 ve 10 haneli ulusal numara (5xx cep, 2xx, 3xx, 4xx sabit, 8xx özel). Ülke kodu verilmeyen numara Türkiye numarası (+90) sayılır.
+37061234567
card
object
Koşullu
Müşterinin girdiği kart bilgileri (aşağıdaki tabloya bakın). card veya bin6 alanlarından yalnızca birini gönderin.
{ … }
bin6
string
Koşullu
Yalnızca hosted BIN için: kart numarasının ilk altı hanesi. Müşteri kart bilgilerini ödeme sayfasında girer. direct BIN için yalnızca bin6 içeren bir istek 4090055230 yanıtını alır.
540061
card.number
string
Zorunlu
Boşluksuz 13–19 rakam, geçerli bir kontrol basamağıyla (American Express için 15 rakam).
4355090000044242
card.holder_name
string
Zorunlu
Kart üzerindeki ad, 2–120 karakter.
JONAS KAZLAUSKAS
card.expiry_month
string
Zorunlu
İki hane, 01 ile 12 arasında.
12
card.expiry_year
string
Zorunlu
İki hane, örneğin 30. Kartın süresi dolmuş olmamalı ve yıl en fazla 30 yıl ileride olabilir.
30
card.cvv
string
Zorunlu
Tam olarak 3 hane; American Express için 4 hane. Asla saklamayın veya günlüğe yazmayın.
123
Tüm kart değerleri dize türündedir. card içindeki bilinmeyen alanlar reddedilir.
hosted BIN için ödeme sayfası ödeyenden e-posta adresini ve telefon numarasını yeniden istemez. Gizlilik gereği customer ödeme nesnesinde ve webhook'larda döndürülmez.
Alan
Neden
errors içindeki mesaj
customer.email
Biçim geçersiz veya 254 karakterden uzun.
customer.email must be a valid e-mail address (max 254 characters).
customer.email
Yer tutucu, örnek veya tek kullanımlık adres.
customer.email must be the payer's real address; placeholder, example or disposable addresses (example.com, test@…, mailinator…) are not accepted.
customer.phone
Uluslararası biçimde geçerli bir numara değil.
customer.phone must be a valid phone number in international format (e.g. +37061234567).
customer.phone
Yinelenen veya ardışık rakam deseni.
customer.phone must be the payer's real number; repeated or sequential digit patterns (5555555555, 1234567890) are not accepted.
customer.phone
Türkiye numarası +90 ve 2, 3, 4, 5 veya 8 ile başlayan 10 hane biçiminde değil.
customer.phone for Türkiye must be +90 followed by a 10-digit number starting with 2, 3, 4, 5 or 8 (e.g. +905321234567).
HTTP 422 · 1000001
{
"type": "https://payniko.com/problems/1000001",
"title": "Validation failed",
"status": 422,
"detail": "The submitted fields are not valid.",
"code": "1000001",
"correlation_id": "4c7a1e9d-2b3f-4d85-9a6e-0f1b2c3d4e5f",
"errors": {
"customer.email": [
"customer.email must be the payer's real address; placeholder, example or disposable addresses (example.com, test@…, mailinator…) are not accepted."
],
"customer.phone": [
"customer.phone must be the payer's real number; repeated or sequential digit patterns (5555555555, 1234567890) are not accepted."
]
}
}
İstek customer yüzünden reddedilirse nedeni ödeyene açıkça gösterin (örneğin: kendi e-posta adresinizi veya telefon numaranızı girin) ve ödemeyi yeniden oluşturmadan önce alanı düzeltmesine izin verin. Direct API errors içindeki mesajları çevrilmiş olarak döndürür; tablo İngilizce metni gösterir.
Payniko ödeme linki sayfaları, ödeyenin orada girdiği e-posta adresi ve telefon numarasına aynı kuralları uygular ve ret nedenini ödeyene gösterir.
Tarayıcıyı next_action.url adresine yönlendirin (3-D Secure veya hosted BIN için ödeme sayfası).
processing
Webhook’u bekleyin ve yeni bir ödeme oluşturmayın.
succeeded
Siparişi teslim edin.
failed
failure.message değerini gösterin ve müşteriden farklı bir kart isteyin.
Payniko bankayla iletişim kurarken çağrı birkaç saniye sürebilir. En az 60 saniyelik bir zaman aşımı kullanın ve zaman aşımından sonra isteği aynı Idempotency-Key ve gövdeyle yeniden gönderin. İlk istek hâlâ çalışıyorken (en fazla 3 dakika) yeniden gönderim metadata.retry_class: retry_same_request ile 4090055503 döndürür: birkaç saniye bekleyip aynı isteği yeniden gönderin.
hosted BIN söz konusu olduğunda gönderilen card bilgileri dikkate alınmaz ve müşteri işleme ödeme sayfasında devam eder.
merchant_order_id için zaten bir ödeme mevcutsa veya aynı Idempotency-Key farklı bir istek gövdesiyle gönderilirse istek 4090055503 ile reddedilir.
Geçersiz kart bilgileri 4090055234 ile reddedilir. İlgili alan metadata.developer_detail ile belirtilir: card_number_invalid, card_expiry_invalid veya card_cvv_invalid. Müşteriden kart bilgilerini yeniden girmesini isteyin.
Eksik veya geçersiz iletişim bilgileri 4221000001 ile reddedilir; errors içinde customer, customer.email veya customer.phone belirtilir.
3. 3-D Secure ve tarayıcı
Durum requires_action olduğunda müşterinin tarayıcısı next_action.url adresini açmalıdır; bu adreste banka 3-D Secure doğrulamasını gerçekleştirir veya müşteri işleme ödeme sayfasında devam eder.
Sayfanın tamamını next_action.url adresine yönlendirin veya adresi yeni bir sekmede açın; her iki yöntem de her zaman desteklenir.
Sayfa kart bilgilerini yeniden istemez; yalnızca bankanın gerektirdiği tarayıcı verilerini toplar.
Her zaman en güncel next_action.url adresini, next_action.expires_at zamanından (3-D Secure adımının bitişi) önce kullanın. Önceki bir URL’yi hiçbir zaman yeniden açmayın.
Ardından tarayıcı imzalı parametrelerle success_url adresine (yalnızca succeeded durumunda) veya fail_url adresine yönlendirilir (bkz. Dönüş URL’si). Bu adımdan sonra ödemeyi sorgulayın.
Çerçeve içinde gösterim
3-D Secure sayfası bir çerçeve içinde gösterilemez; sayfanın tamamını yönlendirin veya yeni bir sekme açın. Yalnızca hosted BIN’e ait ödeme sayfası, yalnızca canlı ortam anahtarlarıyla ve yalnızca ödemenin success_url adresiyle birebir aynı kaynak adres (origin) üzerinde çerçeve içinde gösterilebilir. Hosted API ödeme sayfası hiçbir zaman çerçeve içinde gösterilemez.
hosted BIN’e ait ödeme sayfası, ödeme hizmeti sağlayıcısının kendi güvenli ödeme ve 3-D Secure ekranını içerir ve kendi ödeme formunuzun içinde iframe olarak gösterilebilir.
hosted BIN’e ait ödeme sayfasını çerçeve içinde gösterirseniz, sayfanın pencerenize gönderdiği mesajlar yalnızca ödemeyi kontrol etmeniz gerektiğine dair bir sinyaldir. Bu mesajları yalnızca next_action.url adresinin kaynak adresinden (origin) ve kendi çerçevenizden kabul edin, içeriklerini dikkate almayın ve ödemeyi sunucunuz üzerinden sorgulayın.
Tarayıcı
// Only if you show the payment page of a hosted BIN in a frame on the exact origin of success_url.
// nextActionUrl: next_action.url from your server.
const pageOrigin = new URL(nextActionUrl).origin;
window.addEventListener('message', (event) => {
if (event.origin !== pageOrigin || event.source !== frame.contentWindow) return;
// A wake-up call only: ignore event.data. Ask your server to read the payment.
fetch('/checkout/ORDER-2026-002/refresh', {method: 'POST', credentials: 'same-origin'});
});
Ödeme nesnesi
Oluşturma ve sorgulama çağrıları bu nesneyi data içinde döndürür; her ödeme webhook’u da bu nesneyi içerir. Nesnenin yapısı Hosted API ve Direct API için aynıdır.
Alan
Tür
Açıklama
payment_id
string · LUVEI-ABCD-123456
Ödemenin Payniko ID’si. Bu değeri siparişinizle birlikte saklayın.
merchant_order_id
string
Gönderdiğiniz şekliyle sipariş referansınız.
flow
string
Hosted API için hosted, Direct API için direct.
hosteddirect
status
string
Ödemenin güncel durumu; aşağıdaki durum tablosuna bakın.
Tutarın para birimi; para birimi belirtilmediyse sistemin ana para birimi.
environment
string
Ödemeyi oluşturan anahtarın ortamı.
sandboxlive
next_action
object | null
Müşteriden beklenen bir sonraki işlem. Yalnızca status değeri requires_action iken doldurulur.
next_action.type
string
Her zaman redirect.
next_action.url
string · uri
Bu URL’yi müşterinin tarayıcısında, döndüğü şekliyle açın. URL’yi hiçbir zaman kendiniz oluşturmayın veya değiştirmeyin.
next_action.expires_at
string · date-time
URL’nin kullanılabileceği son zaman. Direct API’de bu değer 3-D Secure adımının bitişidir ve ödemenin kendi expires_at değerinden önce olabilir; Hosted API’de ise ödemenin son geçerlilik zamanıdır. Bu süre dolduğunda ödeme bir sonraki durumuna geçer ve next_action alanı null olur.
card
object | null
Maskelenmiş kart bilgileri; belirlendiğinde doldurulur.
Başarısızlık nedeni. Yalnızca status değeri failed olduğunda doldurulur.
failure.code
string
İş mantığınızda kullanılacak neden kodu; aşağıdaki listeye bakın.
failure.message
string
Müşteriye düz metin olarak gösterilmeye uygun kısa mesaj.
created_at
string · date-time
Ödemenin oluşturulduğu zaman.
expires_at
string · date-time
Ödemenin tamamlanması için son zaman.
completed_at
string · date-time | null
Ödemenin nihai bir duruma ulaştığı zaman.
next_action, requires_action durumunda bile kısa bir süre null olabilir; bu durumda ödemeyi yeniden sorgulayın. Kart numarasının tamamı, CVV, son kullanma tarihi ve kart sahibinin adı hiçbir zaman döndürülmez. card.network ve card.bank bilinmediğinde null olabilir.
Ödeme oluşturulurken gönderilen customer bilgileri bu nesnenin parçası değildir ve webhook'larda yer almaz.
Tarayıcının, 3-D Secure veya hosted BIN’e ait ödeme sayfası için next_action.url adresini açması gerekir.
Müşteriyi next_action.url adresine yönlendirin.
processing
Bankanın yanıtı bekleniyor. Bu durum, oluşturma yanıtında doğrudan da dönebilir.
Webhook'u bekleyin ve yeni bir ödeme oluşturmayın. Banka hiç yanıt vermezse ödeme mutabakata kadar processing durumunda kalabilir ve sonra failed ya da expired olarak biter.
succeeded
Tutar tahsil edildi.
Siparişi teslim edin.
failed
Ödeme tamamlanmadı. Nedeni failure alanında yer alır.
failure.message değerini gösterin. Yeniden denemek için yeni bir merchant_order_id ile yeni bir ödeme oluşturun.
expired
Müşteri ödemeyi expires_at zamanından önce tamamlamadı.
Müşteri hâlâ ödeme yapmak istiyorsa yeni bir ödeme oluşturun.
cancelled
Ödeme iptal edildi.
Siparişi teslim etmeyin.
Senaryo
Durum
Olay
3-D Secure sonrasında onaylandı
requires_action → succeeded
payment.succeeded
Reddedildi
failed
payment.failed
Banka yanıtı bekleniyor
processing
Nihai sonuca kadar gönderilmez
Bankadan yanıt alınamadı; deneme zaman aşımına uğradı
processing → failed / expired
payment.failed
Müşteri ödemeye başlamadı ve süre doldu (müşteri ödemeyi yarıda bırakırsa ödeme failed durumuyla sonuçlanabilir)
expired
payment.failed
Burada listelenmeyen her durumu processing olarak değerlendirin ve ödemeyi yeniden sorgulayın.
Başarısızlık kodları
failed durumundaki bir ödeme failure.code ve failure.message alanlarını içerir. Kodu iş mantığınızda kullanın, mesajı ise müşteriye düz metin olarak gösterin.
Kod
Anlam
payment_declined
Banka ödemeyi reddetti.
do_not_honor
Banka ödemeyi gerekçe belirtmeden reddetti.
insufficient_funds
Kart bakiyesi yetersiz.
card_limit_reached
Kart limitlerinden birine ulaşıldı.
card_details_invalid
Kart bilgileri geçersiz.
card_expiry_invalid
Son kullanma tarihi geçersiz veya kartın süresi dolmuş.
card_security_code_invalid
CVV geçersiz.
card_not_active
Kart bloke edilmiş veya etkin değil.
card_brand_not_supported
Bu kart markası desteklenmiyor.
three_ds_failed
3-D Secure doğrulaması başarısız oldu veya tamamlanmadı.
payment_not_permitted
Kartın bu tür ödemelerde kullanılmasına izin verilmiyor.
amount_not_allowed
Bu kart için bu tutara izin verilmiyor.
currency_not_supported
Bu para birimi bu kart için desteklenmiyor.
payment_details_rejected
Banka ödeme bilgilerini reddetti.
duplicate_payment
Banka ödemeyi mükerrer işlem olarak tespit etti.
payment_timed_out
Ödeme öngörülen süre içinde tamamlanmadı.
bank_unavailable
Bankaya ulaşılamadı.
bank_requested_retry
Banka, işlemin daha sonra yeniden denenmesini talep etti.
service_unavailable
Ödeme şu anda işlenemedi.
Yeni kodlar eklenebilir; bilinmeyen kodları payment_declined olarak değerlendirin. failure.message bankanın kendi metnini içerebilir; aksi takdirde webhook’larda İngilizcedir, GET çağrılarında ise Accept-Language başlığına göre belirlenir.
Dönüş URL’si
Müşteri geri döndüğünde tarayıcı, yalnızca ödeme succeeded durumundaysa success_url adresine, diğer tüm durumlarda fail_url adresine yönlendirilir. URL’ye imzalı parametreler eklenir. Doğru sayfayı göstermek için bu parametreleri doğrulayın; siparişi ise yalnızca webhook veya bir GET çağrısı succeeded durumunu teyit ettikten sonra teslim edin.
Direct API: ödeyen 3-D Secure'dan döndüğünde sonuç henüz nihai değilse tarayıcı fail_url adresine yönlendirilmez; ödeyen tarafsız bir Payniko sonuç sayfasında kalır. Sonuç için webhook'a veya GET'e güvenin.
Alan
Açıklama
payment_id
Ödeme ID’si .
merchant_order_id
Sipariş referansınız. İmzalı değildir: yalnızca siparişi bulmak için kullanın; eşleştirmeyi payment_id üzerinden yapın.
status
Yönlendirme anındaki ödeme durumu, GET ile aynı değerlerle. success_url üzerinde her zaman succeeded. fail_url üzerinde genellikle failed, expired veya cancelled; Hosted API'de ödeyen ödeme nihai duruma ulaşmadan ayrıldıysa (örneğin "Mağazaya dön" ile) requires_action veya processing olabilir.
failure_code
Yalnızca status değeri failed olduğunda yer alır; failure.code ile aynıdır.
ts
Dönüş bağlantısının oluşturulduğu Unix zamanı. Müşteri bağlantıyı daha sonra açabileceğinden eski bir ts değeri tek başına sahtecilik göstergesi değildir.
key_version
Bağlantıyı imzalayan anahtarı belirtir: ilgili ortamda oluşturulan ilk anahtar için 1, sonraki anahtar için 2 ve bu şekilde devam eder. Bir ödeme için bu değer hiçbir zaman değişmez.
sig
64 karakterlik, küçük harfli onaltılık (hex) HMAC-SHA256 değeri.
İmzalanacak dize
ts + "." + payment_id + "." + status + "." + failure_code // failure_code empty: keep the last dot
key_version ile belirtilen anahtarın gizli anahtarını seçin. Anahtarlar sonradan değiştirilmiş olsa bile bu, ödemeyi oluşturan anahtardır.
HMAC-SHA256 değerini hesaplayın ve sig ile sabit zamanlı olarak karşılaştırın. payment_id değerinin size ait ödemelerden birine karşılık geldiğini doğrulayın.
Siparişi teslim etmeden önce ödemeyi GET /api/payments/{payment_id} ile sorgulayın (veya webhook’u bekleyin).
Kendi sorgu parametreleriniz korunur. URL’de yukarıdaki adlardan birine sahip bir parametre zaten varsa bu parametre değiştirilir.
Bağlantı geçici bir hata nedeniyle imzalanamadıysa URL bu parametrelerin hiçbirini içermez. Bu durumda sonucu bilinmiyor olarak kabul edin ve ödemeyi sorgulayın.
// Node.js 20+: check the signed parameters on success_url / fail_url.
import {createHmac, timingSafeEqual} from 'node:crypto';
// query: URLSearchParams of the return URL.
// secretFor(keyVersion): the API secret of that key, or undefined if you do not know it.
export function verifyReturn(query, secretFor) {
const sig = query.get('sig') ?? '';
const secret = secretFor(query.get('key_version'));
if (!secret || !/^[0-9a-f]{64}$/.test(sig)) return false;
const signed = [query.get('ts') ?? '', query.get('payment_id') ?? '',
query.get('status') ?? '', query.get('failure_code') ?? ''].join('.');
const expected = createHmac('sha256', secret).update(signed, 'utf8').digest();
return timingSafeEqual(expected, Buffer.from(sig, 'hex'));
}
// Even a valid return is not proof of payment: read GET /api/payments/{payment_id} before you deliver.
<?php
// PHP 8.1+: check the signed parameters on success_url / fail_url.
// $secretFor(string $keyVersion): the API secret of that key, or null if you do not know it.
function payniko_verify_return(array $query, callable $secretFor): bool
{
$sig = (string) ($query['sig'] ?? '');
$secret = $secretFor((string) ($query['key_version'] ?? ''));
if (!$secret || !preg_match('/^[0-9a-f]{64}$/', $sig)) return false;
$signed = implode('.', [(string) ($query['ts'] ?? ''), (string) ($query['payment_id'] ?? ''),
(string) ($query['status'] ?? ''), (string) ($query['failure_code'] ?? '')]);
return hash_equals(hash_hmac('sha256', $signed, $secret), $sig);
}
// $ok = payniko_verify_return($_GET, fn (string $v) => $secretsByKeyVersion[$v] ?? null);
// Even a valid return is not proof of payment: read GET /api/payments/{payment_id} before you deliver.
# Python 3.9+: check the signed parameters on success_url / fail_url.
import hashlib, hmac, re
def verify_return(query, secret_for) -> bool:
# query: dict-like (for example Flask request.args); secret_for(key_version) -> secret or None
sig = query.get("sig", "")
secret = secret_for(query.get("key_version"))
if not secret or not re.fullmatch(r"[0-9a-f]{64}", sig):
return False
signed = ".".join([query.get("ts", ""), query.get("payment_id", ""),
query.get("status", ""), query.get("failure_code", "")])
expected = hmac.new(secret.encode(), signed.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)
# Even a valid return is not proof of payment: read GET /api/payments/{payment_id} before you deliver.
Dönüş URL’leri · Örnek
https://merchant.example/payment/success?payment_id=LUVEI-PAYN-654321&merchant_order_id=ORDER-2026-001&status=succeeded&ts=1793001234&key_version=1&sig=200e8a5d4a7a03ead362a2c076408f9d8764f8f636f58bd4c17d78a50d59eb8a
string to sign: 1793001234.LUVEI-PAYN-654321.succeeded.
https://merchant.example/payment/failure?payment_id=LUVEI-PAYN-654321&merchant_order_id=ORDER-2026-001&status=failed&failure_code=insufficient_funds&ts=1793001234&key_version=1&sig=1cd0a104ec4ec4f5b2933d48d14c0eb33d949dd7f27936a067ab8f68b413b70d
string to sign: 1793001234.LUVEI-PAYN-654321.failed.insufficient_funds
secret used in this example (fake): example_secret_not_real
GET/api/payments/{payment_id}
Ödeme sorgulama
Güncel durumu içeren { "data": Payment } nesnesini döndürür. Sorgulama ödemede hiçbir değişiklik yapmadığından bu uç noktayı gerektiği sıklıkta çağırabilirsiniz: müşteri geri döndükten sonra, bir webhook tutarsız göründüğünde veya zaman aşımından sonra.
Parametreler
Alan
Tür
Zorunlu
Açıklama
payment_id
string · path
Zorunlu
Ödeme oluşturulurken döndürülen payment_id. Aynı ortama ait her anahtarla bu ödeme sorgulanabilir.
./payniko.sh GET '/api/payments/3c2d7a4e-5b1f-4e8a-9d06-7f2b1c9e4a35'
Her webhook’u doğrulayın: ödemeyi oluşturan anahtarın gizli anahtarıyla hesaplanan X-Timestamp + "." + raw body dizesinin HMAC-SHA256 değeri X-Signature ile aynı olmalı ve X-Timestamp 300 saniye içinde olmalıdır. Her event_id değerini bir kez kaydedin ve vakit kaybetmeden 2xx koduyla yanıt verin. Kod örnekleri ve yeniden deneme davranışı için bkz. Webhook’lar.
Hatalar
Kod
HTTP
Anlam
Önerilen işlem
1000001
422
İstek geçersiz: bilinmeyen bir alan, geçersiz bir biçim, POST isteğinde sorgu dizesi, desteklenmeyen bir sorgu parametresi veya JSON nesnesi olmayan bir istek gövdesi. İlgili alanlar errors içinde listelenir.
Listelenen alanları düzeltin.
1000009
422
Idempotency-Key eksik veya geçersiz.
Her POST isteğinde A-Z a-z 0-9 _ - karakterlerinden oluşan, 8–128 karakter uzunluğunda bir anahtar gönderin.
0055502
401 / 403
Erişim reddedildi. 401: X-Api-Key veya X-Merchant-Code eksik, bir başlık yinelenmiş veya hatalı biçimlendirilmiş, anahtar tanınmıyor veya iptal edilmiş, üye işyeri kodu eşleşmiyor ya da X-Environment gönderilmiş ancak anahtarla uyuşmuyor. 403: hesap veya Direct API erişimi etkin değil.
Başlıkları ve API anahtarını doğrulayın. İsteği otomatik olarak yeniden denemeyin.
0055503
409
Direct API çakışması: bu merchant_order_id için zaten bir ödeme mevcut, Idempotency-Key farklı bir istek gövdesiyle ya da bir Hosted ödeme için kullanılmış veya ilk istek hâlâ işleniyor.
Ödemeyi GET ile sorgulayın. Yalnızca birebir aynı isteği yeniden gönderin.
0055210
422
BIN altı haneli değil.
Kart numarasının ilk altı hanesini gönderin.
0055234
409
Kart bilgileri geçersiz: kart numarası, son kullanma tarihi veya CVV. metadata.developer_detail hangi alanın geçersiz olduğunu belirtir: card_number_invalid, card_expiry_invalid veya card_cvv_invalid.
Müşteriden kart bilgilerini yeniden girmesini isteyin.
0055233
409
BIN tanınmıyor.
Müşteriden farklı bir kart isteyin. Bu bir banka reddi değildir.
0055230
409
Bu kart bu ödeme için kullanılamaz.
Müşteriden farklı bir kart isteyin. Bu bir banka reddi değildir.
0055231
409
Bu kart bu ödemede kabul edilmiyor.
Müşteriden farklı bir kart isteyin.
0055221
409
Bu kartın limitlerinden birine ulaşıldı.
Farklı bir kart kullanın veya daha sonra yeniden deneyin.
0055235
422
Tutar kabul edilmiyor (Direct API ve BIN sorgusu). Bilgi mevcut olduğunda amount_limit alanı min ve max değerlerini içerir.
Tutarı güncelleyin.
0055001
409
Hesabınız bu ödeme için hazır değil. Sandbox anahtarıyla bu aynı zamanda sandbox simülasyonunun geçici olarak kullanılamadığı anlamına gelir (developer_detail: sandbox_unavailable), 9000013 ile eşdeğerdir.
Destek ekibiyle iletişime geçin.
1000002
404
Bulunamadı: yol bilinmiyor ya da bu API ürünü platformda etkin değil. Hesabınız için kapatılmış bir API bunun yerine 403 döndürür (Hosted API'de 9000040, Direct API'de 0055502).
Yolu doğrulayın; API'nin kullanılabilir olması gerekiyorsa destekle iletişime geçin.
1000011
503
Hizmet geçici olarak kullanılamıyor.
Aynı isteği yeniden deneyin. Yeni kaynak oluşturmadan önce mevcut kaynakları sorgulayın.
Direct API hataları ayrıca metadata.retry_class alanını içerir (no_retry, retry_same_request, retry_after, read_order). Tarayıcı penceresinin kapanması veya sizin tarafınızda oluşan bir zaman aşımı, ödemenin başarısız olduğu anlamına gelmez; ödemeyi sorgulayın. Kodların tam listesi için bkz. Hata kodları.
Sandbox kartlarıyla test
Sandbox anahtarı kullanıldığında BIN sorgusu ve ödeme sabit test verileriyle çalışır: 435509 ve 400000 birer direct BIN, 540061 ise bir hosted BIN’dir. İleri tarihli herhangi bir son kullanma tarihi ve 3 haneli herhangi bir CVV kullanın. Ayrıntılar için bkz. Sandbox ve test kartları.
Kart numarası
Sonuç
4000000000000010
3-D Secure olmadan onaylanır. Direct API’de oluşturma yanıtında durum doğrudan succeeded olarak döner.
4000000000000002 · 4355090000044242
Test 3-D Secure adımının ardından succeeded durumu.
4000000000000051 · 4355090000040000
3-D Secure öncesinde reddedilir (do_not_honor). Hosted API: ödeme yaklaşık 5 dakika requires_action durumunda kalabilir ve failed olmadan önce payment.updated gönderebilir. Direct API: oluşturma yanıtı doğrudan failed döner.
4000000000000093 · 4355090000005151
Test 3-D Secure adımının ardından insufficient_funds nedeniyle failed durumu.
4000000000000044
Bankadan yanıt alınmaz ve 3-D Secure uygulanmaz; ödeme processing durumunda kalır.
4355090000089999
Hosted API: 3-D Secure adımı olmadan processing. Direct API: test 3-D Secure adımı, ardından processing.
Direct API'de reddedilen bir test kartı oluşturma yanıtında doğrudan failed döner; ara requires_action yalnızca hosted BIN'in ödeme sayfasında görülür. Kararları webhook'a veya GET'e dayandırın.
Uçtan uca örnek
BIN sorgusu, ödeme oluşturma, 3-D Secure yönlendirmesi, dönüş URL’si ve webhook adımlarını kapsayan asgari bir Node.js sunucusu. Örnek, istek yardımcı fonksiyonunu ve Webhook’lar ile Dönüş URL’si bölümlerindeki doğrulama fonksiyonlarını kullanır.
server.mjs
// server.mjs: Direct API from start to end (Node.js 20+, no dependencies).
import http from 'node:http';
import {randomUUID} from 'node:crypto';
import {api} from './payniko.mjs'; // request helper (Authentication)
import {verifyWebhook} from './verify-webhook.mjs'; // webhook check (Webhooks)
import {verifyReturn} from './verify-return.mjs'; // return URL check (Return URL)
const site = 'https://merchant.example';
const orders = new Map(); // use your database
const seenEvents = new Set(); // use your database
const readBody = async (req) => { const parts = []; for await (const p of req) parts.push(p); return Buffer.concat(parts); };
const reply = (res, data) => {
res.writeHead(200, {'Content-Type': 'application/json'});
res.end(JSON.stringify(data.status === 'requires_action' && data.next_action
? {redirect: data.next_action.url} : {status: data.status, failure: data.failure}));
};
http.createServer(async (req, res) => {
const url = new URL(req.url, site);
// 1. BIN check, 2. create the payment. Your own HTTPS form posts the card here.
if (req.method === 'POST' && url.pathname === '/pay') {
const form = JSON.parse((await readBody(req)).toString('utf8'));
const bin6 = form.number.slice(0, 6);
let mode = null;
try {
mode = (await api('GET', `/api/payments/bin/${bin6}?amount=100.00¤cy=USD`)).data.payment_mode;
} catch (error) {
if (error.status === undefined || error.status >= 500 || error.status === 429) throw error;
}
if (mode !== 'direct' && mode !== 'hosted') { res.writeHead(422).end('Please use another card.'); return; }
const orderId = 'ORDER-' + Date.now();
const key = randomUUID();
orders.set(orderId, {key, status: 'new'});
const body = {
merchant_order_id: orderId, amount: '100.00', currency: 'USD',
customer: {email: form.email, phone: form.phone},
webhook_url: `${site}/webhooks/payniko`,
success_url: `${site}/payment/success`, fail_url: `${site}/payment/failure`,
};
if (mode === 'direct') {
body.card = {number: form.number, holder_name: form.holder_name,
expiry_month: form.expiry_month, expiry_year: form.expiry_year, cvv: form.cvv};
} else {
body.bin6 = bin6; // payer enters the card on the payment page
}
const {data} = await api('POST', '/api/payments/direct', body, key);
Object.assign(orders.get(orderId), {paymentId: data.payment_id, status: data.status});
reply(res, data); // 3. browser goes to next_action.url
return;
}
// Return URL: show a page, but trust only the webhook or a GET.
if (req.method === 'GET' && (url.pathname === '/payment/success' || url.pathname === '/payment/failure')) {
const signed = verifyReturn(url.searchParams, () => process.env.API_SECRET);
let status = 'unknown';
if (signed) status = (await api('GET', `/api/payments/${url.searchParams.get('payment_id')}`)).data.status;
res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
res.end(status === 'succeeded' ? 'Thank you, payment received.' : 'Payment not completed.');
return;
}
// Webhook: verify, store event_id once, answer 2xx fast.
if (req.method === 'POST' && url.pathname === '/webhooks/payniko') {
const raw = await readBody(req);
if (!verifyWebhook(raw, req.headers, process.env.API_SECRET)) { res.writeHead(401).end(); return; }
const event = JSON.parse(raw.toString('utf8'));
if (!seenEvents.has(event.event_id)) { // use a unique index in your database
seenEvents.add(event.event_id);
const order = orders.get(event.data.merchant_order_id);
if (order && event.event_type.startsWith('payment.')) order.status = event.data.status;
}
res.writeHead(204).end();
return;
}
res.writeHead(404).end();
}).listen(8080);