سیدھے مواد پر جائیں
Crypto Merchant

ڈیولپرز

Webhooks

Webhooks آپ کے سرور کو بتاتے ہیں کہ کب کچھ ہوا، جیسے کوئی انوائس ادا ہو گئی، تاکہ آپ کو بار بار API سے پوچھنا نہ پڑے۔

اینڈ پوائنٹ شامل کریں

پورٹل میں ڈیولپرز کھولیں اور اپنے سرور کا وہ HTTPS ایڈریس شامل کریں جس پر ایونٹس موصول ہونے چاہییں۔ ہر اینڈ پوائنٹ کو اپنا سائننگ سیکرٹ ملتا ہے، جو whsec_ سے شروع ہوتا ہے۔ اینڈ پوائنٹ ہر ایونٹ وصول کر سکتا ہے، بشمول بعد میں شامل ہونے والے ایونٹس، یا صرف وہ ایونٹس جو آپ منتخب کریں۔ اپنا اینڈ پوائنٹ چیک کرنے کے لیے ٹیسٹ ایونٹ بھیجیں استعمال کریں۔

ایونٹس

invoice.created
ایک انوائس بنائی گئی۔
invoice.confirming
آن چین ایک ادائیگی نظر آئی ہے اور کنفرمیشنز کا انتظار ہے۔
invoice.paid
انوائس ادا اور کنفرم ہو گئی۔ آرڈرز اسی ایونٹ پر پورے کریں۔
invoice.underpaid
کنفرم شدہ ادائیگی میں کمی آپ کی گنجائش سے زیادہ ہے۔
invoice.expired
کوئی ادائیگی نظر آنے سے پہلے وقت ختم ہو گیا۔
invoice.cancelled
انوائس منسوخ کر دی گئی۔
invoice.refunded
کوائنز کسٹمر کو واپس گئے: کم ادائیگی والی انوائس کا ریفنڈ، یا آپ کا بنایا ہوا کوئی ریفنڈ لنک کلیم کیا گیا۔ ہر کلیم پر بھیجا جاتا ہے، اس لیے جزوی ریفنڈ پر بھی ایک بھیجا جاتا ہے۔
deposit.pending
آپ کے کسی کسٹمر کے مستقل والٹ میں آن چین ایک ڈپازٹ نظر آیا ہے اور کنفرمیشنز کا انتظار ہے۔
deposit.confirmed
ایک مستقل والٹ ڈپازٹ کنفرم ہو کر فیس کٹنے کے بعد آپ کے بیلنس میں شامل ہو گیا: اب اس کسٹمر کے اکاؤنٹ میں رقم شامل کریں۔
deposit.dropped
نظر آنے والا ایک مستقل والٹ ڈپازٹ کبھی کنفرم نہیں ہوا (ناکام ہو گیا یا چین سے ہٹا دیا گیا)۔
payout.sent
آپ کے والٹ میں ایک پے آؤٹ بھیجا گیا، یا کسی پارٹنر کا حصہ پارٹنر کے والٹ میں بھیجا گیا۔
payout.failed
ایک پے آؤٹ نہیں بھیجا جا سکا۔
swap.completed
آپ کے ہولڈنگ کوائن میں ایک سواپ مکمل ہو گیا۔

پے لوڈ

ہر ایونٹ JSON باڈی کے ساتھ HTTP POST کے طور پر بھیجا جاتا ہے:

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

id ہر ایونٹ کے لیے منفرد ہے، type اوپر دیے گئے ایونٹس میں سے ایک ہے، اور created بتاتا ہے کہ یہ کب ہوا۔ انوائس ایونٹس کے لیے data انوائس آبجیکٹ ہوتا ہے۔ پے آؤٹ ایونٹس کے لیے یہ پے آؤٹ آبجیکٹ ہوتا ہے۔ سواپ ایونٹس کے لیے data سواپ کی تفصیل دیتا ہے۔

