رسیدها
توکن رسید امضاشده، اعتبارسنجی آفلاین آن در هر زبانی، و مرز میان اثبات و تحویل سفارش
هر پرداخت تکمیلشده یک توکن رسید دارد: رشتهای کوتاه و خودبسنده که پرداخت را اثبات میکند، بر پایه شرطی که فقط شما میتوانید بررسی کنید. این پاسخ همان وضعیتی است که «مشتری میگوید پرداخت کردهام و من هیچ راهی برای دانستنش ندارم».
v1.eyJwYXltZW50X2lkIjoiYjkyZTRkMTctNmMzOC00YTA1LTlmMmItMWU3ZDNjOGE1MDQ5Iiwic
mVjZWlwdF9ubyI6IkNMUC04RjNLMk05USIsIm1lcmNoYW50X2lkIjoiOGYxYzlhMzQtM2QyZS00Y
jE3LTlmMGEtMmM2ZDViOGU0YTcxIiwicmVmZXJlbmNlX2lkIjoib3JkZXItMTA0OTIiLCJjdXJyZ
W5jeSI6InVzZHQiLCJhbW91bnQiOiIyNC45MCIsImNoYXJnZWRfYW1vdW50IjoiMjQuOTAiLCJmZ
WVfYmVhcmVyIjoibWVyY2hhbnQiLCJuZXRfYW1vdW50IjoiMjQuNzgiLCJwYWlkX2F0IjoiMjAyN
i0wOC0xMVQxMjowNDozMVoifQ.k7Qw3xR2mB9pLd4vN8sYc1TfHj0aXeU6ZgO5rWqتوکن را در سه جا دریافت میکنید: روی شیء payment با نام receipt، افزوده به return_url شما بهصورت
?receipt=...، و درون همان توکنی که مشتری میتواند از تاریخچه پرداختهای خودش کپی کند.
قالب
سه بخش جدا شده با نقطه، که بخش میانی تمام محتوا است:
v1.<base64url(payload JSON)>.<base64url(HMAC-SHA256(webhook_secret, "v1." + base64url(payload)))>v1نسخه قالب است. توکنی که بخش اول آن را نمیشناسید رد کنید، بهجای اینکه حدس بزنید.- محتوا یک JSON ساده است که با base64url و بدون padding کدگذاری شده. رمزنگاری نشده — هر کسی میتواند آن را بخواند. این عامدانه است: توکن یک اثبات است، نه یک راز.
- امضا یک HMAC-SHA256 روی رشته دقیقِ
"v1." + <بخش دوم>است، با کلید مخفی وبهوک شما، و سپس کدگذاریشده با base64url.
به آنچه امضا میشود دقت کنید: دو بخش اول به هم چسبیده، از جمله پیشوند v1. و نقطه. امضای تنهای محتوا به
کسی اجازه میداد یک امضای معتبر را روی نسخهای دیگر از قالب جابهجا کند.
فیلدهای محتوا
نام
نوع
اگر امضا را خودتان بازسازی میکنید، ترتیب فیلدها مهم است
محتوا همانطور که سریالایز شده امضا میشود، پس هر کسی که آن را از اجزا بازمیسازد باید کلیدها را با
همین ترتیب قانونی بنویسد: payment_id، receipt_no، merchant_id، reference_id، currency،
amount، charged_amount، fee_bearer، net_amount، paid_at. برای اعتبارسنجی توکنی که به شما
داده شده هیچکدام از اینها لازم نیست — رشته را همانطور که رسیده هش میکنید.
هیچ فیلد انقضایی وجود ندارد. رسید ثبت چیزی است که اتفاق افتاده، و همانطور درست میمانَد؛ اگر به تازگی
زمان اهمیت دارد، خودتان paid_at را مقایسه کنید.
چرا قابل جعل نیست
امضا یک HMAC است با کلید مخفی وبهوک شما، و آن کلید دقیقاً در دو جا وجود دارد: در انبار رمزشده کوینلند و روی سرور شما. هرگز به مشتری نشان داده نمیشود، هرگز به مرورگر فرستاده نمیشود، و هرگز بخشی از توکن نیست.
پس یک مشتری — یا هر کسی که صد رسید معتبر دیده باشد — میتواند محتوا را رمزگشایی کند، amount را به عددی
بزرگتر تغییر دهد و دوباره کدگذاری کند. کاری که نمیتواند بکند تولید امضایی است که با محتوای تغییریافته
بخواند، چون محاسبه آن به کلید نیاز دارد. کد اعتبارسنجی شما آن را در همان خطی رد میکند که یک رسید واقعی را
میپذیرفت.
این همان ساختاری است که امضای وبهوک دارد، با همان کلید، یعنی یک کلید برای چرخاندن و یک تابع رمزنگاری برای درست پیاده کردن.
اعتبارسنجی آفلاین
آفلاین مسیر پیشنهادی است. چند خط کد با کتابخانه استاندارد است، هیچ رفتوبرگشت شبکهای ندارد، و وقتی کوینلند در دسترس نباشد هم کار میکند.
import crypto from "node:crypto";
/**
* Returns the payload if the token is authentic, or null.
* `secret` is your webhook secret.
*/
export function verifyReceipt(
token: string,
secret: string,
expectedMerchantId: string,
): Record<string, string> | null {
const parts = token.split(".");
if (parts.length !== 3 || parts[0] !== "v1") return null;
const [version, body, signature] = parts;
const expected = crypto
.createHmac("sha256", secret)
.update(`${version}.${body}`)
.digest("base64url");
// Constant-time: a comparison that returns early leaks how much of a guess
// was right, which is enough to reconstruct a signature.
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(signature, "utf8");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return null;
const payload = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
// A signature only proves the token came from a Coinland secret. Checking the
// merchant id is what proves it came from YOURS.
if (payload.merchant_id !== expectedMerchantId) return null;
return payload;
}سه چیز که در هر زبانی باید درست انجام شوند:
- base64url، نه base64. کاراکترهای
-و_جای+و/را میگیرند و padding با=حذف میشود. یک رمزگشای base64 معمولی روی بعضی توکنها شکست میخورد و روی بعضی موفق میشود، که بدترین نوع باگ برای داشتن در یک مسیر پرداخت است. - مقایسه زمانثابت.
crypto.timingSafeEqual،hash_equals،hmac.compare_digest— هر نامی که کتابخانه استاندارد شما دارد. یک==با خروج زودهنگام روی امضا یک ضعف واقعی و بهرهبرداریشده است، نه نظری. merchant_idرا بررسی کنید. امضای معتبر اثبات میکند توکن با یکی از کلیدهای وبهوک کوینلند ساخته شده. مقایسه شناسه کسبوکار است که اثبات میکند با کلید شما ساخته شده و نه کلید کسبوکار دیگری.
اعتبارسنجی از طریق API
اگر ترجیح میدهید HMAC را خودتان پیاده نکنید، POST /api/pay/v1/receipts/verify این کار را برایتان انجام
میدهد. این اندپوینت هم امضا را با کلید شما بررسی میکند و هم اینکه پرداختی با همان مشخصات هنوز وجود
دارد، پس چیزی را میگیرد که اعتبارسنجی آفلاین نمیتواند: یک توکن درستامضاشده برای پرداختی که بعداً معلوم
شد چیزی جز آنچه ادعا میکرد بوده است.
curl -X POST https://my.coinlandexchange.com/api/pay/v1/receipts/verify \
-H "Authorization: Bearer $COINLAND_PAY_KEY" \
-H "Content-Type: application/json" \
-d '{"receipt":"v1.eyJwYXltZW50X2lkIjoi....k7Qw3xR2mB9pLd4vN8sYc1TfHj0aXeU6ZgO5rWq"}'{
"valid": true,
"payment": {
"id": "b92e4d17-6c38-4a05-9f2b-1e7d3c8a5049",
"receipt_no": "CLP-8F3K2M9Q",
"reference_id": "order-10492",
"currency": "usdt",
"amount": "24.90",
"charged_amount": "24.90",
"fee_bearer": "merchant",
"net_amount": "24.78",
"paid_at": "2026-08-11T12:04:31Z"
}
}امضای نامعتبر، محتوای تغییریافته و پرداخت ناشناس همه با PAY_RECEIPT_INVALID (۴۲۲) پاسخ میگیرند — یک کد،
چون راهحل در هر سه حالت یکی است و تفکیک آنها به یک مهاجم میگفت کدام نیمه از جعلش داشته کار میکرده.
این اندپوینت یک امکان راحت است، نه مرجع. همان رکوردهایی را میخواند که اعتبارسنجی آفلاین دربارهشان استدلال میکند، پس توکنی که آفلاین با کلید خودتان تأیید شود همین حالا اثبات شده است؛ فراخوانی شبکه بررسی وجود را اضافه میکند، نه اعتماد را.
اثبات، تحویل نیست
تحویل را بر پایه وبهوک یا API انجام دهید
یک رسید تأییدشده به شما میگوید پرداختی انجام شده است. به شما نمیگوید که این سفارش قبلاً تحویل نشده، و
توسط هر کسی که آن را در دست دارد ارائه میشود. کالا را از هندلر وبهوک یا از
GET /payments/{id} آزاد کنید، بر پایه وضعیت سفارش در سیستم خودتان.
این تفکیک مهم است چون رسید عامدانه قابلحمل است. همان توکن میتواند دو بار نشان داده شود، یا توسط کسی که مشتری آن را برایش فرستاده. هیچکدام نقص نیست — همین است که رسید را مفید میکند — اما یعنی توکن به «آیا این پرداخت شد؟» پاسخ میدهد و هرگز به «آیا باید ارسال کنم؟».
کاربردهای درست یک رسید:
- پشتیبانی. مشتری توکنش را میچسباند؛ کارشناس شما با یک فراخوانی تابع آن را تأیید میکند و بیدرنگ میداند باید حرفش را باور کند یا نه، بدون هیچ استعلامی از کوینلند.
- سیستمهای پاییندستی. یک سرویس تحویل یا یک شریک میتواند پرداخت را بررسی کند بدون آنکه کلید API شما را داشته باشد، چون اعتبارسنجی فقط به کلید مخفی وبهوک نیاز دارد و به شبکه نیازی ندارد.
- سوابق. توکن را همراه سفارش ذخیره کنید. سالها بعد هم اثبات میکند چه چیزی پرداخت شده، حتی اگر API جلو رفته باشد.
کاری که یک رسید هرگز نباید باشد:
- چیزی که صفحه
return_urlشما برای پرداختشده علامت زدن سفارش به آن اعتماد کند. - توکن دسترسی برای هر چیزی. رسید هیچ اجازهای نمیدهد؛ گواهی میدهد.