Webhook
Webhook, kayıtlarınızdan birinde bir şey değiştiğinde gateway sunucusunun sizin sunucunuza gönderdiği imzalı bir POST isteğidir ve değişiklikten kısa süre içinde gelir. Müşteri sekmeyi kapatıp dönüş POST'u hiç gelmese de webhook gelir.
Gövde yalnızca değişen kaydın token'ını taşır; durum, tutar ya da müşteri taşımaz. Bildirimi aldığınızda token ile ilgili retrieve-* ucunu çağırın ve kararı o yanıta göre verin. Yanıttaki reference, amount, currency ve durum alanlarını kendi kaydınızla karşılaştırın.
Adres tanımlama
API isteklerinde webhook adresi gönderilmez. Adresler panelde Ödeme Ayarları → Webhook sayfasında tanımlanır. Her webhook iki bilgiden oluşur:
| Alan | Açıklama |
|---|---|
| URL | Bildirimin gönderileceği adres. https ile başlayan, herkese açık bir adres olmalıdır. Bir adres yalnızca bir kez eklenir. |
| Olaylar | Aşağıdaki tablolardan bu adrese gönderilecek olanlar; en az biri seçilir. Tüm olaylar seçilirse her olay aynı adrese gelir. |
Aynı olay birden fazla adreste seçilebilir; bildirim her adrese ayrı ayrı gönderilir. Bir webhook panelden kapatılabilir; kapalıyken hiçbir olayı için bildirim gönderilmez. Kapatılmadan önce sıraya girmiş bildirimler de gitmez; silinen adres için de aynısı geçerlidir. Hiçbir adreste seçilmemiş olay için bildirim gönderilmez. Olay listesinde yalnızca çalışma alanınızda açık olan modüllerin olayları yer alır.
Olaylar
Sipariş
| Olay | Ne zaman | Doğrulama |
|---|---|---|
order.paid | Siparişin ödemesi başarılı olduğunda. | retrieve-orders: order.status paid. |
order.payment_refunded | Siparişin ödemesinden tamamen ya da kısmen iade yapıldığında. | retrieve-orders: order.transaction.payment_status refunded ya da partially_refunded. |
order.payment_cancelled | Siparişin ödemesi iptal edildiğinde. | retrieve-orders: order.transaction.payment_status cancelled. |
Ödeme linki
| Olay | Ne zaman | Doğrulama |
|---|---|---|
payment_link.paid | Linkten bir ödeme başarıyla alındığında. | retrieve-link-payments (link_payment.token ile): status paid, payment_link.token linkin token'ı, transaction.token bildirimdeki ödeme. |
payment_link.payment_refunded | Linkten alınan bir ödemeden tamamen ya da kısmen iade yapıldığında. | retrieve-link-payments (link_payment.token ile) ya da retrieve-payments (transaction.token ile): transaction.payment_status refunded ya da partially_refunded. |
payment_link.payment_cancelled | Linkten alınan bir ödeme iptal edildiğinde. | retrieve-link-payments (link_payment.token ile) ya da retrieve-payments (transaction.token ile): transaction.payment_status cancelled. |
Abonelik
| Olay | Ne zaman | Doğrulama |
|---|---|---|
subscription.active | Aboneliğin ilk ödemesi ve sonraki her başarılı yenileme ödemesi alındığında. | retrieve-subscriptions: subscription.status active. |
subscription.past_due | Yenileme ödemesi kayıtlı karttan çekilemediğinde. | retrieve-subscriptions: subscription.status past_due. |
subscription.cancelled | Abonelik iptal edildiğinde. Ödenmiş dönem sürüyorsa abonelik dönem sonuna kadar devam eder. | retrieve-subscriptions: subscription.cancelled_at. |
subscription.ended | İptal edilmiş abonelik durduğunda: ödenmiş dönem yoksa iptalle birlikte, varsa dönem bitince. | retrieve-subscriptions: subscription.status cancelled. |
subscription.completed | renewal_limit kadar ödeme alınıp son dönem bittiğinde. | retrieve-subscriptions: subscription.status completed. |
subscription.payment_refunded | Aboneliğin bir ödemesinden tamamen ya da kısmen iade yapıldığında. | retrieve-payments (transaction.token ile): transaction.payment_status, transaction.subscription.token. |
subscription.payment_cancelled | Aboneliğin bir ödemesi iptal edildiğinde. | retrieve-payments: transaction.payment_status cancelled. |
API ödemesi
| Olay | Ne zaman | Doğrulama |
|---|---|---|
transaction.successful | secure-payment ya da regular-payment ile başlatılan ödeme başarılı olduğunda. | retrieve-payments: transaction.status successful. |
transaction.failed | API ödemesi reddedildiğinde ya da hatayla sonuçlandığında. | retrieve-payments: transaction.status failed. |
transaction.expired | secure-payment ile başlatılan ödemenin redirect_url adresi 15 dakika içinde açılmadığında. | retrieve-payments: transaction.status expired. |
transaction.payment_refunded | API ödemesinden tamamen ya da kısmen iade yapıldığında. | retrieve-payments: transaction.payment_status. |
transaction.payment_cancelled | API ödemesi iptal edildiğinde. | retrieve-payments: transaction.payment_status cancelled. |
Sipariş, ödeme linki ya da abonelikte alınan ödemeler için transaction.* olayları gönderilmez; o kaydın kendi olayı gönderilir. Sağlayıcısı yanıt vermeyen (timeout) ödeme için bildirim gönderilmez. İade ve iptal olayları yalnızca iade ya da iptal başarılı olduğunda gönderilir.
İstek
POST /odemehub/webhook HTTP/1.1
Host: magazam.com
Content-Type: application/json
X-Timestamp: 1759396530
X-Signature: 9a3f...e1
{"id":"a41d7e90-3c2f-4f8a-8b6e-9d0c1a2b3e55","event":"order.payment_refunded","created_at":"2026-10-02T09:15:30.000000Z","order":{"token":"3b7e1f20-5c4a-4d8e-9f1b-7a6c5d4e3f21"},"transaction":{"token":"0b8f6c2d-3e41-4a7b-9c5d-1e2f3a4b5c6d"}}
| Başlık | Değer |
|---|---|
Content-Type | application/json |
X-Timestamp | İmzanın atıldığı an, Unix saniye. |
X-Signature | İmza; bkz. İmza doğrulama. |
Gövde
{
"id": "a41d7e90-3c2f-4f8a-8b6e-9d0c1a2b3e55",
"event": "order.payment_refunded",
"created_at": "2026-10-02T09:15:30.000000Z",
"order": { "token": "3b7e1f20-5c4a-4d8e-9f1b-7a6c5d4e3f21" },
"transaction": { "token": "0b8f6c2d-3e41-4a7b-9c5d-1e2f3a4b5c6d" }
}
Ödeme linki olayında gövde linkin ve link ödemesinin token'ını birlikte taşır (payment_link.paid):
{
"id": "c7e2a9b4-1f3d-4e6a-8b5c-2d4f6a8b0c1e",
"event": "payment_link.paid",
"created_at": "2026-10-02T09:15:30.000000Z",
"payment_link": { "token": "5f1a3c7e-9b2d-4e8f-a6c4-1d3e5f7a9b0c" },
"link_payment": { "token": "e8b4d2f0-6a1c-4f3e-9d7b-5c3a1e9f7d2b" },
"transaction": { "token": "0b8f6c2d-3e41-4a7b-9c5d-1e2f3a4b5c6d" }
}
| Alan | Tip | Açıklama |
|---|---|---|
id | string | Bildirimin kimliği (UUID). Aynı bildirim yeniden gönderildiğinde değişmez; tekrar gelen bildirimi ayıklamak için kullanın. |
event | string | Olay adı. |
created_at | string | Bildirimin oluşturulduğu an, ISO 8601 UTC. |
order / payment_link / subscription / transaction | object | Olay adının ilk kısmındaki kayıt: { "token": "…" }. |
link_payment | object | Yalnızca payment_link.* olaylarında: link ödemesinin token'ı, { "token": "…" }. retrieve-link-payments ile sorgulanır. |
transaction | object | Sipariş, ödeme linki ve abonelikte para hareketi olan olaylarda (*.paid, subscription.active, *.payment_refunded, *.payment_cancelled) ödemenin token'ı: { "token": "…" }. |
Gövde yalnızca token taşır; tutar, durum ya da discount taşımaz. Kayıtları kendi sisteminizde create-* yanıtında aldığınız token ile eşleştirin; reference tekil değildir (ayrıntı).
Abonelik ve ödeme linki olaylarında transaction.token ile retrieve-payments çağırdığınızda yanıttaki transaction.subscription.token ya da transaction.payment_link.token ve transaction.link_payment.token, ödemenin bildirimdeki kayda ait olduğunu gösterir. Ödeme linki olaylarında link_payment.token ile retrieve-link-payments çağırarak link ödemesinin kalemlerini, tutarını ve durumunu da alabilirsiniz.
İmza doğrulama
İmza, API istek ve yanıtlarıyla aynı yöntemle alınır:
HMAC-SHA256(api_secret, "{X-Timestamp}\nPOST\n{path}\n{body}")
path webhook adresinizin yoludur (baştaki / dahil, sorgu dizesi hariç; yol yoksa /). body gövdenin ham hâlidir. Zaman damgası 5 dakikadan eski olan bildirimi kabul etmeyin. Gövdeyi ayrıştırıp yeniden yazan bir katman (ör. Express'te express.json()) imzayı bozar; gövdeyi ham okuyun.
Örneklerdeki $client / client nesnesinin kurulumu: İstemciyi Kurmak.
- PHP
- Node.js
- Python
- Java
- Elle
use Gurmehub\Odemehub\Exception\SignatureException;
use Gurmehub\Odemehub\Request\{RetrieveLinkPayments, RetrieveOrders, RetrievePayments, RetrieveSubscriptions};
try {
$webhook = $client->webhook(
$_SERVER['REQUEST_METHOD'],
parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH),
file_get_contents('php://input'),
$_SERVER['HTTP_X_TIMESTAMP'] ?? null,
$_SERVER['HTTP_X_SIGNATURE'] ?? null,
);
} catch (SignatureException $exception) {
http_response_code(401);
exit;
}
if ($webhook->orderToken !== null) {
$order = $client->retrieveOrders(RetrieveOrders::byToken($webhook->orderToken))->orders[0] ?? null;
} elseif ($webhook->linkPaymentToken !== null) { // payment_link.*
$linkPayment = $client->retrieveLinkPayments(RetrieveLinkPayments::byToken($webhook->linkPaymentToken))->linkPayments[0] ?? null;
} elseif ($webhook->subscriptionToken !== null) {
$subscription = $client->retrieveSubscriptions(RetrieveSubscriptions::byToken($webhook->subscriptionToken))->subscriptions[0] ?? null;
} elseif ($webhook->transactionToken !== null) {
$transaction = $client->retrievePayments(RetrievePayments::byToken($webhook->transactionToken))->payments[0] ?? null;
}
http_response_code(204);
import express from 'express';
import { SignatureError } from '@odemehub/node-sdk';
app.post('/odemehub/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
let webhook;
try {
webhook = client.webhook(req.method, req.path, req.body, req.get('X-Timestamp'), req.get('X-Signature'));
} catch (error) {
if (error instanceof SignatureError) {
return res.sendStatus(401);
}
throw error;
}
if (webhook.orderToken !== null) {
const [order] = (await client.retrieveOrders({ token: webhook.orderToken })).orders;
} else if (webhook.linkPaymentToken !== null) { // payment_link.*
const [linkPayment] = (await client.retrieveLinkPayments({ token: webhook.linkPaymentToken })).linkPayments;
} else if (webhook.subscriptionToken !== null) {
const [subscription] = (await client.retrieveSubscriptions({ token: webhook.subscriptionToken })).subscriptions;
} else if (webhook.transactionToken !== null) {
const [transaction] = (await client.retrievePayments({ token: webhook.transactionToken })).payments;
}
res.sendStatus(204);
});
from odemehub import SignatureError
from odemehub.request import RetrieveLinkPayments, RetrieveOrders, RetrievePayments, RetrieveSubscriptions
@app.post("/odemehub/webhook")
def webhook():
try:
webhook = client.webhook(
request.method,
request.path,
request.get_data(), # Django: request.body
request.headers.get("X-Timestamp"),
request.headers.get("X-Signature"),
)
except SignatureError:
return "", 401
if webhook.order_token is not None:
orders = client.retrieve_orders(RetrieveOrders(token=webhook.order_token)).orders
elif webhook.link_payment_token is not None: # payment_link.*
link_payments = client.retrieve_link_payments(RetrieveLinkPayments(token=webhook.link_payment_token)).link_payments
elif webhook.subscription_token is not None:
subscriptions = client.retrieve_subscriptions(RetrieveSubscriptions(token=webhook.subscription_token)).subscriptions
elif webhook.transaction_token is not None:
payments = client.retrieve_payments(RetrievePayments(token=webhook.transaction_token)).payments
return "", 204
import com.odemehub.exception.SignatureException;
import com.odemehub.request.RetrieveLinkPayments;
import com.odemehub.request.RetrieveOrders;
import com.odemehub.request.RetrievePayments;
import com.odemehub.request.RetrieveSubscriptions;
@PostMapping("/odemehub/webhook")
public ResponseEntity<Void> webhook(
HttpServletRequest request,
@RequestBody byte[] payload,
@RequestHeader(value = "X-Timestamp", required = false) String timestamp,
@RequestHeader(value = "X-Signature", required = false) String signature
) {
com.odemehub.response.Webhook webhook;
try {
webhook = client.webhook(request.getMethod(), request.getRequestURI(), payload, timestamp, signature);
} catch (SignatureException exception) {
return ResponseEntity.status(401).build();
}
if (webhook.getOrderToken() != null) {
var orders = client.retrieveOrders(RetrieveOrders.byToken(webhook.getOrderToken())).getOrders();
} else if (webhook.getLinkPaymentToken() != null) { // payment_link.*
var linkPayments = client.retrieveLinkPayments(RetrieveLinkPayments.byToken(webhook.getLinkPaymentToken())).getLinkPayments();
} else if (webhook.getSubscriptionToken() != null) {
var subscriptions = client.retrieveSubscriptions(RetrieveSubscriptions.byToken(webhook.getSubscriptionToken())).getSubscriptions();
} else if (webhook.getTransactionToken() != null) {
var payments = client.retrievePayments(RetrievePayments.byToken(webhook.getTransactionToken())).getPayments();
}
return ResponseEntity.noContent().build();
}
# Ham gövde body.json'da; başlıklar $TIMESTAMP ve $RECEIVED değişkenlerinde, adresin yolu $WEBHOOK_PATH'te
EXPECTED=$( { printf '%s\nPOST\n%s\n' "$TIMESTAMP" "$WEBHOOK_PATH"; cat body.json; } | openssl dgst -sha256 -hmac "$ODEMEHUB_API_SECRET" | sed 's/^.* //')
[ "$EXPECTED" = "$RECEIVED" ] && echo "imza geçerli" || echo "imza geçersiz"
Yanıt ve yeniden deneme
| Kural | Değer |
|---|---|
| İlk gönderim | Olaydan 30 saniye sonra. Panelden elle yeniden gönderilen bildirim beklemeden gider. |
| Beklenen yanıt | Herhangi bir 2xx. Yanıt gövdesi kullanılmaz. |
| Zaman aşımı | 15 saniye. |
| Yönlendirme | İzlenmez; 3xx başarısız sayılır. |
| Başarısız sayılan | 2xx dışı yanıt, bağlantı hatası ya da zaman aşımı. |
| Yeniden deneme | Toplam 5 deneme; denemeler arası 60 sn, 5 dk, 15 dk, 30 dk. |
Teslim edilemeyen bildirimler panelde ilgili kaydın sayfasında HTTP kodu ve yanıtıyla listelenir; denemeleri biten bildirim oradan yeniden gönderilebilir. Yeniden gönderilen bildirim aynı id ve gövdeyle gelir. Elle yeniden gönderim, o bildirimin kaydındaki adrese gider; adres sonradan değiştirilmiş, kapatılmış ya da silinmiş olsa da.
Tekrar ve sıra
- Aynı bildirim birden fazla kez gelebilir.
idalanıyla ayıklayın; işleyiciniz aynı bildirimi ikinci kez işlediğinde sonucu değiştirmemelidir. - Bildirimlerin sırası garanti edilmez. Karar her zaman
retrieve-*yanıtındaki güncel duruma göre verildiğinde sıra önemli değildir. - Bildirim, müşterinin tarayıcısının dönüş POST'undan önce ya da sonra gelebilir; ikisi birbirinden bağımsızdır.