ہیڈرز

Content-Type
ہمیشہ application/json۔
CM-Signature
ٹائم اسٹیمپ اور سگنیچر: t= سیکنڈز میں Unix وقت، پھر v1= ہیکس HMAC-SHA256 سگنیچر۔
CM-Event-Id
ایونٹ ID، وہی جو باڈی میں id ہے۔
User-Agent
ہمیشہ 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_…

سگنیچر کی تصدیق کریں

کسی ایونٹ پر بھروسہ کرنے سے پہلے ہمیشہ سگنیچر چیک کریں۔ کوئی بھی آپ کے اینڈ پوائنٹ پر درخواست بھیج سکتا ہے، لیکن آپ کا سیکرٹ صرف Crypto Merchant جانتا ہے۔

  1. 1

    JSON پارس کرنے سے پہلے درخواست کی خام باڈی بالکل ویسی ہی پڑھیں جیسی موصول ہوئی۔

  2. 2

    CM-Signature ہیڈر کو کوماز پر تقسیم کریں اور t اور v1 کی ویلیوز پڑھیں۔

  3. 3

    ٹائم اسٹیمپ، ایک ڈاٹ (.) اور خام باڈی کا HMAC-SHA256 نکالیں، اپنے اینڈ پوائنٹ سیکرٹ کو کی کے طور پر استعمال کرتے ہوئے، چھوٹے حروف والے ہیکس میں۔

  4. 4

    کانسٹنٹ ٹائم موازنے کے ذریعے اسے v1 سے ملائیں۔

  5. 5

    ایسے ایونٹس رد کر دیں جن کا ٹائم اسٹیمپ پانچ منٹ سے زیادہ پرانا ہو، تاکہ پرانی درخواستیں دوبارہ نہ چلائی جا سکیں۔

verify-webhook.js · Node.js میں webhook کی تصدیق کریں
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);
}

جواب دینا اور دوبارہ کوششیں

ایونٹ موصول ہونے کی تصدیق کے لیے 10 سیکنڈ کے اندر کسی بھی 2xx اسٹیٹس کے ساتھ جواب دیں۔ کوئی اور جواب، یا کوئی جواب نہ ملنا، ناکامی شمار ہوتا ہے، اور ہم اتنی دیر بعد دوبارہ کوشش کرتے ہیں:

  1. 1 منٹ
  2. 5 منٹ
  3. 15 منٹ
  4. 1 گھنٹہ
  5. 3 گھنٹے
  6. 6 گھنٹے
  7. 12 گھنٹے
  8. 24 گھنٹے

یعنی تقریباً دو دن میں زیادہ سے زیادہ آٹھ دوبارہ کوششیں۔ ہر کوشش پر نئے ٹائم اسٹیمپ کے ساتھ دوبارہ سائن کیا جاتا ہے۔

اچھے طریقے

  • جواب دینے سے پہلے ہر ایونٹ محفوظ کریں، پھر جو محفوظ کیا ہے اسی سے کام کریں، مثلاً بیک گراؤنڈ جاب میں۔ اگر محفوظ کرنا ناکام ہو جائے تو ایرر اسٹیٹس کے ساتھ جواب دیں، اور ہم ایونٹ دوبارہ بھیجیں گے۔
  • ایونٹس ایک سے زیادہ بار آ سکتے ہیں، اور ہمیشہ ترتیب سے نہیں آتے۔ جو ایونٹس آپ پہلے ہینڈل کر چکے ہیں انہیں چھوڑنے کے لیے ایونٹ کی id استعمال کریں۔
  • کوئی قیمتی آرڈر بھیجنے سے پہلے آپ API سے انوائس حاصل کر کے اس کا اسٹیٹس دوبارہ چیک کر سکتے ہیں۔
  • اپنا whsec_ سیکرٹ API کی کی طرح اپنے سرور پر ہی رکھیں۔