Geliştiriciler/Entegrasyon

Webhook'lar

Webhook, bir sipariş tamamlandığında CardV'nin sunucunuza gönderdiği bir mesajdır. Sizi sürekli sorgulama yapmaktan kurtarır. Hiçbir zaman kod içermez: Webhook geldikten sonra siparişi GET/api/v1/orders/{order_id} ile okuyun.

Webhook adreslerini Portal'dan ayarlarsınız (Owner, Integrations → Webhooks). Live ve Sandbox için ayrı ayrı ayarlanır. Bunun için bir API yoktur.

İlgili: Ürünler ve siparişler · Güvenlik

#Olaylar

OlaySiparişin yeni durumu
order.succeededsucceeded
order.partially_succeededpartially_succeeded
order.failedfailed (bu henüz iade değildir)
order.refundedrefunded (para cüzdanınıza geri döndü)
  • Her olay, bir sipariş ve bir Webhook adresi için en fazla bir kez gönderilir (yeniden denemeler hariç).
  • accepted ve processing durumları ile bakiye yükleme için olay gönderilmez.

#Mesaj içeriği

CardV, HTTPS adresinize bir POST isteği gönderir:

HTTP
POST /cardv/webhook HTTP/1.1
Content-Type: application/json
User-Agent: CardV-B2B-Webhook/1.0
X-CardV-Event: order.succeeded
X-CardV-Delivery: 5521
X-CardV-Timestamp: 1790000100
X-CardV-Signature: t=1790000100,v1=2baac51da272ee8d4b7b1382d48037976c7272f1749c9399a50e758604de8a27

Gövde burada okunaklı olsun diye biçimlendirilmiştir (CardV tek satır olarak gönderir):

JSON
{
  "event": "order.succeeded",
  "order": {
    "id": "O-00000001",
    "order_id": "O-00000001",
    "external_order_id": "TEST-0001",
    "status": "succeeded",
    "currency": "USD",
    "total_amount": "9.2500",
    "items": [
      {
        "sku_id": "S000001",
        "product_name": "Example Card",
        "quantity": 1,
        "delivery_count": 1
      }
    ]
  }
}
  • order.order_id, CardV sipariş numarasıdır. order.id de aynı değeri taşır.
  • order.external_order_id, kendi sipariş numaranızdır.
  • order.status, olay anındaki durumdur. Güncel olmayabilir.
  • X-CardV-Delivery, bu mesajın numarasıdır. Portal'daki gönderim geçmişinde görünür.

#İmza kontrolü

Her Webhook, o Webhook'a ait imzalama sırrıyla (whsec_...) imzalanır. Portal bu sırrı yalnızca bir kez gösterir: Webhook'u oluşturduğunuzda veya sırrı sıfırladığınızda.

İmza şöyle oluşturulur:

Text
X-CardV-Signature: t=<Unix saniyesi>,v1=<küçük harfli hex>
v1 = HMAC-SHA256(anahtar = imzalama sırrı, mesaj = "<t>" + "." + ham gövde baytları)

Adımlar:

  1. JSON'u ayrıştırmadan önce ham gövde baytlarını okuyun.
  2. Başlıktan t ve v1 değerlerini alın.
  3. t, sizin saatinizden 300 saniyeden fazla farklıysa mesajı reddedin.
  4. Beklenen v1 değerini hesaplayın ve sabit süreli (constant-time) bir karşılaştırmayla kontrol edin.
  5. JSON'u ancak bundan sonra ayrıştırın.

Yalnızca t ve gövde imzalanır. Olay türünü X-CardV-Event başlığından değil, gövdedeki event alanından alın.

Test verisi (sahte sır):

Text
secret   whsec_TEST_ONLY_not_a_real_secret_000000000000
t        1790000100
v1       2baac51da272ee8d4b7b1382d48037976c7272f1749c9399a50e758604de8a27

Bu test verisinin gövdesi tam olarak şu tek satırdır (266 bayt, sonunda yeni satır yok):

JSON
{"event":"order.succeeded","order":{"id":"O-00000001","order_id":"O-00000001","external_order_id":"TEST-0001","status":"succeeded","currency":"USD","total_amount":"9.2500","items":[{"sku_id":"S000001","product_name":"Example Card","quantity":1,"delivery_count":1}]}}

Python (Flask)

Python
import hashlib, hmac, os, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["CARDV_WEBHOOK_SECRET"].encode()


def verify(raw_body: bytes, header: str, tolerance: int = 300) -> bool:
    try:
        parts = dict(item.split("=", 1) for item in header.split(","))
        t, v1 = parts["t"].strip(), parts["v1"].strip()
    except (KeyError, ValueError):
        return False
    if not t.isdigit() or abs(time.time() - int(t)) > tolerance:
        return False
    signed = t.encode() + b"." + raw_body
    expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected.encode(), v1.encode())  # constant time


