WinPay Entegrasyon BelgeleriEnglish →

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ü:

  1. Bozuk imzalı callback'i reddediyorsunuz.
  2. Aynı event_id ikinci kez gelince bakiye bir daha yazılmıyor.
  3. approved_amount kullanıyorsunuz, amount değil.
  4. 201 gelince 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.