İçeriğe geç
Crypto Merchant

Geliştiriciler

Webhook'lar

Webhook'lar, örneğin bir fatura ödendiğinde olduğu gibi bir şey gerçekleştiğinde sunucunuza haber verir; böylece API'yi sürekli sorgulamanız gerekmez.

Uç nokta ekleme

Portalda Geliştiriciler bölümünü açın ve sunucunuzda olayları alacak HTTPS adresini ekleyin. Her uç nokta, whsec_ ile başlayan kendi imzalama sırrını alır. Bir uç nokta, sonradan eklenenler dahil tüm olayları veya yalnızca seçtiğiniz olayları alabilir. Uç noktanızın çalıştığını kontrol etmek için Test olayı gönder seçeneğini kullanın.

Olaylar

invoice.created
Bir fatura oluşturuldu.
invoice.confirming
Zincir üstünde bir ödeme görüldü ve onay bekliyor.
invoice.paid
Fatura ödendi ve onaylandı. Siparişleri bu olaya göre teslim edin.
invoice.underpaid
Onaylanan ödemedeki eksik, toleransınızdan fazla.
invoice.expired
Herhangi bir ödeme görülmeden süre doldu.
invoice.cancelled
Fatura iptal edildi.
invoice.refunded
Coinler müşteriye geri gitti: eksik ödenen bir faturanın iadesi yapıldı veya oluşturduğunuz bir iade bağlantısı talep edildi. Her talep için gönderilir; bu yüzden kısmi bir iade de bir olay gönderir.
deposit.pending
Müşterilerinizden birinin sabit cüzdanına yapılan bir yatırma zincir üstünde görüldü ve onay bekliyor.
deposit.confirmed
Bir sabit cüzdan yatırması onaylandı ve ücret düşülerek bakiyenize eklendi: tutarı şimdi o müşterinin hesabına ekleyin.
deposit.dropped
Görülen bir sabit cüzdan yatırması hiç onaylanmadı (başarısız oldu veya zincirden kaldırıldı).
payout.sent
Cüzdanınıza bir ödeme gönderimi yapıldı veya bir iş ortağı payı iş ortağının cüzdanına gönderildi.
payout.failed
Bir ödeme gönderimi yapılamadı.
swap.completed
Tutulacak coininize yapılan bir takas tamamlandı.

Yük (payload)

Her olay, JSON gövdeli bir HTTP POST isteği olarak gönderilir:

JSON
{
  "id": "evt_…",
  "type": "invoice.paid",
  "created": "2026-10-28T09:41:07.000Z",
  "data": {
    "id": "inv_…",
    "object": "invoice",
    "status": "paid",
    …
  }
}

id olaya özgüdür, type yukarıdaki olaylardan biridir ve created olayın gerçekleştiği zamandır. Fatura olaylarında data, fatura nesnesidir. Ödeme gönderimi olaylarında ödeme gönderimi nesnesidir. Takas olaylarında data takası açıklar.

Başlıklar

Content-Type
Her zaman application/json.
CM-Signature
Zaman damgası ve imza: t= saniye cinsinden Unix zamanı, ardından v1= onaltılık (hex) HMAC-SHA256 imzası.
CM-Event-Id
Olay kimliği; gövdedeki id ile aynıdır.
User-Agent
Her zaman CryptoMerchant-Webhooks/1.
HTTP
POST /webhooks/crypto-merchant HTTP/1.1
Content-Type: application/json
User-Agent: CryptoMerchant-Webhooks/1
CM-Signature: t=1792834867,v1=5f2c…
CM-Event-Id: evt_…

İmzayı doğrulama

Bir olaya güvenmeden önce imzayı her zaman kontrol edin. Herkes uç noktanıza istek gönderebilir, ancak sırrınızı yalnızca Crypto Merchant bilir.

  1. 1

    JSON ayrıştırılmadan önce ham istek gövdesini tam olarak geldiği haliyle okuyun.

  2. 2

    CM-Signature başlığını virgüllerden bölün ve t ile v1 değerlerini okuyun.

  3. 3

    Uç nokta sırrınızı anahtar olarak kullanarak zaman damgası, bir nokta ve ham gövdeden oluşan dizenin HMAC-SHA256 değerini küçük harfli onaltılık (hex) olarak hesaplayın.

  4. 4

    Bunu sabit süreli bir karşılaştırma kullanarak v1 ile karşılaştırın.

  5. 5

    Eski isteklerin yeniden oynatılamaması için zaman damgası beş dakikadan eski olan olayları reddedin.

verify-webhook.js · Node.js ile webhook doğrulama
import crypto from "node:crypto";

/**
 * Checks a Crypto Merchant webhook and returns the event.
 * rawBody: the request body exactly as it arrived (string or Buffer).
 */