@app.post("/cardv/webhook")
def cardv_webhook():
    raw = request.get_data()  # raw bytes, before JSON parsing
    if not verify(raw, request.headers.get("X-CardV-Signature", "")):
        abort(400)
    event = request.get_json()
    store_event(request.headers["X-CardV-Delivery"], event)  # your code
    return "", 204  # answer fast; do the work later

Node.js (Express)

Node.js
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.CARDV_WEBHOOK_SECRET;

function verify(rawBody, header, toleranceSeconds = 300) {
  const parts = {};
  for (const p of String(header || "").split(",")) {
    const i = p.indexOf("=");
    parts[p.slice(0, i).trim()] = p.slice(i + 1).trim();
  }
  const { t, v1 } = parts;
  if (!t || !v1 || !/^\d+$/.test(t)) return false;
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - Number(t)) > toleranceSeconds) return false;
  const expected = crypto.createHmac("sha256", SECRET)
    .update(Buffer.concat([Buffer.from(`${t}.`, "utf8"), rawBody]))
    .digest("hex");
  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(v1, "utf8");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// express.raw keeps the exact bytes. Do not use express.json() on this route.
const rawJson = express.raw({ type: "application/json" });

app.post("/cardv/webhook", rawJson, async (req, res) => {
  if (!verify(req.body, req.get("X-CardV-Signature"))) return res.sendStatus(400);
  const event = JSON.parse(req.body.toString("utf8"));
  await storeEvent(req.get("X-CardV-Delivery"), event); // your code
  res.sendStatus(204);
});

İki örnek de test verisini doğru şekilde doğrular.

#Yeniden denemeler

  • Olayı kaydettikten sonra 15 saniye içinde herhangi bir 2xx durum koduyla yanıt verin. Zaman alan işleri (siparişi okumak, kodları göndermek) sonraya bırakın.
  • Bunun dışındaki her şey başarısız sayılır: başka bir durum kodu, zaman aşımı veya bağlantı hatası.
  • CardV yönlendirmeleri takip etmez. 3xx yanıtı başarısız sayılır. Son adresin tam halini kaydedin.
  • Başarısız bir mesaj, toplam 6 denemeye kadar yeniden gönderilir:
DenemeNe zaman
1Olaydan hemen sonra
21. deneme başarısız olduktan yaklaşık 1 dakika sonra
32. deneme başarısız olduktan yaklaşık 5 dakika sonra
43. deneme başarısız olduktan yaklaşık 15 dakika sonra
54. deneme başarısız olduktan yaklaşık 30 dakika sonra
65. deneme başarısız olduktan yaklaşık 60 dakika sonra

Süreler bir dakikaya kadar gecikebilir. 6. başarısız denemeden sonra CardV göndermeyi bırakır. Tüm mesajları Portal'daki gönderim geçmişinde görebilir ve yeniden gönderebilirsiniz.

#Tekrarlanan mesajlar

Aynı olay birden fazla kez gelebilir ve olaylar sırasız gelebilir.

  • Daha önce işlediğiniz bir olayı yok sayın. Doğrulanmış gövdedeki order.order_id ve event değerlerini birlikte kontrol edin. Portal'dan yeniden gönderilen mesaj yeni bir X-CardV-Delivery numarası alır. Bu yüzden tek başına o numara yeterli değildir.
  • Gövde, olayın gerçekleştiği andaki bir anlık görüntüdür. Siparişi her zaman yeniden okuyun ve güncel durumuna göre işlem yapın.
  • Webhook'lar kaçabilir. Birkaç dakikadan eski, henüz tamamlanmamış siparişleri kontrol eden bir zamanlanmış görev de çalıştırın.

#Adres kuralları

  • Adres https:// ile başlamalı ve internetten erişilebilen herkese açık bir adrese gitmelidir. Özel ve yerel adresler reddedilir.
  • Dört olayın hepsine veya yalnızca bazılarına abone olabilirsiniz.
  • İmzalama sırrını sıfırlamak hemen geçerli olur. Eski sır çalışmayı bırakır. Kesinti olmadan geçiş yapmak için yeni sırla ikinci bir Webhook ekleyin, sunucunuza dağıtın, sonra eskisini devre dışı bırakın.
  • CardV, sorun gidermek için yanıtınızın ilk 1.000 karakterini saklar. Yanıtınıza gizli bilgi veya kişisel veri koymayın.

Entegrasyonla ilgili sorunuz mu var? Merchant ID’nizi ve sipariş ya da istek kimliğini ekleyerek [email protected] adresine yazın.