Webhook'lar
Webhook'lar, bir şey olduğunda sunucunuza haber verir: bir ödeme başarılı oldu, bir iade tamamlandı, bir itiraz açıldı. Sürekli sorgulama (polling) yerine bunları kullanın; müşteri bankanın sayfasından hiç dönmediğinde de gerçeğin kaynağı olarak webhook'lara güvenin.
Uç nokta kaydı
Uç noktaları Geliştiriciler → Webhook'lar altında veya API ile oluşturun. URL herkese açık bir https adresi olmalı; ayrıca kaydettiğiniz bir web sitesinde bulunmalı veya Ayarlar → Web siteleri ve geri dönüş adresleri altında kaydettiğiniz bir geri dönüş (callback) adresiyle eşleşmelidir (aksi halde 400 webhook_url_not_allowed).
{
"url": "https://shop.example.com/webhooks/pf",
"enabled_events": ["payment_intent.succeeded", "payment_intent.payment_failed", "refund.succeeded", "refund.failed"],
"description": "Order system"
}
enabled_events, istediğiniz olay türlerini listeler; tümü için ["*"] kullanın.
{
"id": "we_01J9ZQC8D0F2H4K6M8P1R3T5V7",
"object": "webhook_endpoint",
"url": "https://shop.example.com/webhooks/pf",
"enabled_events": ["payment_intent.succeeded", "payment_intent.payment_failed", "refund.succeeded", "refund.failed"],
"status": "enabled",
"description": "Order system",
"secret": "REPLACE_WITH_THE_SECRET_FROM_THE_RESPONSE",
"created": 1790000200
}
İmzalama gizli anahtarı bu yanıtta yalnızca bir kez döndürülür. Diğer gizli bilgilerinizle birlikte saklayın. Kaybolduysa veya sızdıysa POST /v1/webhook_endpoints/{id}/roll_secret ile yenileyin; yanıt yeni gizli anahtarı taşır ve yalnızca bir kez gösterilir.
Olay
Her teslimat, gövdesi bir JSON (olay) olan bir POST isteğidir:
{
"id": "evt_01J9ZQ4V8K2M3N5P7R9T1W3Y5E",
"object": "event",
"type": "payment_intent.succeeded",
"created": 1790000010,
"merchant_id": "mer_01J9ZPZZ0A1B2C3D4E5F6G7H8J",
"api_version": "2026-10-01",
"livemode": false,
"aggregate_type": "payment_intent",
"aggregate_id": "pi_01J9ZQ4V8K2M3N5P7R9T1W3Y5A",
"sequence": 4,
"request_id": "req_01J9ZQ4V8K2M3N5P7R9T1W3Y5F",
"data": {
"object": {
"id": "pi_01J9ZQ4V8K2M3N5P7R9T1W3Y5A",
"object": "payment_intent",
"amount": 12500,
"currency": "try",
"status": "succeeded",
"capture_method": "automatic",
"created": 1790000000,
"livemode": false
},
"previous_attributes": {"status": "processing"}
}
}
data.object, değişiklikten sonraki API nesnesidir ve API'nin döndürdüğüyle birebir aynıdır; previous_attributes değişen alanları içerir. sequence, aynı nesnenin her değişikliğinde bir artar ve bir nesnenin bir uç noktaya yapılan teslimatları bu sırayla ulaşır. Olaylar kart numarası içermez.
Olay türleri
| Olay | data.object |
|---|---|
payment_intent.created |
PaymentIntent |
payment_intent.requires_action |
PaymentIntent |
payment_intent.processing |
PaymentIntent |
payment_intent.amount_capturable_updated |
PaymentIntent |
payment_intent.succeeded |
PaymentIntent |
payment_intent.payment_failed |
PaymentIntent |
payment_intent.canceled |
PaymentIntent |
payment_intent.fraud_updated |
PaymentIntent |
refund.created |
Refund |
refund.updated |
Refund |
refund.succeeded |
Refund |
refund.failed |
Refund |
dispute.created |
Dispute |
dispute.updated |
Dispute |
dispute.closed |
Dispute |
payout.created |
Payout |
payout.paid |
Payout |
payout.failed |
Payout |
topup.succeeded |
Topup |
balance.available |
Balance |
balance.negative_limit_exceeded |
Balance |
merchant.updated |
Merchant |
topup.updated |
Topup |
customer.created |
Customer |
customer.updated |
Customer |
customer.deleted |
Customer |
Olaylar değiştirilemez ve saklanır; bu nedenle daha sonra GET /v1/events ve GET /v1/events/{id} ile okuyabilir, POST /v1/events/{id}/resend ile bir teslimatı yeniden isteyebilirsiniz.
İmzayı doğrulama
URL'nizi bilen herkes ona istek gönderebilir; bu yüzden her teslimatı doğrulayın. Başlık şöyledir:
PF-Signature: t=1790000011,v1=<64 hexadecimal characters>
t, teslimatın imzalandığı unix zamanıdır; v1 ise t + "." + body dizesinin imzalama gizli anahtarınızla hesaplanan onaltılık (hex) HMAC-SHA256 değeridir. Doğrulamak için:
- Ham istek gövdesini alındığı haliyle okuyun. Önce ayrıştırıp yeniden kodlamayın.
- Başlığı bölün;
tdeğerini ve herv1değerini alın. t, saatinizden 5 dakikadan fazla uzaksa reddedin (tekrar oynatma saldırılarına karşı korur).HMAC_SHA256(secret, t + "." + body)değerini hex olarak hesaplayın ve herv1ile sabit zamanlı (constant time) karşılaştırın. Biri eşleşirse kabul edin.
Gizli anahtar yenileme sırasında birden fazla v1 bulunabilir.
<?php
function verifyPfSignature(string $payload, string $header, string $secret, int $tolerance = 300): bool
{
$t = null;
$signatures = [];
foreach (explode(',', $header) as $part) {
[$key, $value] = array_pad(explode('=', trim($part), 2), 2, '');
if ($key === 't') {
$t = $value;
} elseif ($key === 'v1') {
$signatures[] = $value;
}
}
if ($t === null || !ctype_digit($t) || abs(time() - (int) $t) > $tolerance) {
return false;
}
$expected = hash_hmac('sha256', $t . '.' . $payload, $secret);
foreach ($signatures as $signature) {
if (hash_equals($expected, $signature)) {
return true;
}
}
return false;
}
$payload = file_get_contents('php://input');
$header = $_SERVER['HTTP_PF_SIGNATURE'] ?? '';
if (!verifyPfSignature($payload, $header, getenv('PF_WEBHOOK_SECRET'))) {
http_response_code(400);
exit;
}
$event = json_decode($payload, true);
http_response_code(200);
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyPfSignature(rawBody, header, secret, toleranceSeconds = 300) {
let t = null;
const signatures = [];
for (const part of String(header ?? '').split(',')) {
const [key, value = ''] = part.trim().split('=');
if (key === 't') t = value;
if (key === 'v1') signatures.push(value);
}
if (t === null || !/^\d+$/.test(t)) return false;
if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
return signatures.some((sig) => {
const given = Buffer.from(sig, 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
});
}
// Express: req.body'nin ham Buffer olması için express.raw({ type: 'application/json' }) kullanın.
// app.post('/webhooks/pf', express.raw({ type: 'application/json' }), (req, res) => {
// if (!verifyPfSignature(req.body.toString('utf8'), req.get('PF-Signature'), process.env.PF_WEBHOOK_SECRET)) return res.sendStatus(400);
// const event = JSON.parse(req.body.toString('utf8'));
// res.sendStatus(200);
// });
package webhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"math"
"strconv"
"strings"
"time"
)
// Verify, bir PF-Signature başlığını ham istek gövdesine karşı doğrular.
func Verify(payload []byte, header, secret string, tolerance time.Duration, now time.Time) bool {
var t string
var signatures []string
for _, part := range strings.Split(header, ",") {
key, value, _ := strings.Cut(strings.TrimSpace(part), "=")
switch key {
case "t":
t = value
case "v1":
signatures = append(signatures, value)
}
}
ts, err := strconv.ParseInt(t, 10, 64)
if err != nil || math.Abs(float64(now.Unix()-ts)) > tolerance.Seconds() {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(t + "."))
mac.Write(payload)
expected := mac.Sum(nil)
for _, sig := range signatures {
given, err := hex.DecodeString(sig)
if err == nil && hmac.Equal(given, expected) {
return true
}
}
return false
}
import hashlib
import hmac
import time
def verify_pf_signature(payload: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
t = None
signatures = []
for part in (header or "").split(","):
key, _, value = part.strip().partition("=")
if key == "t":
t = value
elif key == "v1":
signatures.append(value)
if t is None or not t.isdigit() or abs(time.time() - int(t)) > tolerance:
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + payload, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, sig) for sig in signatures)
# Flask: verify_pf_signature(request.get_data(), request.headers.get("PF-Signature", ""), SECRET)
Yanıt verme ve yeniden deneme
- Onaylamak için herhangi bir
2xxdurumuyla yanıt verin. Asıl işi olayı kaydettikten sonra yapın; çünkü platform yanıtınız için yalnızca birkaç saniye (varsayılan 10) bekler. - Zaman aşımı, bağlantı hatası veya başka herhangi bir durum başarısızlık sayılır ve artan aralıklarla yeniden denenir: 1 dakika, 5 dakika, 30 dakika, 2 saat, 6 saat, 12 saat sonra ve ardından üç kez 24 saat sonra; toplamda yaklaşık üç gün. Son denemeden sonra uç nokta
failingolarak işaretlenir ve bekleyen teslimatları durdurulur. Uç noktanızı düzeltin,POST /v1/webhook_endpoints/{id}("disabled": false) ile yeniden etkinleştirin ve kaçırdıklarınızıGET /v1/eventsvePOST /v1/events/{id}/resendile tekrar oynatın. - Teslimat en az bir kez (at least once) yapılır: aynı olay birden fazla kez gelebilir. İşlediğiniz her olayın
iddeğerini saklayarak işleyicinizi idempotent yapın. - Farklı nesneler arasında sıra varsaymayın. Tek bir nesne içinde
sequencedeğerini kullanın veya nesneyi yeniden okuyun. - Her teslimat, tüm denemeleriyle (durum, gecikme, yanıtınızdan maskelenmiş bir alıntı) Geliştiriciler → Webhook'lar altında,
GET /v1/webhook_endpoints/{id}/deliveriesveGET /v1/events/{id}/deliveriesiçinde görünür.
Kontrol listesi
- İmzayı ham gövde üzerinde doğrulayın ve zaman damgasını kontrol edin.
- Hızlıca
2xxdöndürün; işlemeyi asenkron yapın. - Olay
iddeğerine göre tekrarları eleyin. - Sandbox ve canlı için ayrı uç noktalar (ve gizli anahtarlar) kullanın.