export function verifyWebhook(rawBody, signatureHeader, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(
    String(signatureHeader ?? "")
      .split(",")
      .map((part) => part.trim().split("=")),
  );
  const t = parts.t;
  const v1 = parts.v1 ?? "";
  if (!t || !v1) throw new Error("Missing signature");

  const body = Buffer.isBuffer(rawBody) ? rawBody.toString("utf8") : rawBody;
  const expected = crypto
    .createHmac("sha256", secret) // your endpoint secret, whsec_…
    .update(`${t}.${body}`)
    .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(v1);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    throw new Error("Bad signature");
  }
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) {
    throw new Error("Timestamp too old");
  }
  return JSON.parse(body);
}
server.js · Express
import express from "express";
import { verifyWebhook } from "./verify-webhook.js";
import { db } from "./db.js"; // your database, e.g. a pg Pool

// CREATE TABLE webhook_events (
//   id text PRIMARY KEY, type text NOT NULL, payload jsonb NOT NULL,
//   received_at timestamptz NOT NULL DEFAULT now(), processed_at timestamptz
// );

const app = express();

// Keep the raw body: re-serialised JSON would not match the signature.
app.post(
  "/webhooks/crypto-merchant",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    let event;
    try {
      event = verifyWebhook(req.body, req.get("CM-Signature"), process.env.CM_WEBHOOK_SECRET);
    } catch {
      return res.sendStatus(400);
    }

    try {
      // 1. Save the event before replying. A repeat delivery has the same id and is ignored.
      await db.query(
        "INSERT INTO webhook_events (id, type, payload) VALUES ($1, $2, $3) ON CONFLICT (id) DO NOTHING",
        [event.id, event.type, event],
      );
    } catch {
      return res.sendStatus(500); // not saved: we send it again later
    }

    // 2. It's stored, so confirm it.
    res.sendStatus(200);

    // 3. Do the work from what you saved, here or in a background job.
    //    If your server stops now, the event is still waiting in the table.
    processEvents().catch(console.error);
  },
);

async function processEvents() {
  const { rows } = await db.query(
    "SELECT payload FROM webhook_events WHERE processed_at IS NULL ORDER BY received_at",
  );
  for (const { payload: event } of rows) {
    if (event.type === "invoice.paid") {
      // Mark the order in event.data.order_id as paid. Doing it twice must change nothing.
    }
    await db.query("UPDATE webhook_events SET processed_at = now() WHERE id = $1", [event.id]);
  }
}
verify_webhook.py · Python
import hashlib, hmac, json, time

def verify_webhook(raw_body: bytes, signature_header: str, secret: str, tolerance_sec: int = 300) -> dict:
    """Checks a Crypto Merchant webhook and returns the event. raw_body: the request body exactly as it arrived."""
    parts = dict(part.strip().split("=", 1) for part in (signature_header or "").split(",") if "=" in part)
    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not v1:
        raise ValueError("Missing signature")
    signed = t.encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()  # your endpoint secret, whsec_…
    if not hmac.compare_digest(expected, v1):
        raise ValueError("Bad signature")
    if abs(time.time() - int(t)) > tolerance_sec:
        raise ValueError("Timestamp too old")
    return json.loads(raw_body)
verify_webhook.php · PHP
<?php
// Checks a Crypto Merchant webhook and returns the event.
// Read the raw body with file_get_contents('php://input') and the header with $_SERVER['HTTP_CM_SIGNATURE'].
function verify_webhook(string $rawBody, string $signatureHeader, string $secret, int $toleranceSec = 300): array {
    $parts = [];
    foreach (explode(',', $signatureHeader) as $part) {
        [$key, $value] = array_pad(explode('=', trim($part), 2), 2, '');
        $parts[$key] = $value;
    }
    $t = $parts['t'] ?? '';
    $v1 = $parts['v1'] ?? '';
    if ($t === '' || $v1 === '') {
        throw new RuntimeException('Missing signature');
    }
    $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret); // your endpoint secret, whsec_…
    if (!hash_equals($expected, $v1)) {
        throw new RuntimeException('Bad signature');
    }
    if (abs(time() - (int) $t) > $toleranceSec) {
        throw new RuntimeException('Timestamp too old');
    }
    return json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
}

Yanıt verme ve yeniden denemeler

Olayı aldığınızı onaylamak için 10 saniye içinde herhangi bir 2xx durum koduyla yanıt verin. Başka herhangi bir yanıt veya yanıt verilmemesi başarısızlık sayılır ve şu sürelerden sonra yeniden deneriz:

  1. 1 dakika
  2. 5 dakika
  3. 15 dakika
  4. 1 saat
  5. 3 saat
  6. 6 saat
  7. 12 saat
  8. 24 saat

Bu, yaklaşık iki gün boyunca en fazla sekiz yeniden deneme demektir. Her deneme yeni bir zaman damgasıyla yeniden imzalanır.

İyi uygulamalar

  • Yanıt vermeden önce her olayı kaydedin, ardından işi kaydettiğiniz veriler üzerinden yapın; örneğin bir arka plan işinde. Kaydetme başarısız olursa bir hata ile yanıt verin; olayı yeniden göndeririz.
  • Olaylar birden fazla kez ve her zaman sırayla olmadan gelebilir. Daha önce işlediğiniz olayları atlamak için olayın id değerini kullanın.
  • Değerli bir siparişi göndermeden önce, durumunu bir kez daha kontrol etmek için faturayı API üzerinden alabilirsiniz.
  • whsec_ sırrınızı bir API anahtarı gibi sunucunuzda saklayın.