راه‌اندازی و اتصال

اتصال سایت به درگاه پرداخت رمزارزی با REST API و وب‌هوک

راهنمای عملی اتصال با API: ساخت فاکتور، بررسی امضای وب‌هوک، idempotency و مدیریت حالت‌های لبه — با نمونه کد.

این نوشته پیش‌نویس است و هنوز منتشر نشده. محتوای آن ممکن است ناقص یا در حال بازنویسی باشد.

اگر دنبال یک راهنمای مفهومی درباره بلاکچین هستید، این نوشته آن نیست. اینجا فرض بر این است که شما یک سایت دارید، می‌خواهید پرداخت رمزارزی بگیرید، و می‌خواهید بدانید دقیقاً چه درخواستی بفرستید و چه چیزی برگردانده می‌شود.

نکته‌ای که بیشتر از بقیه این متن اهمیت دارد و معمولاً هم جا می‌افتد: بخش سخت اتصال، ساخت فاکتور نیست؛ درست دریافت‌کردن وب‌هوک است. ساخت فاکتور یک درخواست ساده است. وب‌هوک جایی است که یکپارچه‌سازی‌ها در هفته دوم می‌شکنند.

کل API درگاه پرداخت ارز دیجیتال در عمل به چند endpoint خلاصه می‌شود و در یک بعدازظهر وصل می‌شود. آنچه وقت می‌برد، حالت‌های لبه است: وب‌هوکی که دو بار می‌رسد، وب‌هوکی که اصلاً نمی‌رسد، و پرداختی که مبلغش با فاکتور نمی‌خواند. این راهنما وقت بیشتری روی همان‌ها می‌گذارد تا روی خود درخواست‌ها.

جریان کامل پرداخت در یک نگاه

پیش از هر کدی، مسیر را ببینید. پنج مرحله است و ترتیبش عوض نمی‌شود:

  1. مشتری سفارش را ثبت می‌کند و سرور شما یک فاکتور می‌سازد (POST /v1/invoices).
  2. پاسخ شامل payment_url است. مشتری را به آن آدرس بفرستید؛ انتخاب ارز و شبکه آنجا انجام می‌شود، نه در سایت شما.
  3. مشتری پرداخت می‌کند و تراکنش روی زنجیره ثبت می‌شود.
  4. پس از قطعی‌شدن، وب‌هوک invoice.paid به سرور شما می‌رسد.
  5. سرور شما امضا را بررسی می‌کند و اگر معتبر بود، سفارش را به وضعیت پرداخت‌شده می‌برد.

مرحله ۵ همان‌جایی است که امنیت این مسیر تعیین می‌شود. یک درخواست جعلی به آدرس وب‌هوک شما، اگر امضایش بررسی نشود، دقیقاً همان کاری را می‌کند که یک رسید جعلی با یک اپراتور بی‌دقت می‌کند.

احراز هویت و کلید API

همه درخواست‌ها به https://api.ircryptopay.online می‌روند و با یک توکن Bearer احراز هویت می‌شوند:

Authorization: Bearer $IRCRYPTO_KEY

سه قاعده درباره این کلید:

  • فقط سمت سرور. این کلید هرگز نباید در جاوااسکریپت مرورگر، اپلیکیشن موبایل یا مخزن کد شما باشد. هر کسی که کلید را داشته باشد می‌تواند به‌جای شما فاکتور بسازد.
  • در متغیر محیطی نگه دارید، نه در فایل تنظیمات کامیت‌شده.
  • اگر لو رفت، از داشبورد باطلش کنید و کلید تازه بسازید. عوض‌کردن کلید هزینه‌ای ندارد؛ لو رفتنش دارد.

کلید API پس از فعال‌سازی حساب در دسترس قرار می‌گیرد.

ساخت فاکتور

ساده‌ترین بخش کار. یک درخواست POST:

curl -X POST "https://api.ircryptopay.online/v1/invoices" \
  -H "Authorization: Bearer $IRCRYPTO_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_mode": "fixed",
    "amount": "1520000.00",
    "currency": "IRR",
    "external_ref": "order-2041",
    "description": "سفارش ۲۰۴۱",
    "redirect_url": "https://shop.example.ir/thanks",
    "expires_in": 86400
  }'

و پاسخ:

{
  "invoice": {
    "status": "open",
    "amount_mode": "fixed",
    "price_amount": "1520000.000000",
    "price_currency": "IRR",
    "external_ref": "order-2041",
    "public_token": "a7685a591742daae1a742f337f9b185e",
    "payment_url": "https://pay.ircryptopay.online/pay/a7685a591742daae1a742f337f9b185e",
    "expires_at": "2026-08-18T15:39:12.000Z",
    "paid_at": null
  },
  "created": true
}

