Genel bakış
API sanal POS, Shopidi satıcılarının kendi web sitelerinde (WooCommerce, OpenCart, özel yazılım vb.) kartlı ödeme almasını sağlar. Kart bilgileri sizin sitenize hiç uğramaz: alıcı, shopidi.com üzerindeki ortak ödeme sayfasında 3D Secure ile öder. Böylece PCI DSS yükümlülüğünüz en aza iner.
- Temel adres:
https://shopidi.com/api/v1 - Biçim: JSON istek ve yanıt, UTF-8. Tutarlar iki ondalıklı metin (
"1249.90"), tarihler ISO 8601. - Para birimi: Türk lirası (TRY).
- Hız sınırı: anahtar başına dakikada 120 istek.
- Ücretlendirme: her başarılı ödemeden mağazanıza tanımlı API sanal POS komisyonu kesilir; net tutar bloke süresi sonunda bakiyenize geçer ve hakediş talebiyle IBAN'ınıza ödenir. Güncel oranınızı
GET /pingdöndürür.
Başlamak için
- Satıcı hesabı açın ve mağazanızı onaylatın.
- Satıcı panelinde Finans → API sanal POS sayfasından web sitenizi ve iş modelinizi yazıp API Sanal POS Sözleşmesi'ni onaylayarak başvurun.
- Başvurunuz onaylanınca aynı sayfada API anahtarınızı, gizli anahtarınızı (yalnızca bir kez gösterilir) ve webhook imza anahtarınızı görürsünüz.
Ödeme akışı
- Alıcı sitenizde "Öde" der. Sunucunuz
POST /api/v1/paymentsile ödeme oluşturur. - Yanıttaki
payment_urladresine alıcıyı yönlendirirsiniz. - Alıcı ödeme sayfasında bilgilerini girip kartla öder.
- Sonuç
callback_urladresinize imzalı webhook olarak gelir. Siparişi burada onaylayın. - Alıcı
success_urlveyafail_urladresinize döner.
GET /payments/{id} sorgusunu kullanın.Kimlik doğrulama
Her isteğe gizli anahtarınızı Authorization başlığında Bearer olarak ekleyin. Gizli anahtar yalnızca sunucunuzda durmalı; tarayıcıya, mobil uygulamaya veya herkese açık depoya koymayın. Anahtarınız sızdıysa panelden hemen yenileyin, eskisi o anda geçersiz olur.
| Anahtar | Biçim | Kullanım |
|---|---|---|
| API anahtarı | pk_live_… | Hesabınızı tanımlar, destek taleplerinde paylaşabilirsiniz. |
| Gizli anahtar | sk_live_… | API isteklerini yetkilendirir. Gizli tutun. |
| Webhook imza anahtarı | whsec_… | Gelen webhook'ların imzasını doğrulamak için. |
/api/v1/pingAnahtarınızı ve bağlantınızı test eder; komisyon oranı ve bloke süresini döndürür.
curl https://shopidi.com/api/v1/ping -H "Authorization: Bearer sk_live_XXXXXXXXXXXXXXXX"
{
"ok": true,
"merchant": { "name": "Toprak Atölye Ltd", "website": "https://www.siteniz.com" },
"commission_rate": 5,
"hold_days": 7,
"max_amount": 50000
}
Ödeme oluştur
/api/v1/paymentsTek kullanımlık bir ödeme oluşturur ve 201 ile ödeme nesnesini döndürür. Yanıttaki id değerini siparişinizle birlikte saklayın.
| Alan | Tür | Açıklama |
|---|---|---|
amount zorunlu | string / sayı | Tahsil edilecek tutar, TL. En fazla iki ondalık, nokta ile: 1249.90. En az 1 TL; üst sınır /ping yanıtındaki max_amount. |
order_id zorunlu | string | Sizin sipariş numaranız. Harf, rakam, . _ -; en fazla 64 karakter. Hesabınızda benzersiz olmalı (aynı sipariş için iki kez ödeme açılamaz). |
description zorunlu | string | Ödeme sayfasında alıcıya gösterilen açıklama (en fazla 160 karakter). |
success_url zorunlu | url | Başarılı ödemeden sonra alıcının döneceği adres. |
fail_url zorunlu | url | Ödeme başarısız olduğunda veya alıcı vazgeçtiğinde dönülecek adres. |
callback_url | url | Bu ödemenin webhook adresi. Gönderilmezse panelde kayıtlı varsayılan webhook adresi kullanılır. |
currency | string | Yalnızca TRY. |
expires_in | tamsayı | Ödeme sayfasının geçerlilik süresi, dakika. 5–10080, varsayılan 60. |
customer.name / .email / .phone | string | Ödeme formunda önceden doldurulur. Alıcı değiştirebilir. |
metadata | nesne | En fazla 20 anahtar, değerler en fazla 255 karakterlik metin. Size aynen geri döner. |
success_url, fail_url ve callback_url https olmalı ve başvuruda bildirdiğiniz web sitesinin alan adında veya bir alt alan adında bulunmalıdır (ör. siteniz.com için www.siteniz.com, odeme.siteniz.com). Farklı bir alan adı kullanacaksanız destek ekibine yazın.curl -X POST https://shopidi.com/api/v1/payments \
-H "Authorization: Bearer sk_live_XXXXXXXXXXXXXXXX" \
-H "Content-Type: application/json" \
-d '{
"amount": "1249.90",
"order_id": "SIP-100245",
"description": "Sipariş #100245",
"success_url": "https://www.siteniz.com/odeme/basarili",
"fail_url": "https://www.siteniz.com/odeme/hata",
"callback_url": "https://www.siteniz.com/shopidi/webhook",
"expires_in": 60,
"customer": { "name": "Ayşe Kaya", "email": "ayse@ornek.com", "phone": "05551112233" },
"metadata": { "cart_id": "88812" }
}'
<?php
$payload = [
'amount' => '1249.90',
'order_id' => 'SIP-100245',
'description' => 'Sipariş #100245',
'success_url' => 'https://www.siteniz.com/odeme/basarili',
'fail_url' => 'https://www.siteniz.com/odeme/hata',
'customer' => ['name' => 'Ayşe Kaya', 'email' => 'ayse@ornek.com'],
];
$ch = curl_init('https://shopidi.com/api/v1/payments');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('SHOPIDI_SECRET_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status === 201) {
// Ödeme kimliğini siparişinizle eşleştirip saklayın
saveShopidiPaymentId('SIP-100245', $response['id']);
header('Location: ' . $response['payment_url']);
exit;
}
// Hata: $response['error']['code'], $response['error']['message']
const res = await fetch('https://shopidi.com/api/v1/payments', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SHOPIDI_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: '1249.90',
order_id: 'SIP-100245',
description: 'Sipariş #100245',
success_url: 'https://www.siteniz.com/odeme/basarili',
fail_url: 'https://www.siteniz.com/odeme/hata',
}),
});
const payment = await res.json();
if (res.status === 201) {
// Express örneği
return reply.redirect(payment.payment_url);
}
console.error(payment.error.code, payment.error.message);
Ödeme sorgula
/api/v1/payments/{id}/api/v1/payments?order_id={order_id}Ödemenin güncel durumunu döndürür. Webhook gelmediyse veya alıcı sitenize döndüğünde durumu doğrulamak için kullanın.
# Ödeme kimliğiyle
curl https://shopidi.com/api/v1/payments/K7PQ2XAB9M \
-H "Authorization: Bearer sk_live_XXXXXXXXXXXXXXXX"
# Kendi sipariş numaranızla
curl "https://shopidi.com/api/v1/payments?order_id=SIP-100245" \
-H "Authorization: Bearer sk_live_XXXXXXXXXXXXXXXX"
İptal
/api/v1/payments/{id}/cancelHenüz ödenmemiş bir ödemeyi iptal eder; ödeme sayfası artık açılmaz. Örneğin alıcı sepetini değiştirdiğinde eski ödemeyi iptal edip yenisini oluşturun.
curl -X POST https://shopidi.com/api/v1/payments/K7PQ2XAB9M/cancel \
-H "Authorization: Bearer sk_live_XXXXXXXXXXXXXXXX"
İade
/api/v1/payments/{id}/refundsTahsil edilmiş ödemeyi tamamen veya kısmen iade eder. Tutar alıcının kartına bankasına göre birkaç iş günü içinde yansır. İade tutarı mağaza bakiyenizden düşülür; komisyon iade edilen tutar oranında geri verilir.
| Alan | Tür | Açıklama |
|---|---|---|
amount | string / sayı | İade tutarı. Gönderilmezse iade edilebilir tutarın tamamı. |
reason | string | İade nedeni (kayıtlarda tutulur). |
# Tam iade (amount gönderilmezse iade edilebilir tutarın tamamı)
curl -X POST https://shopidi.com/api/v1/payments/K7PQ2XAB9M/refunds \
-H "Authorization: Bearer sk_live_XXXXXXXXXXXXXXXX" \
-H "Content-Type: application/json" \
-d '{ "amount": "250.00", "reason": "Ürün iadesi" }'
Ödeme nesnesi
Tüm uç noktalar ve webhook'lar aynı nesneyi döndürür. transaction son kart denemesini gösterir; commission ve net_amount yalnızca ödenmiş ödemelerde bulunur.
{
"id": "K7PQ2XAB9M",
"object": "payment",
"status": "paid",
"order_id": "SIP-100245",
"amount": "1249.90",
"currency": "TRY",
"description": "Sipariş #100245",
"payment_url": "https://shopidi.com/odeme/K7PQ2XAB9M",
"customer": { "name": "Ayşe Kaya", "email": "ayse@ornek.com" },
"metadata": { "cart_id": "88812" },
"created_at": "2026-10-23T14:02:11+03:00",
"expires_at": "2026-10-23T15:02:11+03:00",
"paid_at": "2026-10-23T14:04:37+03:00",
"refunded_amount": "0.00",
"transaction": {
"number": "LP-2610-48213",
"status": "succeeded",
"card": "VISA **** 4242",
"commission": "62.50",
"net_amount": "1187.40"
}
}
Durumlar
| Durum | Anlamı |
|---|---|
| pending | Ödeme bekleniyor (alıcı henüz ödemedi veya banka onayı sürüyor). |
| paid | Tahsilat başarılı. Siparişi bu durumda onaylayın. |
| failed | Son deneme başarısız. Süre dolmadıysa alıcı aynı bağlantıdan tekrar deneyebilir. |
| expired | expires_at geçti, artık ödenemez. |
| cancelled | API ile iptal edildi. |
| partially_refunded | Tutarın bir kısmı iade edildi. |
| refunded | Tamamı iade edildi. |
Dönüş adresleri
Alıcı ödemeden sonra aşağıdaki parametrelerle sitenize döner:
payment_id: ödeme kimliği (id)order_id: sizin sipariş numaranızstatus:paidveyafailed
Başarısız ödemede alıcı önce ödeme sayfasında "Tekrar dene" seçeneğini görür; vazgeçerse fail_url adresine döner. Ödeme sayfasının süresi dolana kadar aynı payment_url tekrar kullanılabilir.
<?php
// success_url: https://www.siteniz.com/odeme/basarili?payment_id=K7PQ2XAB9M&order_id=SIP-100245&status=paid
// Bu parametrelere güvenmeyin: sonucu sunucunuzdan API ile doğrulayın.
$payment = shopidiGet('/api/v1/payments/' . urlencode($_GET['payment_id']));
if ($payment['status'] === 'paid' && $payment['order_id'] === $_GET['order_id']) {
showThankYou($payment);
} else {
showPaymentPending();
}
Webhook
Ödeme durumu değiştiğinde Shopidi, callback_url adresinize (yoksa paneldeki varsayılan adrese) JSON gövdeli bir POST isteği gönderir. Adresiniz 8 saniye içinde 2xx yanıt vermezse bildirim 1, 5, 15, 30 dakika ve 1, 3, 6, 12 saat sonra yeniden gönderilir.
| Olay | Ne zaman |
|---|---|
payment.succeeded | Tahsilat başarılı oldu. |
payment.failed | Bir ödeme denemesi başarısız oldu (alıcı tekrar deneyebilir). |
payment.refunded | Kısmi veya tam iade yapıldı (refunded_amount güncel tutarı içerir). |
POST /shopidi/webhook HTTP/1.1
Content-Type: application/json
User-Agent: Shopidi-Webhook/1.0
X-Shopidi-Event: payment.succeeded
X-Shopidi-Signature: t=1792753477,v1=5f2c0e8b9d...e41a
{
"id": "evt_3n9x0q1v7c8k2m4p6r5t0w1y",
"type": "payment.succeeded",
"created_at": "2026-10-23T14:04:38+03:00",
"data": { "id": "K7PQ2XAB9M", "object": "payment", "status": "paid", "order_id": "SIP-100245", "amount": "1249.90", "...": "..." }
}
İmza doğrulama
X-Shopidi-Signature başlığı t=zaman_damgası,v1=imza biçimindedir. İmza, "{t}.{ham gövde}" metninin webhook imza anahtarınızla HMAC-SHA256 özetidir (hex). İmzayı mutlaka doğrulayın, karşılaştırmayı sabit zamanlı yapın ve 5 dakikadan eski zaman damgalarını reddedin. Aynı olay birden fazla kez gelebilir; işleminizi tekrar gelse de aynı sonucu verecek şekilde yazın.
<?php
$secret = getenv('SHOPIDI_WEBHOOK_SECRET'); // whsec_...
$body = file_get_contents('php://input'); // ham gövde, json_decode etmeden önce
$header = $_SERVER['HTTP_X_SHOPIDI_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $sig); // ['t' => ..., 'v1' => ...]
$expected = hash_hmac('sha256', ($sig['t'] ?? '') . '.' . $body, $secret);
if (empty($sig['v1']) || ! hash_equals($expected, $sig['v1']) || abs(time() - (int) $sig['t']) > 300) {
http_response_code(400);
exit('invalid signature');
}
$event = json_decode($body, true);
$payment = $event['data'];
// Aynı olay birden çok kez gelebilir: event id veya ödeme durumuyla tekrarı engelleyin
if ($event['type'] === 'payment.succeeded' && $payment['status'] === 'paid') {
markOrderPaid($payment['order_id'], $payment['id'], $payment['amount']);
}
http_response_code(200);
echo 'ok';
import crypto from 'node:crypto';
import express from 'express';
const app = express();
// Ham gövde gerekir: express.json() yerine express.raw()
app.post('/shopidi/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-Shopidi-Signature') || '';
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = crypto
.createHmac('sha256', process.env.SHOPIDI_WEBHOOK_SECRET)
.update(`${parts.t}.${req.body}`)
.digest('hex');
const valid = parts.v1 && parts.v1.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected))
&& Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
if (!valid) return res.status(400).send('invalid signature');
const event = JSON.parse(req.body);
if (event.type === 'payment.succeeded') {
markOrderPaid(event.data.order_id, event.data.id, event.data.amount);
}
res.send('ok');
});
Hatalar
Hatalarda uygun HTTP kodu ve aşağıdaki biçimde gövde döner:
HTTP/1.1 422 Unprocessable Content
{
"error": {
"code": "validation_error",
"message": "Gönderilen veriler geçersiz.",
"fields": {
"success_url": ["Adres https olmalı ve başvurudaki web sitenizin alan adında (siteniz.com) bulunmalı."]
}
}
}
| HTTP | code | Açıklama |
|---|---|---|
| 401 | unauthorized | Authorization başlığı yok veya gizli anahtar hatalı. |
| 403 | forbidden | API erişiminiz askıda, onaysız veya mağaza hesabınız aktif değil. |
| 404 | not_found | Ödeme bulunamadı (yalnızca kendi ödemelerinizi görebilirsiniz). |
| 409 | duplicate_order_id | Bu order_id ile ödeme zaten var. Yanıttaki payment alanında mevcut ödeme döner. |
| 409 | invalid_state | İşlem bu durumda yapılamaz (ör. ödenmiş ödemeyi iptal etmek). |
| 422 | validation_error | Alan hataları fields içinde döner. |
| 422 | refund_failed | İade yapılamadı (ör. tutar iade edilebilir tutarı aşıyor veya bakiye yetersiz). |
| 429 | — | Dakikada 120 istek sınırı aşıldı. Retry-After başlığına göre bekleyin. |
| 503 | service_unavailable | Hizmet geçici olarak kapalı. |
Canlıya geçiş kontrol listesi
- Gizli anahtar yalnızca sunucuda, ortam değişkeninde saklanıyor.
- Siparişler yalnızca imzası doğrulanmış webhook'la veya API sorgusuyla onaylanıyor.
- Webhook işleyiciniz aynı olayın tekrarına dayanıklı ve tutarı siparişle karşılaştırıyor.
- Dönüş adresleriniz başvurudaki alan adında ve https.
- Ödeme sayfasının süresi (
expires_in) sepet sürenize uygun. - İlk canlı ödemeyi küçük bir tutarla deneyip panelden iade ederek uçtan uca kontrol edin.
Sorunuz mu var? Destek ekibine yazın; isteğinize API anahtarınızı (pk_live_…) ve ödeme kimliğini ekleyin. Gizli anahtarınızı kimseyle paylaşmayın.