WinPay API — Entegrasyon Kılavuzu
Bu belge, WinPay ödeme yöntemini kendi sitesine bağlayacak geliştiriciler içindir. Yalnızca API sözleşmesini anlatır: üç uç, imza, sonuç bildirimi, durumlar ve hatalar.
Sürüm: v1 · Taban adres: https://api.win-pay.co · English version
1. Genel bakış
WinPay bir ödeme yöntemidir: sizin adınıza tahsilat ve ödeme yapar, sonucu imzalı bir bildirimle (callback) size iletir. Üyenizin bakiyesini siz yönetirsiniz.
| Ne | Metot ve yol |
|---|---|
| Yatırım talebi aç | POST /api/v1/deposits/init |
| Çekim talebi aç | POST /api/v1/withdrawals/init |
| Sonuç bildirimi | POST — sizin HTTPS adresinize |
Yatırım: siteniz talep açar → yanıttaki payment_url adresine üyeyi yönlendirir → üye ödeme
sayfasında tutarı seçer, IBAN ve QR ile transferi yapar → ödeme doğrulanınca size callback gelir →
siteniz bakiyeyi artırır.
Çekim: siteniz talep açar (üyenin IBAN'ı ile) → transfer yapılır → size callback gelir → siteniz çekimi tamamlanmış işaretler.
Önemli:
201yanıtı "para geldi/gitti" demek değildir, "talep oluşturuldu" demektir. Bakiyeyi yalnızca callback geldiğinde değiştirin.
İşlem durumu sorgulayan bir uç yoktur. Sonucu callback ile alırsınız; kaybolduğunu
düşünüyorsanız aynı request_id ile talebi tekrar gönderin — mevcut işlem döner (bölüm 8).
2. Erişim bilgileri
Size verilecekler:
| Değer | Ne için |
|---|---|
| Taban adres | https://api.win-pay.co |
api_key |
Sitenizi tanıtır — X-API-Key başlığında gönderilir |
api_secret |
İsteklerinizi imzalamak için |
callback_secret |
Gelen bildirimi doğrulamak için (farklı bir değerdir) |
Sizin vermeniz gerekenler:
| Değer | Not |
|---|---|
| Callback URL'iniz | HTTPS zorunlu, yönlendirme yapmamalı. Örn. https://api.sitem.com/odeme/callback |
| Sunucunuzun çıkış IP'si | IP izin listesine eklenir. Birden fazlaysa hepsini bildirin |
Güvenlik kuralları
api_keyveapi_secretyalnızca sunucunuzda durur. HTML'e, mobil uygulama paketine, tarayıcıya konmaz.- Üç sır birbirinden farklıdır ve birbirinin yerine kullanılmaz.
- Sunucunuzun saati doğru olmalıdır (NTP) — imza zaman penceresi kullanır.
3. Kimlik doğrulama ve imza
Her istekte üç katman vardır:
| Katman | Nasıl |
|---|---|
| Site kimliği | X-API-Key başlığı |
| IP kontrolü | Çıkış IP'niz izin listesinde olmalı |
| İstek imzası | X-WinPay-Signature — HMAC-SHA256 |
İmza nasıl hesaplanır
imzalanan_metin = <unix_saniye> + "." + <istek gövdesinin ham baytları>
imza = HMAC_SHA256(imzalanan_metin, api_secret) → 64 haneli küçük harf hex
Gönderilecek başlıklar:
X-API-Key: <api_key>
X-WinPay-Timestamp: 1789305600
X-WinPay-Signature: 3f2a9c...64 hane...b1
Content-Type: application/json
Zaman penceresi ±300 saniyedir. Saatiniz 5 dakikadan fazla kayarsa istekler reddedilir.
⚠️ En sık yapılan hata: gövdeyi bir kez oluşturup aynı string'i hem imzalayın hem gönderin. JSON'u ayrıştırıp yeniden üretirseniz anahtar sırası değişebilir ve imza tutmaz. İmza gönderdiğiniz baytların üzerinden doğrulanır.
4. Yatırım
4.1 Talep
POST /api/v1/deposits/init
{
"request_id": "dep-20260910-000001",
"member_id": "uye-42",
"member_name": "Ali Veli",
"amount": "1000.00",
"is_fast": false
}
| Alan | Tür | Zorunlu | Kural |
|---|---|---|---|
request_id |
string | ✅ | 1–100 karakter; harf, rakam, . _ : -. Her talepte benzersiz |
member_id |
string | ✅ | Boş olmayan metin, en fazla 100. Sayısal ID'yi de string gönderin |
member_name |
string | — | En fazla 150 karakter |
amount |
string | ✅ | Pozitif, noktalı ondalık, en fazla 2 basamak: "1000.00" |
is_fast |
boolean | — | FAST transfer talebi |
Kabul edilmeyen tutarlar: "1.000,00", -100, 0, "1.001", 1e3, "abc" → 400.
Banka hesabını ve IBAN'ı siz seçmezsiniz; sistem tutara uygun hesabı belirler. Siteniz gövdeden değil, API anahtarından tanınır.
4.2 Yanıt — 201
{
"success": true,
"data": {
"transaction_id": 201,
"payment_url": "https://api.win-pay.co/pay/<64-hane-token>",
"amount": "1000.00",
"status": "pending_payment",
"expires_at": "2026-09-10T17:00:00.000Z"
}
}
| Alan | Tür | Not |
|---|---|---|
transaction_id |
integer | WinPay işlem numarası — saklayın |
payment_url |
string | Üyeyi buraya yönlendirin |
amount |
string | Kabul edilen tutar |
status |
string | Oluşturulduğunda her zaman pending_payment |
expires_at |
string | ISO-8601 UTC, 20 dakika sonrası |
idempotent |
boolean | Yalnızca aynı request_id ile tekrarda gelir |
4.3 Üyeyi yönlendirin
- Tokeni siz üretmeyin, yanıttan geleni kullanın.
- Üst seviye yönlendirme yapın (iframe içinde çalışacağını varsaymayın).
- Linki analytics, chat, referrer ve loglarda paylaşmayın — linki bilen sayfayı görür.
Süre: 20 dakika. Bu süre içinde onaylanmayan talep
expiredolur.
Tutar değişebilir. Üye ödeme sayfasında sizin gönderdiğinizden farklı bir tutar seçebilir. Bakiyeye yazacağınız tutar callback'teki
approved_amountolmalıdır — ilkamountdeğil.
5. Çekim
POST /api/v1/withdrawals/init
{
"request_id": "wd-20260910-000001",
"member_id": "uye-42",
"member_name": "Ali Veli",
"amount": "500.00",
"iban": "TR330006100519786457841326",
"account_name": "ALI VELI"
}
Yatırımla aynı alanlar, is_fast yok, iki ek zorunlu alan var:
| Alan | Tür | Zorunlu | Kural |
|---|---|---|---|
iban |
string | ✅ | TR IBAN, 26 karakter, mod-97 kontrolü yapılır |
account_name |
string | ✅ | Boş olmayan metin, en fazla 150 |
Yanıt — 201:
{
"success": true,
"data": {
"transaction_id": 202,
"status": "pending_assignment",
"amount": "500.00"
}
}
Çekimde payment_url dönmez. Sonucu callback ile alırsınız.
IBAN doğrulaması biçimi kontrol eder, hesabın kime ait olduğunu kanıtlamaz. Hesabın üyeye ait olduğunu doğrulamak ve çekilebilir bakiyeyi bloke etmek/iade etmek sizin tarafınızdadır.
6. Sonuç bildirimi (callback)
Entegrasyonun en kritik kısmı. Para burada kesinleşir.
6.1 Gelen istek
POST https://api.sitem.com/odeme/callback
Content-Type: application/json
X-WinPay-Signature: <64 haneli hex HMAC-SHA256>
X-WinPay-Event-Id: winpay:901
{
"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"
}
| Alan | Tür | Not |
|---|---|---|
event_id |
string | winpay:<n> — mükerrer kontrol anahtarınız |
event |
string | transaction.approved / .rejected / .completed / .expired |
transaction_id |
integer | Talep yanıtındaki numara |
request_id |
string | Sizin gönderdiğiniz request_id |
type |
string | deposit veya withdrawal |
amount |
string | İlk talep edilen tutar |
approved_amount |
string | Gerçekten hareket eden tutar — bunu kullanın |
status |
string | Bölüm 6.3'teki tablo |
member_id |
string | Gönderdiğiniz üye kimliği |
timestamp |
string | ISO-8601 UTC, bildirimin hazırlandığı an |
Callback imzasında zaman damgası öneki yoktur (isteklerinizden farklı) ve
api_secretdeğilcallback_secretkullanılır:HMAC_SHA256(ham gövde, callback_secret).
6.2 Doğrulama — sırayla yapın
- İmzayı ham gövde baytları üzerinden doğrulayın. JSON'u parse edip yeniden stringify
etmeyin. Karşılaştırmayı sabit süreli yapın (
crypto.timingSafeEqual,hash_equals,hmac.compare_digest). - Gövdedeki
event_idileX-WinPay-Event-Idbaşlığı aynı olmalı. request_idsizin açtığınız, o üyeye ait bir talep olmalı.event_iddeğerini UNIQUE indeksli bir kolona yazın. Zaten varsa2xxdönüp durun, ikinci bir mali hareket oluşturmayın.- Bakiye değişikliğini aynı veritabanı transaction'ında uygulayın.
- Commit ettikten sonra
2xxdönün. Gövde okunmaz.
6.3 Hangi durumda ne yapmalı
type |
status |
Yapılacak |
|---|---|---|
deposit |
approved |
Üyeye approved_amount kadar bakiye yaz. Kesin |
deposit |
rejected |
Bakiye değişmez |
deposit |
expired |
Bakiye değişmez |
withdrawal |
completed |
Çekimi tamamlanmış işaretle. İkinci bir ödeme emri değildir. Kesin |
withdrawal |
rejected |
Bloke ettiğiniz tutarı üyeye iade edin |
6.4 Teslimat davranışı
| Konu | Değer |
|---|---|
| Zaman aşımı | 10 saniye |
| Yönlendirme (redirect) | Takip edilmez |
| Okunan en büyük yanıt | 64 KB |
| Başarısız olursa | Artan aralıklarla tekrar denenir, en fazla 1 saat arayla |
| Tekrarlarda | event_id ve gövde bayt bayt aynı kalır |
| Protokol | Yalnız HTTPS |
Tekrarlar saatler sonra gelebilir. Sırf zaman geçti diye geçerli bir bildirimi atmayın; mükerrer korumasını kalıcı
event_idkaydıyla yapın, zaman penceresiyle değil.
7. Durum kodları
yatırım: pending_payment → pending_approval → approved
→ rejected
→ expired (20 dk)
çekim: pending_assignment → assigned → processing → completed
→ rejected
| Durum | Anlamı |
|---|---|
pending_payment |
Talep açıldı, üye henüz ödeme bildirmedi |
pending_approval |
Üye "ödedim" dedi — para geldi demek değildir |
approved |
Ödeme doğrulandı. Kesin, geri dönüşü yok |
pending_assignment |
Çekim talebi alındı |
assigned / processing |
İşleme alındı |
completed |
Transfer yapıldı. Kesin |
rejected / expired |
Para hareketi olmadı |
Yalnızca
approved(yatırım) vecompleted(çekim) başarı demektir. Yatırım hiçbir zamancompletedolmaz.
8. Tekrar eden istekler
Tekrar anahtarı: site + işlem türü + request_id
| Durum | Sonuç |
|---|---|
Aynı request_id, aynı gövde |
Mevcut işlem döner, data içinde "idempotent": true. Yeni işlem oluşmaz |
Aynı request_id, farklı gövde |
409 CONFLICT |
request_id ile Idempotency-Key başlığı farklı |
400 |
Zaman aşımı aldıysanız yeni bir request_id üretmeyin; aynı request_id ile tekrar
gönderin — sorgu gibi davranır ve mevcut işlemi döner.
Idempotency-Key başlığını da kullanabilirsiniz; gövdedeki request_id ile aynı olmalıdır.
Tekrar yanıtında
expires_atbulunmayabilir. Yatırım ve çekim referansları ayrı sayılır, yine dedep-/wd-gibi ayırt edici önekler kullanın.
9. Hata kodları
Tüm hatalar aynı biçimdedir. code ile dallanın, message ile değil — mesajlar Türkçe,
insan içindir ve değişebilir.
{ "success": false, "code": "BAD_REQUEST", "message": "Açıklama" }
| HTTP | code |
Anlamı | Yapılacak |
|---|---|---|---|
| 400 | BAD_REQUEST |
Biçim, tutar, IBAN, referans veya limit hatası | Veriyi düzeltin. Kör tekrar etmeyin |
| 400 | INVALID_JSON |
Gövde geçerli JSON değil | Serileştiriciyi düzeltin |
| 401 | UNAUTHORIZED |
API anahtarı geçersiz / imza tutmuyor / zaman damgası pencere dışı | Kimlik ve saat kontrolü |
| 403 | FORBIDDEN |
IP izinli değil, kara liste veya site kapalı | IP listesini bizimle kontrol edin |
| 404 | NOT_FOUND |
Yanlış adres | Yolu doğrulayın |
| 409 | CONFLICT |
Aynı request_id, farklı gövde |
Mevcut işlemi inceleyin, yeni hareket açmayın |
| 429 | TX_RATE_LIMIT |
init uçları — dakikada 600, API anahtarı başına | Üstel geri çekilme |
| 429 | RATE_LIMIT |
Diğer /api/ yolları — dakikada 200, IP başına |
Üstel geri çekilme |
| 503 | SERVICE_UNAVAILABLE |
Tutara uygun hesap yok veya bakım | Üyeye gösterin; sürerse bize bildirin |
| 500 | INTERNAL_ERROR |
Beklenmeyen hata | Aynı request_id ile bir kez tekrar |
Sınırlı uçlarda RateLimit-* yanıt başlıkları gönderilir.
Gerçek hata yanıtları
| Gönderilen | Yanıt |
|---|---|
"amount": "1.000,00" |
400 · Tutar: ondalık ayırıcı nokta, en fazla iki kuruş hanesi kullanın |
"amount": "100.001" |
400 · aynı mesaj |
member_id yok |
400 · member_id gerekli |
| Kontrol basamağı hatalı IBAN | 400 · IBAN kontrol basamakları hatalı |
| Sitenizin limitini aşan tutar | 400 · Tutar sitenin yatırım limitleri dışında |
| Yanlış imza | 401 · Istek imzasi dogrulanamadi |
| İmza başlıkları eksik | 401 · X-WinPay-Signature basligi gerekli (64 haneli hex HMAC-SHA256) |
| Saat 300 sn'den fazla kaymış | 401 · Istek zaman damgasi 300 saniyelik pencerenin disinda; sunucu saatini kontrol edin |
| IP izinli değil | 403 · Bu IP site API erişimine izinli değil |
| Üye veya IBAN kara listede | 403 · İşlem kabul edilmedi |
| Tutara uygun hesap yok | 503 · Bu tutar için uygun IBAN yok; tutarı veya zamanı değiştirin |
10. İstek sınırları
| Uç | Sınır | Anahtar |
|---|---|---|
/deposits/init, /withdrawals/init |
600 istek / dakika | API anahtarı başına — diğer siteler sizi etkilemez |
Diğer /api/ yolları |
200 istek / dakika | IP başına |
Aşarsanız 429 alırsınız; üstel geri çekilme (exponential backoff) uygulayın.
11. Örnek istemci kodu
Node.js
const crypto = require('crypto');
async function winpayCall(path, payload) {
const body = JSON.stringify(payload); // BİR KEZ üret
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
});
return { status: res.status, data: await res.json() };
}
// Callback doğrulama (Express — ham gövde şart)
app.post('/odeme/callback',
express.raw({ type: 'application/json' }),
async (req, res) => {
const expected = crypto
.createHmac('sha256', process.env.WINPAY_CALLBACK_SECRET)
.update(req.body) // Buffer, parse edilmemiş
.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);
}
const event = JSON.parse(req.body.toString('utf8'));
if (event.event_id !== req.get('X-WinPay-Event-Id')) return res.sendStatus(400);
await db.transaction(async (t) => {
const [, created] = await t.findOrCreate({ where: { event_id: event.event_id } });
if (!created) return; // zaten işlenmiş
if (event.type === 'deposit' && event.status === 'approved') {
await creditMember(t, event.member_id, event.approved_amount);
}
});
res.sendStatus(200);
});
PHP
<?php
function winpay_call(string $path, array $payload): array {
$body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$timestamp = (string) time();
$signature = hash_hmac('sha256', $timestamp . '.' . $body, getenv('WINPAY_API_SECRET'));
$ch = curl_init(getenv('WINPAY_BASE_URL') . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-API-Key: ' . getenv('WINPAY_API_KEY'),
'X-WinPay-Timestamp: ' . $timestamp,
'X-WinPay-Signature: ' . $signature,
],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ['status' => $status, 'data' => json_decode($response, true)];
}
// Callback doğrulama
$raw = file_get_contents('php://input'); // HAM gövde
$expected = hash_hmac('sha256', $raw, getenv('WINPAY_CALLBACK_SECRET'));
$got = $_SERVER['HTTP_X_WINPAY_SIGNATURE'] ?? '';
if (!hash_equals($expected, $got)) { http_response_code(401); exit; }
$event = json_decode($raw, true);
Python
import hmac, hashlib, json, time, os, requests
def winpay_call(path, payload):
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
timestamp = str(int(time.time()))
signature = hmac.new(
os.environ["WINPAY_API_SECRET"].encode(),
f"{timestamp}.".encode() + body.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return requests.post(
os.environ["WINPAY_BASE_URL"] + path,
data=body.encode("utf-8"),
headers={
"Content-Type": "application/json",
"X-API-Key": os.environ["WINPAY_API_KEY"],
"X-WinPay-Timestamp": timestamp,
"X-WinPay-Signature": signature,
},
timeout=20,
)
# Callback doğrulama
def verify(raw_body: bytes, header_signature: str) -> bool:
expected = hmac.new(
os.environ["WINPAY_CALLBACK_SECRET"].encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header_signature)
12. Uygulamanızı çevrimdışı doğrulayın
Aşağıdakiler yalnızca test değerleridir, üretimde kullanılmaz. Gerçek erişim bilgileri verilmeden önce HMAC kodunuzu bunlarla sınayabilirsiniz.
api_key = wp_test_key_0001
api_secret = test_api_secret_do_not_use_in_production
callback_secret = test_callback_secret_do_not_use_in_production
İstek imzası — bu zaman damgası ve gövde tam olarak bu imzayı üretmelidir:
timestamp = 1789041098
body = {"request_id":"dep-20260910-000001","member_id":"member-42","member_name":"Ali Veli","amount":"1000.00","is_fast":false}
signature = e55ffec5d4ea23e78d86048bbd2d778349dd69efa0ea8c5961857d4ee1bba4fe
Callback doğrulaması — bu gövde tam olarak bu imzayı üretmelidir:
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"}
signature = 9b2182c5a8155721a85bb246497c9d6a3d70c2b2c431bae1ab0fafdb23bf9aa7
Gerçek erişim bilgileriyle duman testi:
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"
Aynı request_id ile iki kez çalıştırın: ikinci çağrı aynı transaction_id ile
"idempotent": true dönmelidir.
13. Canlıya çıkmadan önce kontrol listesi
Kimlik ve imza
- Yanlış
api_key→ 401 - İmzasız istek → 401
- Bozuk imza → 401
- Saati 10 dakika ileri alıp istek → 401
- İzin listesi dışı bir IP'den istek → 403
Talep
- Geçerli yatırım → 201 +
payment_url - Geçerli çekim → 201
-
amountolarak-100,0,1.001,"1.000,00",abc→ hepsi 400 - Geçersiz IBAN → 400
Tekrar
- Aynı
request_id+ aynı gövde → tek işlem,idempotent: true - Aynı
request_id+ farklı tutar → 409
Callback
- İmzayı ham gövde ile doğruluyorsunuz
- Bozuk imzalı bildirimi reddediyorsunuz
- Aynı
event_idiki kez gelince ikinci kez bakiye yazmıyorsunuz -
deposit/approvediçinapproved_amountkullanıyorsunuz -
withdrawal/completedikinci bir ödeme emri tetiklemiyor - Bakiyeyi yazdıktan sonra 2xx dönüyorsunuz
Uçtan uca
- Yatırım: talep → ödeme sayfası → callback → bakiye
- Farklı tutar seçilen yatırım:
approved_amountdoğru işleniyor - Süresi dolan yatırım:
expiredgeliyor, bakiye değişmiyor - Çekim: talep →
completed→ çekim kapanıyor
14. Sık yapılan hatalar
| Hata | Sonuç | Doğrusu |
|---|---|---|
| Gövdeyi imzaladıktan sonra yeniden oluşturmak | Sürekli 401 | Bir kez üretin, aynısını gönderin |
| Callback'te JSON'u parse edip yeniden stringify etmek | İmza tutmaz | Ham baytlar üzerinden doğrulayın |
amount ile bakiye yazmak |
Yanlış tutar | approved_amount kullanın |
201 gelince bakiye yazmak |
Para gelmeden kredi | Yalnız callback'te yazın |
Aynı event_id için tekrar bakiye yazmak |
Çift kredi | event_id benzersiz kaydı tutun |
Zaman aşımında yeni request_id ile tekrar |
Çift talep | Aynı request_id ile tekrar |
withdrawal/completed görünce tekrar ödeme yapmak |
Çift ödeme | Bu bir bildirimdir, emir değil |
| API anahtarını tarayıcıya koymak | Anahtar sızar | Yalnız sunucuda |
| Sunucu saatini NTP'siz bırakmak | Rastgele 401 | NTP kurun |
request_id tekrar kullanmak |
409 | Her talepte benzersiz üretin |
Destek
Sorun bildirirken şunları hazırlayın: request_id, isteğin tam zamanı (saat dilimiyle), aldığınız
HTTP kodu ve code alanı, sunucunuzun çıkış IP'si.
Sır göndermeyin — api_key, api_secret veya callback_secret değerlerini mesaja, ekran
görüntüsüne veya loga yazmayın.