سه نکته که ارزش دارد همین‌جا بدانید:

مبلغ رشته است، نه عدد. "1520000.00" و نه 1520000.00. مبالغ مالی در اعداد اعشاری جاوااسکریپت سالم نمی‌مانند؛ رشته‌بودن یک انتخاب عمدی است. در سمت خودتان هم با رشته یا decimal کار کنید، نه با float.

external_ref شناسه سفارش شماست و کلید یکتاسازی. این مهم‌ترین فیلد این درخواست است — در بخش idempotency برمی‌گردیم به آن.

amount_mode می‌تواند open باشد. در آن حالت مبلغ نمی‌فرستید و هر مقداری که مشتری بفرستد ثبت می‌شود؛ مناسب شارژ کیف پول یا پرداخت آزاد.

سپس مشتری را به payment_url بفرستید. انتخاب شبکه و ارز، نمایش آدرس و QR، و پایش زنجیره همه آنجا انجام می‌شود.

وب‌هوک و تأیید امضا

اینجا بخش جدی کار است، و بخشی که در اتصال api پرداخت رمزارز بیشتر از هر جای دیگری اشتباه انجام می‌شود.

وقتی پرداخت روی زنجیره قطعی شد، رویداد invoice.paid با POST به آدرسی که در داشبورد ثبت کرده‌اید فرستاده می‌شود، به‌همراه این هدرها:

هدرمعنا
ir-signatureبه شکل t=<unix>,v1=<hex>
ir-event-typeنوع رویداد، مثلاً invoice.paid
ir-delivery-idشناسه یکتای این تلاش تحویل

مقدار v1 امضای HMAC-SHA256 روی رشته "<t>.<raw body>" با secret همان endpoint است. مهلت پذیرش ۳۰۰ ثانیه است.

نمونه بررسی در Node.js:

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret, rawBody, header, toleranceSec = 300) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.trim().split("=")),
  );
  if (!parts.t || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSec) return false;

  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(parts.v1, "utf8");
  return a.length === b.length && timingSafeEqual(a, b);
}

سه اشتباهی که در این تابع رخ می‌دهد و هر سه هم بی‌سر و صدا هستند:

۱. امضا را روی بدنه پارس‌شده حساب کردن. امضا روی بایت‌های خام بدنه است. اگر JSON را پارس کنید و دوباره به رشته تبدیل کنید، یک فاصله یا ترتیب کلید متفاوت، امضا را باطل می‌کند. در اکسپرس یعنی باید express.raw() استفاده کنید، نه express.json().

۲. مقایسه با ==. مقایسه معمولی رشته‌ها به‌محض رسیدن به اولین بایت متفاوت متوقف می‌شود، و همین اختلاف زمانی به مهاجم اجازه می‌دهد امضا را بایت‌به‌بایت حدس بزند. از timingSafeEqual یا hmac.compare_digest استفاده کنید.

۳. نادیده‌گرفتن مهر زمانی. اگر t را بررسی نکنید، یک درخواست معتبر قدیمی که کسی ضبطش کرده تا همیشه قابل ارسال دوباره است.

نمونه‌های Python، Go و زبان‌های دیگر در مستندات API آمده است.

تکرار وب‌هوک و idempotency

این همان چیزی است که در هفته دوم خرابی ایجاد می‌کند.

وب‌هوک ممکن است بیش از یک بار برسد. اگر سرور شما دیر پاسخ دهد یا خطا برگرداند، تحویل دوباره تلاش می‌شود. اگر هندلر شما موجودی را کم کند یا ایمیل بفرستد، بار دوم همان کار را دوباره انجام می‌دهد.

راه‌حل این است که هندلر را idempotent بنویسید: کلید را external_ref (یا شناسه فاکتور) بگذارید و پیش از هر کاری بررسی کنید که این سفارش قبلاً پرداخت‌شده علامت نخورده باشد.

if (order.status === "paid") return res.sendStatus(200);

سمت ساخت فاکتور هم همین محافظت وجود دارد: اگر external_ref تکراری بفرستید، همان فاکتور قبلی برگردانده می‌شود و created برابر false است. یعنی ارسال دوباره درخواست، فاکتور دوم نمی‌سازد. اگر کاربر روی دکمه پرداخت دو بار کلیک کند، دو فاکتور نخواهید داشت.

