Production API
PayFerrari Merchant API
Bahis, casino ve e-ticaret siteleri için yatırım (deposit) ve oyuncu çekimi (withdraw) entegrasyonu. Public/secret key kimlik doğrulama, HMAC-SHA256 imza, IP kısıtlaması, imzalı webhook'lar ve iş yeri paneli.
Kimlik doğrulama
Her iş yerinin iki anahtarı vardır. İkisi de iş yeri paneli → API anahtarları sayfasındadır.
pk_live_8fK2…
İş yerinizi tanımlar. Her istekte X-Public-Key başlığında gönderilir. Gizli değildir ve yenilenmez.
sk_live_Qe71…
İstekleri ve webhook'ları imzalar. Yalnızca sunucunuzda, ortam değişkeninde tutun. Tarayıcıya, mobil uygulamaya veya repoya koymayın.
Sandbox iş yerlerinde anahtarlar pk_test_ / sk_test_ önekiyle başlar ve tüm yanıtlarda "sandbox": true döner.
Başlıklar
| Başlık | Zorunlu | Açıklama |
|---|---|---|
X-Public-Key | Tüm istekler | Public key (pk_live_…) |
X-Signature | İmzalı uç noktalar | Base64(HMAC-SHA256(secret_key, imza_dizgisi)) |
X-PayFerrari-Timestamp | Önerilir | İsteğin Unix zaman damgası (saniye). İmzaya dahil edilir ve tekrar saldırılarını önler. |
Content-Type | POST | application/json |
X-Public-Key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx
X-PayFerrari-Timestamp: 1760000000
X-Signature: 3q0b7cF0m6l3…Vt8=
Content-Type: application/json
X-Merchant-Key, X-PayFerrari-Signature başlıkları ve gövdedeki merchant_key / payferrari_token alanları geriye dönük uyumluluk için kabul edilmeye devam eder. Yeni entegrasyonlarda X-Public-Key ve X-Signature kullanın.
Panelde IP kısıtlaması tanımlıysa yalnızca listedeki IPv4/IPv6 adreslerinden ve CIDR bloklarından gelen istekler kabul edilir; diğerleri 403 ip_denied alır.
İstek imzalama
Önerilen (zaman damgalı): İsteğe X-PayFerrari-Timestamp başlığını ekleyin ve imza dizgisini v2 etiketi, zaman damgası ve her alan yeni satır (\n) ile ayrılmış olacak şekilde oluşturun. Ayraç kullanımı dizgi belirsizliğini ortadan kaldırır; zaman damgası imzaya bağlandığı için yakalanan bir istek tekrar oynatılamaz.
message = "v2\n" + timestamp + "\n" + alan1 + "\n" + alan2 + …
signature = Base64( HMAC-SHA256( secret_key, message ) )
| Uç nokta | İmza alanları (sırayla) |
|---|---|
POST/v1/payments/init | public_key, merchant_oid, payment_amount, email, currency, merchant_ok_url, merchant_fail_url |
POST/v1/payments/status | public_key, merchant_oid |
POST/v1/wallets/lookup | public_key, wallet_id |
POST/v1/payouts | public_key, merchant_oid, payment_amount, wallet_id, wallet_name |
payment_amountkuruş cinsinden tam sayıdır ve dizgiye ondalıksız yazılır (50000).emailküçük harfe çevrilmiş ve baştaki/sondaki boşlukları kırpılmış haliyle imzalanır;currencybüyük harfle (TRY).- Cüzdan doğrulamada
wallet_idyalnızca rakamlarına indirgenmiş haliyle imzalanır ("97 15 29"→971529). - Diğer alanlar gövdede gönderdiğiniz değerle birebir aynı olmalıdır.
| Kontrol | Davranış |
|---|---|
| Zaman penceresi | Zaman damgası sunucu saatinden en fazla ±5 dakika sapabilir; aksi halde 401 timestamp. |
| Tekrar koruması | Her imza yalnızca bir kez kabul edilir; aynı imza tekrar gelirse 409 replay. |
| Durum sorgulama | Aynı işlemi tekrar sorgularken her istekte güncel bir zaman damgası gönderin; böylece imza her seferinde değişir. |
X-PayFerrari-Timestamp göndermeyen mevcut entegrasyonlar için imza, alanların ayraç olmadan art arda eklenmesiyle oluşan dizginin HMAC-SHA256 Base64 halidir (HMAC(secret, public_key + alan1 + alan2 + …)) ve geriye dönük uyumluluk için kabul edilmeye devam eder. Yeni oluşturulan iş yerleri zaman damgalı imzayı zorunlu tutar.
Anahtar yenileme
Secret key'in sızdığından şüpheleniyorsanız veya periyodik olarak değiştirmek istiyorsanız panelde API anahtarları → Secret key'i yenile adımını kullanın. İşlem hesap şifrenizle onaylanır; public key değişmez.
| Geçiş süresi | Davranış |
|---|---|
| Hemen | Eski secret ile imzalanan istekler anında 401 hash alır. |
| 1 saat · 24 saat · 72 saat | Süre boyunca hem eski hem yeni secret ile imzalanan istekler kabul edilir. Süre dolmadan panelden “Şimdi geçersiz kıl” diyebilirsiniz. |
hash değerini iki anahtarla da doğrulayın.
Bağlantı testi
GET/v1/ping
Public key'i ve IP kısıtlamasını doğrular. İmza gerekmez; askıdaki iş yerleri için de yanıt döner.
curl https://payferrari.com/v1/ping \
-H "X-Public-Key: $PF_PUBLIC_KEY"
{
"status": "success",
"service": "payferrari",
"merchant": "Demo Mağaza",
"sandbox": false,
"time": "2026-10-12T12:00:00.000Z"
}
Yatırım başlatma
POST/v1/payments/init
Oyuncu bakiyesine yükleme için tek kullanımlık ödeme oturumu açar. Oyuncuyu yanıttaki payment_url adresine yönlendirin. Oturum 15 dakika geçerlidir (sandbox: 10 dakika).
Gövde
| Alan | Tip | Açıklama |
|---|---|---|
merchant_oid zorunlu | string ≤ 64 | Sizin sipariş numaranız; iş yeri içinde benzersiz. |
payment_amount zorunlu | integer | Kuruş cinsinden tutar (500,00 TL → 50000). |
email zorunlu | string | Oyuncunun e-posta adresi. |
currency | string | Yalnızca TRY (varsayılan). |
merchant_ok_url zorunlu | url | Başarılı ödeme sonrası dönüş adresi. |
merchant_fail_url zorunlu | url | Başarısız / iptal dönüş adresi. |
merchant_notify_url zorunlu | https url | Webhook adresi; herkese açık HTTPS olmalı. |
user_basket | array | [{ "name", "price", "qty" }]; price kuruş cinsinden birim fiyattır. Ödeme sayfasında kalem listesi olarak gösterilir. |
{
"merchant_oid": "DEP-10001",
"payment_amount": 50000,
"email": "oyuncu@site.com",
"currency": "TRY",
"merchant_ok_url": "https://siteniz.com/deposit/ok",
"merchant_fail_url": "https://siteniz.com/deposit/fail",
"merchant_notify_url": "https://siteniz.com/api/payferrari/notify",
"user_basket": [{ "name": "Bakiye yükleme", "price": 50000, "qty": 1 }]
}
Yanıt
{
"status": "success",
"token": "W53_qCE6mXZ3WH2aWOqt1bzhF_4CeqfG",
"payment_url": "https://payferrari.com/pay/W53_qCE6mXZ3WH2aWOqt1bzhF_4CeqfG",
"expires_at": "2026-10-12T12:15:00.000Z",
"sandbox": false
}
Siparişi yalnızca webhook status=success geldiğinde ve hash doğrulandıktan sonra “ödendi” işaretleyin. Dönüş URL'leri yalnızca kullanıcı deneyimi içindir.
İşlem durumu
POST/v1/payments/status
Yatırım veya çekim durumunu sorgular. Webhook gecikirse yedek kontrol olarak kullanın.
{ "merchant_oid": "DEP-10001" }
Yanıt
{
"status": "success",
"type": "deposit",
"sandbox": false,
"payment": {
"merchant_oid": "DEP-10001",
"status": "paid",
"amount_kurus": 50000,
"currency": "TRY",
"paid_at": "2026-10-12T12:05:00.000Z",
"notify_status": "delivered",
"created_at": "2026-10-12T12:00:00.000Z",
"expires_at": "2026-10-12T12:15:00.000Z"
}
}
payment.status | Anlam |
|---|---|
pending | Oturum açık, ödeme bekleniyor |
paid | Ödeme tamamlandı |
expired | Oturum süresi doldu |
refunded | İş yeri tarafından iade edildi |
Çekimlerde type = withdraw ve payment.status = paid döner. Kayıt yoksa 404 not_found.
Cüzdan doğrulama
POST/v1/wallets/lookup
Çekimden önce üye numarası ile ad soyadın eşleştiğini kontrol edin.
{
"wallet_id": "971529",
"wallet_name": "Ege Yılmaz"
}
{ "status": "success", "wallet_no": "971529", "match": true, "sandbox": false }
match: ad gönderilmezse null; gönderilirse büyük/küçük harf ve Türkçe karakter farkları yok sayılarak karşılaştırılır.
Oyuncu çekimi
POST/v1/payouts
İş yeri bakiyenizden oyuncunun PayFerrari cüzdanına anında aktarım yapar. Ad soyad eşleşmesi zorunludur.
| Alan | Tip | Açıklama |
|---|---|---|
merchant_oid zorunlu | string ≤ 64 | Çekim referansınız; benzersiz. |
payment_amount zorunlu | integer | Kuruş cinsinden tutar. |
wallet_id zorunlu | string | Oyuncunun PayFerrari üye numarası. |
wallet_name zorunlu | string | Cüzdan sahibinin ad soyadı. |
merchant_notify_url | https url | Boşsa paneldeki bildirim adresi kullanılır. |
{
"merchant_oid": "WD-90001",
"payment_amount": 25000,
"wallet_id": "971529",
"wallet_name": "Ege Yılmaz",
"merchant_notify_url": "https://siteniz.com/api/payferrari/notify"
}
{ "status": "success", "merchant_oid": "WD-90001", "wallet_no": "971529", "sandbox": false }
Aynı cüzdana bir saat içinde ikinci çekim gönderilemez (429 rate_limit). Bakiye yetersizse 400 balance döner.
Webhook'lar
İşlem sonuçlandığında PayFerrari, merchant_notify_url adresine POST application/json gönderir. Her teslimat X-PayFerrari-Signature ve X-PayFerrari-Timestamp başlıklarını taşır; imza, ham gövdenin zaman damgasına bağlı HMAC-SHA256 Base64 halidir. Yanıtınız HTTP 2xx olmalı ve gövdesinde OK geçmelidir.
X-PayFerrari-Timestamp: 1760000000
X-PayFerrari-Signature: Base64( HMAC-SHA256( secret_key, timestamp + "." + ham_gövde ) )
X-PayFerrari-Event: deposit
{
"merchant_oid": "DEP-10001",
"status": "success",
"payment_amount": 50000,
"currency": "TRY",
"hash": "Base64(HMAC-SHA256(secret_key, merchant_oid + status + payment_amount))",
"type": "deposit"
}
- Doğrulamayı
X-PayFerrari-Signaturebaşlığıyla yapın (önerilen); gövdedekihashalanı geriye dönük uyumluluk içindir. - Zaman damgası ±5 dakikadan eski ise bildirimi reddedin (tekrar koruması).
type: yatırımdadeposit, oyuncu çekimindewithdraw.metadata(isteğe bağlı): panelden oluşturduğunuz ödeme bağlantısına eklediğiniz ek bilgiler varsa anahtar/değer nesnesi olarak aynen iletilir.- Zaman aşımı 2,5 saniyedir. Başarısız teslimatlar üstel artan aralıklarla (en fazla 5 dakikada bir) yeniden denenir.
- Aynı bildirim birden fazla gelebilir;
merchant_oidbazında idempotent işleyin. - Teslimat kayıtlarını, test gönderimini ve manuel yeniden denemeyi panelde Webhook'lar sayfasında bulabilirsiniz.
import crypto from "node:crypto";
import express from "express";
const SECRET_KEY = process.env.PF_SECRET_KEY;
const app = express();
// İmza ham gövde üzerinden doğrulanır: express.raw kullanın
app.post("/api/payferrari/notify", express.raw({ type: "*/*" }), (req, res) => {
const ts = req.get("X-PayFerrari-Timestamp") || "";
const sig = req.get("X-PayFerrari-Signature") || "";
const raw = req.body; // Buffer
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.status(401).send("timestamp");
const expected = crypto.createHmac("sha256", SECRET_KEY)
.update(`${ts}.${raw}`).digest("base64");
const a = Buffer.from(sig), b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.status(401).send("sign");
const { merchant_oid, status } = JSON.parse(raw.toString("utf8"));
// merchant_oid için idempotent: daha önce işlendiyse yalnızca OK dönün
res.type("text/plain").send("OK");
});
<?php
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_PAYFERRARI_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_PAYFERRARI_SIGNATURE'] ?? '';
$expected = base64_encode(hash_hmac('sha256', $ts . '.' . $raw, getenv('PF_SECRET_KEY'), true));
if (abs(time() - (int) $ts) > 300 || !hash_equals($expected, (string) $sig)) {
http_response_code(401);
exit('sign');
}
$p = json_decode($raw, true) ?: [];
// merchant_oid için idempotent işleyin
echo 'OK';
import base64, hashlib, hmac, os, time
from flask import Flask, abort, request
SECRET_KEY = os.environ["PF_SECRET_KEY"].encode()
app = Flask(__name__)
@app.post("/api/payferrari/notify")
def notify():
raw = request.get_data()
ts = request.headers.get("X-PayFerrari-Timestamp", "")
sig = request.headers.get("X-PayFerrari-Signature", "")
expected = base64.b64encode(hmac.new(SECRET_KEY, f"{ts}.".encode() + raw, hashlib.sha256).digest()).decode()
if abs(time.time() - int(ts or 0)) > 300 or not hmac.compare_digest(expected, sig):
abort(401)
p = request.get_json(force=True)
# merchant_oid için idempotent işleyin
return "OK"
Tutarlar ve limitler
- Tüm tutarlar kuruş cinsindendir: 500,00 TL →
50000. Para birimiTRY. - Minimum yatırım ve minimum çekim tutarları iş yerinize özeldir (panel → Güvenlik ve ayarlar → Hesap bilgileri).
merchant_oidiş yeri içinde benzersizdir; tekrar gönderimde409 duplicate_oiddöner.- IP başına dakikalık istek limitleri: init 120, status 180, lookup 60, payouts 80. Aşımda
429 rate_limit.
Hata kodları
Hatalı isteklerde gövde { "status": "failed", "reason": "…" } biçimindedir; bazı hatalarda açıklayıcı bir message alanı da bulunur.
| HTTP | reason | Anlam |
|---|---|---|
| 401 | merchant | Public key eksik veya geçersiz |
| 401 | merchant_status | İş yeri askıya alınmış |
| 401 | hash | İmza hatalı veya eski secret'ın geçiş süresi dolmuş |
| 401 | timestamp | Zaman damgası eksik, hatalı veya ±5 dakika penceresinin dışında |
| 409 | replay | Aynı imza daha önce kullanılmış (tekrar oynatma) |
| 403 | ip_denied | İstek IP'si kısıtlama listesinde değil |
| 400 | params | Eksik veya hatalı alan |
| 400 | notify_url | Bildirim adresi herkese açık HTTPS değil |
| 400 | currency | Desteklenmeyen para birimi |
| 400 | min_deposit · min_withdraw | Tutar iş yeri limitinin altında |
| 400 | balance | İş yeri bakiyesi çekim için yetersiz |
| 400 | name_mismatch | Ad soyad cüzdan sahibiyle eşleşmedi |
| 404 | wallet | Cüzdan bulunamadı |
| 404 | not_found | İşlem bulunamadı |
| 409 | duplicate_oid | merchant_oid daha önce kullanılmış |
| 429 | rate_limit | İstek limiti aşıldı / aynı cüzdana kısa sürede çekim |
Örnek kod
Anahtarları PF_PUBLIC_KEY ve PF_SECRET_KEY ortam değişkenlerinde saklayın. Örnek, yatırım oturumu açar.
import crypto from "node:crypto";
const PUBLIC_KEY = process.env.PF_PUBLIC_KEY; // pk_live_…
const SECRET_KEY = process.env.PF_SECRET_KEY; // sk_live_… yalnızca sunucuda
const sign = (ts, ...fields) =>
crypto.createHmac("sha256", SECRET_KEY).update(["v2", ts, ...fields].join("\n")).digest("base64");
const order = {
merchant_oid: `DEP-${Date.now()}`,
payment_amount: 50000, // 500,00 TL
email: "oyuncu@site.com",
currency: "TRY",
merchant_ok_url: "https://siteniz.com/deposit/ok",
merchant_fail_url: "https://siteniz.com/deposit/fail",
merchant_notify_url: "https://siteniz.com/api/payferrari/notify",
};
const ts = Math.floor(Date.now() / 1000);
const res = await fetch("https://payferrari.com/v1/payments/init", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Public-Key": PUBLIC_KEY,
"X-PayFerrari-Timestamp": String(ts),
"X-Signature": sign(ts, PUBLIC_KEY, order.merchant_oid, order.payment_amount, order.email,
order.currency, order.merchant_ok_url, order.merchant_fail_url),
},
body: JSON.stringify(order),
});
const data = await res.json();
if (data.status !== "success") throw new Error(data.reason);
// Oyuncuyu data.payment_url adresine yönlendirin
<?php
$publicKey = getenv('PF_PUBLIC_KEY');
$secretKey = getenv('PF_SECRET_KEY');
$order = [
'merchant_oid' => 'DEP-' . time(),
'payment_amount' => 50000,
'email' => 'oyuncu@site.com',
'currency' => 'TRY',
'merchant_ok_url' => 'https://siteniz.com/deposit/ok',
'merchant_fail_url' => 'https://siteniz.com/deposit/fail',
'merchant_notify_url' => 'https://siteniz.com/api/payferrari/notify',
];
$ts = time();
$raw = "v2\n" . $ts . "\n" . implode("\n", [
$publicKey, $order['merchant_oid'], $order['payment_amount'], $order['email'],
$order['currency'], $order['merchant_ok_url'], $order['merchant_fail_url'],
]);
$signature = base64_encode(hash_hmac('sha256', $raw, $secretKey, true));
$ch = curl_init('https://payferrari.com/v1/payments/init');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-Public-Key: ' . $publicKey,
'X-PayFerrari-Timestamp: ' . $ts,
'X-Signature: ' . $signature,
],
CURLOPT_POSTFIELDS => json_encode($order),
]);
$data = json_decode(curl_exec($ch), true);
// header('Location: ' . $data['payment_url']);
import base64, hashlib, hmac, json, os, time, urllib.request
PUBLIC_KEY = os.environ["PF_PUBLIC_KEY"]
SECRET_KEY = os.environ["PF_SECRET_KEY"]
def sign(ts, *fields):
raw = "\n".join(["v2", str(ts), *[str(f) for f in fields]]).encode()
return base64.b64encode(hmac.new(SECRET_KEY.encode(), raw, hashlib.sha256).digest()).decode()
order = {
"merchant_oid": f"DEP-{int(time.time())}",
"payment_amount": 50000,
"email": "oyuncu@site.com",
"currency": "TRY",
"merchant_ok_url": "https://siteniz.com/deposit/ok",
"merchant_fail_url": "https://siteniz.com/deposit/fail",
"merchant_notify_url": "https://siteniz.com/api/payferrari/notify",
}
ts = int(time.time())
signature = sign(ts, PUBLIC_KEY, order["merchant_oid"], order["payment_amount"], order["email"],
order["currency"], order["merchant_ok_url"], order["merchant_fail_url"])
req = urllib.request.Request(
"https://payferrari.com/v1/payments/init",
data=json.dumps(order).encode(), method="POST",
headers={"Content-Type": "application/json", "X-Public-Key": PUBLIC_KEY,
"X-PayFerrari-Timestamp": str(ts), "X-Signature": signature},
)
data = json.loads(urllib.request.urlopen(req).read())
# Oyuncuyu data["payment_url"] adresine yönlendirin
OID="DEP-$(date +%s)"
AMOUNT=50000
EMAIL="oyuncu@site.com"
OK="https://siteniz.com/deposit/ok"
FAIL="https://siteniz.com/deposit/fail"
TS=$(date +%s)
SIG=$(printf 'v2\n%s\n%s\n%s\n%s\n%s\nTRY\n%s\n%s' "$TS" "$PF_PUBLIC_KEY" "$OID" "$AMOUNT" "$EMAIL" "$OK" "$FAIL" \
| openssl dgst -sha256 -hmac "$PF_SECRET_KEY" -binary | base64)
curl https://payferrari.com/v1/payments/init \
-H "Content-Type: application/json" \
-H "X-Public-Key: $PF_PUBLIC_KEY" \
-H "X-PayFerrari-Timestamp: $TS" \
-H "X-Signature: $SIG" \
-d "{\"merchant_oid\":\"$OID\",\"payment_amount\":$AMOUNT,\"email\":\"$EMAIL\",\"currency\":\"TRY\",
\"merchant_ok_url\":\"$OK\",\"merchant_fail_url\":\"$FAIL\",
\"merchant_notify_url\":\"https://siteniz.com/api/payferrari/notify\"}"
Canlıya geçiş
- Panelde API anahtarları sayfasından public key'i kopyalayın; secret key'i şifrenizle görüntüleyip sunucu ortam değişkenine kaydedin.
GET /v1/pingile public key'i ve IP kısıtlamasını doğrulayın.- Bildirim adresinizi HTTPS olarak kaydedin ve panelden test webhook'u gönderin.
- Üretim sunucusu IP'lerinizi panelde IP kısıtlamasına ekleyin.
- Yatırım: init → yönlendirme → webhook
hashdoğrulama → bakiye yükleme (merchant_ok_urlyalnızca kullanıcı deneyimi içindir). - Çekim: lookup (
match=true) → payouts → webhook → site bakiyesinden düşme. merchant_oiddeğerlerini kendi tarafınızda benzersiz ve idempotent saklayın.- Secret key'i düzenli olarak geçiş süreli yenileyin.
Destek: iş yeri paneli · kurumsal