Entegrasyon rehberi — adım adım kod
Bu sayfa sıfırdan çalışan bir entegrasyonu 7 adımda kurar. Her adımda kopyalanabilir kod var (Node.js; PHP ve Python karşılıkları API referansı sayfasında). Alanların tam listesi ve hata kodları referansta.
Adım 1 — Erişim bilgilerini alın ve saklayın
WinPay'den üç değer gelir; hepsini sunucu ortam değişkeni olarak saklayın:
WINPAY_BASE_URL=https://api.win-pay.co
WINPAY_API_KEY=... # sizi tanıtır
WINPAY_API_SECRET=... # isteklerinizi imzalar
WINPAY_CALLBACK_SECRET=... # gelen bildirimi doğrular (farklı bir değer)
Siz de bize iki şey verin: callback adresiniz (HTTPS) ve sunucunuzun çıkış IP'si.
Anahtarlar tarayıcıya, mobil uygulamaya, HTML'e, log'a girmez. Sızarsa bize haber verin, yenisini üretiriz.
Adım 2 — Sunucu saatini doğrulayın
İmzada zaman damgası var; saatiniz 5 dakikadan fazla kayarsa her istek 401 alır.
timedatectl | grep synchronized # "yes" olmalı
Adım 3 — İmzalama fonksiyonu
Her istekte üç başlık gider. İmza, gönderdiğiniz baytların üzerinden hesaplanır:
imza = HMAC_SHA256( <unix_saniye> + "." + <gövde string'i>, api_secret ) → 64 hex
const crypto = require('crypto');
async function winpayCall(path, payload) {
const body = JSON.stringify(payload); // gövdeyi BİR KEZ üretin
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = crypto
.createHmac('sha256', process.env.WINPAY_API_SECRET)
.update(timestamp).update('.').update(Buffer.from(body, 'utf8'))
.digest('hex');
const res = await fetch(process.env.WINPAY_BASE_URL + path, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.WINPAY_API_KEY,
'X-WinPay-Timestamp': timestamp,
'X-WinPay-Signature': signature,
},
body, // AYNI string'i gönderin
});
return { status: res.status, data: await res.json() };
}
Kendinizi çevrimdışı test edin. Şu değerlerle fonksiyonunuz şu imzayı üretmeli (bunlar yalnızca test değerleri, üretimde kullanılmaz):
api_secret = test_api_secret_do_not_use_in_production
timestamp = 1789041098
body = {"request_id":"dep-20260910-000001","member_id":"member-42","member_name":"Ali Veli","amount":"1000.00","is_fast":false}
imza = e55ffec5d4ea23e78d86048bbd2d778349dd69efa0ea8c5961857d4ee1bba4fe
Tutmuyorsa: gövdeyi imzaladıktan sonra yeniden mi üretiyorsunuz? Anahtar sırası değişmiş olabilir. Aynı string'i hem imzalayın hem gönderin.
Adım 4 — Callback ucunu yazın (önce bunu, sonra istekleri)
WinPay sonucu bu adrese POST eder. Sıra önemli:
// Express — ham gövde şart, JSON parse ETMEDEN imzalayın
app.post('/odeme/callback',
express.raw({ type: 'application/json' }),
async (req, res) => {
// 1) imza: ham gövde + callback_secret, sabit süreli karşılaştırma
const expected = crypto
.createHmac('sha256', process.env.WINPAY_CALLBACK_SECRET)
.update(req.body)
.digest('hex');
const got = req.get('X-WinPay-Signature') || '';
if (got.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))) {
return res.sendStatus(401);
}
// 2) gövde ve başlık event_id aynı mı
const event = JSON.parse(req.body.toString('utf8'));
if (event.event_id !== req.get('X-WinPay-Event-Id')) return res.sendStatus(400);
// 3) bu request_id sizin mi
const kayit = await db.talepBul(event.request_id);
if (!kayit) return res.sendStatus(400);
// 4-5) mükerrer koruması + bakiye, AYNI transaction içinde
await db.transaction(async (t) => {
const yeni = await t.eventIdKaydet(event.event_id); // UNIQUE kolon
if (!yeni) return; // daha önce işlendi → hiçbir şey yapma
if (event.type === 'deposit' && event.status === 'approved') {
await t.bakiyeArtir(event.member_id, event.approved_amount); // amount DEĞİL
}
if (event.type === 'withdrawal' && event.status === 'completed') {
await t.cekimiKapat(kayit.id);
}
if (event.type === 'withdrawal' && event.status === 'rejected') {
await t.blokeIade(kayit.id);
}
await t.durumYaz(kayit.id, event.status);
});
// 6) bakiye yazıldıktan SONRA 200
res.sendStatus(200);
});
Callback'in gövdesi şöyle gelir (yatırım ve çekim için aynı şekil):
{
"event_id": "winpay:901",
"event": "transaction.approved",
"transaction_id": 201,
"request_id": "dep-20260910-000001",
"type": "deposit",
"amount": "1000.00",
"approved_amount": "950.00",
"status": "approved",
"member_id": "uye-42",
"timestamp": "2026-09-10T16:45:00.000Z"
}
Çevrimdışı test: callback_secret = test_callback_secret_do_not_use_in_production ile şu gövde
şu imzayı vermeli:
body = {"event_id":"winpay:1","event":"transaction.approved","transaction_id":1,"request_id":"dep-20260910-000001","type":"deposit","amount":"1000.00","approved_amount":"950.00","status":"approved","member_id":"member-42","timestamp":"2026-09-10T11:51:38.941Z"}
imza = 9b2182c5a8155721a85bb246497c9d6a3d70c2b2c431bae1ab0fafdb23bf9aa7
Adım 5 — Yatırım akışı
async function yatirimBaslat(uye, tutar) {
const requestId = `dep-${Date.now()}-${uye.id}`; // benzersiz, kaydedin
await db.talepAc({ requestId, uyeId: uye.id, tutar, tur: 'deposit', durum: 'bekliyor' });
const { status, data } = await winpayCall('/api/v1/deposits/init', {
request_id: requestId,
member_id: String(uye.id), // string gönderin
member_name: uye.adSoyad,
amount: tutar.toFixed(2), // "1000.00"
});
if (status === 201) return data.data.payment_url; // üyeyi buraya yönlendirin
if (status === 429) throw new Error('Biraz sonra tekrar deneyin');
if (status === 503) throw new Error('Şu an kullanılamıyor');
throw new Error(`WinPay ${status}: ${data.code}`);
}
Sonra: res.redirect(paymentUrl) — üst seviye yönlendirme, iframe değil. Linki log'a,
analytics'e yazmayın. Bakiyeyi burada değil, callback'te yazın.
Zaman aşımı alırsanız aynı request_id ile tekrar gönderin; yeni işlem açılmaz, mevcut döner
("idempotent": true).
Adım 6 — Çekim akışı
async function cekimBaslat(uye, tutar, iban, hesapSahibi) {
if (uye.bakiye < tutar) throw new Error('Yetersiz bakiye');
const requestId = `wd-${Date.now()}-${uye.id}`;
await db.transaction(async (t) => {
await t.blokeEt(uye.id, tutar); // bakiyeden düş, "bekliyor"
await t.talepAc({ requestId, uyeId: uye.id, tutar, tur: 'withdrawal', durum: 'bekliyor' });
});
const { status, data } = await winpayCall('/api/v1/withdrawals/init', {
request_id: requestId,
member_id: String(uye.id),
member_name: uye.adSoyad,
amount: tutar.toFixed(2),
iban, // TR + 24 hane, boşluksuz
account_name: hesapSahibi,
});
if (status === 201) return data.data.transaction_id; // sonuç callback ile gelecek
await db.blokeIade(uye.id, tutar); // istek kabul edilmediyse iade
throw new Error(`WinPay ${status}: ${data.code}`);
}
201 = talep alındı, para henüz gitmedi. Callback completed gelince çekim biter,
rejected gelince blokeyi iade edersiniz. İkinci bir istek atmayın.
Adım 7 — Test edin, sonra canlıya alın
Test, SSS, sorun giderme sayfasındaki listeyi uygulayın. En kritik dördü:
- Bozuk imzalı callback'i reddediyorsunuz.
- Aynı
event_idikinci kez gelince bakiye bir daha yazılmıyor. approved_amountkullanıyorsunuz,amountdeğil.201gelince değil, callback gelince bakiye yazıyorsunuz.
Gerçek erişim bilgileriyle ilk deneme:
API_SECRET='<api_secret>'; API_KEY='<api_key>'; BASE='https://api.win-pay.co'
BODY='{"request_id":"dep-test-0001","member_id":"member-42","member_name":"Ali Veli","amount":"1000.00"}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$API_SECRET" -r | cut -d' ' -f1)
curl -i -X POST "$BASE/api/v1/deposits/init" \
-H 'Content-Type: application/json' -H "X-API-Key: $API_KEY" \
-H "X-WinPay-Timestamp: $TS" -H "X-WinPay-Signature: $SIG" --data-raw "$BODY"
201 ve içinde payment_url görüyorsanız entegrasyon çalışıyor. Aynı komutu ikinci kez
çalıştırın: aynı transaction_id ve "idempotent": true dönmeli.