و همیشه سریع 200 برگردانید. کار سنگین را در صف پس‌زمینه انجام دهید، نه داخل هندلر وب‌هوک؛ وگرنه تایم‌اوت می‌گیرید و تحویل دوباره تلاش می‌شود.

خطاها و حالت‌های لبه

اگر وب‌هوک اصلاً نرسید. شبکه قطع بوده، سرور شما پایین بوده، یا آدرس اشتباه ثبت شده. دو راه جبران دارید: GET /v1/invoices/{id} که وضعیت لحظه‌ای را برمی‌گرداند، و GET /v1/events که همان رویدادها را برای بازخوانی یک بازه در اختیار می‌گذارد. هیچ‌وقت وب‌هوک را تنها منبع حقیقت نگذارید.

پرداخت ناقص. فاکتور در وضعیت partially_paid می‌ماند و مشتری می‌تواند مابه‌التفاوت را به همان آدرس بفرستد. سیستم شما باید این وضعیت را بشناسد و سفارش را نه پرداخت‌شده و نه لغو‌شده در نظر بگیرد.

انقضا. فاکتور به expired می‌رود. اگر پرداخت بعد از انقضا برسد، پول گم نمی‌شود اما رسیدگی دستی لازم دارد — پس expires_in را بی‌جهت کوتاه نگذارید.

لغو. POST /v1/invoices/{id}/cancel یک فاکتور پرداخت‌نشده را لغو می‌کند. اگر تراکنشی برایش ثبت شده باشد درخواست رد می‌شود؛ لغو فاکتور، پول روی زنجیره را برنمی‌گرداند.

وضعیت‌هایی که باید در سیستم خود مدیریت کنید: open، paid، partially_paid، expired و canceled.

پیش از رفتن روی تولید

قبل از اینکه درگاه را برای مشتریان واقعی روشن کنید، این فهرست را یک بار رد کنید. هر بند، یک خرابی واقعی است که در یکپارچه‌سازی‌ها دیده می‌شود:

  • درخواست‌ها را بدون ساختن کلید امتحان کنید. در مستندات یک کنسول آزمایش هست که همین درخواست‌ها را با یک کلید آزمایشی سمت سرور اجرا می‌کند و داده‌اش در حساب آزمایشی می‌ماند. برای دیدن شکل واقعی پاسخ‌ها، سریع‌ترین راه همین است.
  • مطمئن شوید هندلر وب‌هوک بدنه خام را می‌بیند. رایج‌ترین خرابی همین است و علامتش این است که امضا همیشه نامعتبر می‌شود، حتی وقتی کد درست به نظر می‌رسد.
  • یک وب‌هوک را دو بار بفرستید و ببینید سفارش دوباره پردازش نمی‌شود.
  • یک امضای دستکاری‌شده بفرستید و مطمئن شوید رد می‌شود. اگر پذیرفته شد، بررسی امضای شما کار نمی‌کند و بهتر است همین حالا بفهمید.
  • آدرس وب‌هوک را از بیرون تست کنید. اگر پشت فایروال یا روی localhost باشد، هیچ‌وقت چیزی دریافت نمی‌کنید.
  • یک پرداخت واقعی با مبلغ کوچک انجام دهید. کل مسیر را یک بار از سفارش تا تغییر وضعیت ببینید. هیچ آزمایشی جای این یکی را نمی‌گیرد.

یک نکته درباره لاگ: شناسه فاکتور، external_ref و ir-delivery-id هر رویداد را ذخیره کنید. وقتی یک ماه بعد مشتری بگوید «پرداخت کردم ولی سفارشم ثبت نشد»، این سه فیلد تفاوت بین یک پاسخ پنج‌دقیقه‌ای و یک ساعت جست‌وجو هستند.

بدنه خام وب‌هوک را هم برای مدت کوتاهی نگه دارید. اگر مشکلی در بررسی امضا پیش بیاید، بدون بدنه خام نمی‌توانید بازتولیدش کنید.


اگر همه این‌ها را پیاده‌سازی نمی‌کنید و فروشگاهتان روی ووکامرس است، افزونه آماده همین کارها را انجام می‌دهد و نیازی به کدنویسی ندارید. جزئیات در درگاه پرداخت تتر.

برای اینکه بدانید یک پرداخت از دید زنجیره چه زمانی واقعاً قطعی است — همان چیزی که وب‌هوک منتظرش می‌ماند — این راهنما را ببینید. مرجع کامل API درگاه پرداخت ارز دیجیتال با نمونه‌های cURL، Python، JavaScript و Go در مستندات است، و اگر تازه با سرویس آشنا می‌شوید، درگاه پرداخت ارز دیجیتال نقطه شروع بهتری است.