شروع سریع
از یک حساب ارتقایافته تا یک سفارش تحویلشده، همراه با متن کامل درخواستها
این کل یکپارچهسازی از ابتدا تا انتها است. پنج مرحله، بدون نیاز به هیچ SDK.
۱. حسابتان را به کسبوکار ارتقا دهید
نه ثبتنام جداگانهای برای کسبوکار وجود دارد و نه مسیر ثبتنام خاصی. کوینلند پی روی یک حساب کاربری معمولی کوینلند کار میکند و دسترسی در سه مرحله به دست میآید:
ابتدا مانند یک کاربر عادی ثبتنام کنید در my.coinlandexchange.com/register، از همان مسیر ثبتنام معمولی. هیچ چیز این مرحله مخصوص کسبوکار نیست.
سپس از بخش پشتیبانی داشبورد یک تیکت ثبت کنید و فعالسازی حساب کسبوکار و دسترسی به کوینلند پی را درخواست کنید. اطلاعات کسبوکارتان را در همان تیکت بنویسید.
تیم کوینلند بررسی میکند و در همان تیکت پاسخ میدهد. پس از تأیید، حساب شما ارتقا مییابد و بخش «مدیریت کسبوکار» در داشبوردتان ظاهر میشود — همان جایی که کلید API میسازید و ویجت را تنظیم میکنید.
نه فرم درخواستی وجود دارد و نه فعالسازی خودکار: یک نفر تیکت شما را میخواند.
بعد از ارتقا، بخش مدیریت کسبوکار (کنسول کسبوکار) در داشبورد شما و در نشانی
https://my.coinlandexchange.com/business ظاهر میشود. آنجا اینها را تعیین میکنید:
- ارزهای پذیرفتهشده. فقط اینها میتوانند در
amountsیک جلسه پرداخت بیایند. هر چیز دیگری باPAY_CURRENCY_NOT_ACCEPTEDرد میشود. کوینلند پی فقط از ارزهای دیجیتال پشتیبانی میکند؛ پرداخت تومانی در این سرویس ارائه نمیشود. - نام نمایشی و لوگو. آنچه مشتری در ویجت، بالای عنوان سفارش، میبیند.
- نشانی وبهوک (فقط https) و کلید مخفی وبهوک شما، که توکنهای رسید را هم امضا میکند.
- مدت اعتبار جلسه، یعنی TTL پیشفرض یک جلسه پرداخت.
- تبدیل خودکار، که پیشفرض خاموش است: هر ارزی را که دریافت میکنید به تتر (USDT) بفروشید تا فقط یک دارایی نگه دارید. این یک معامله واقعی در بازار است، نه یک تبدیل رایگان — پرداختها را ببینید.
پرداختهای تکمیلشده در یک کیفپول کسبوکارِ اختصاصی مینشینند که از موجودی اسپات و معاملاتی شخصی شما جداست. برای خرج کردن یا برداشت درآمدتان، آن را از کیفپول کسبوکار به کیفپول اصلی منتقل میکنید — فوری و بدون کارمزد — و بعد مثل همیشه معامله یا برداشت میکنید. پرداختها را ببینید.
۲. یک کلید API بسازید
در کنسول کسبوکار یک کلید بسازید. شکلش این است:
clpay_live_4f9d2c8a1b7e6f3d0a5c9b2e8f1a6d4c7b0e3f9a2c5d8b1e4f7a0c3d6b9e2f5aفقط یک بار نمایش داده میشود
کلید کامل تنها در لحظه ساخت نشان داده میشود. کوینلند فقط هش آن را ذخیره میکند، نه مقدارش، پس کلید گمشده قابل بازیابی نیست — آن را باطل کنید و کلید تازه بسازید. فقط پیشوند کلید نگه داشته میشود تا در کنسول بتوانید کلیدهایتان را از هم تشخیص دهید.
آن را همانطور نگه دارید که رمز عبور پایگاه داده را نگه میدارید: در سیستم مدیریت اسرار سرورتان، هرگز در یک مخزن کد، باندل مرورگر، یا فایل اجرایی اپلیکیشن موبایل. هر درخواست کلید را بهصورت توکن bearer حمل میکند:
Authorization: Bearer clpay_live_<64 hex>با یک فراخوانی هم درست کار کردن کلید را تأیید کنید و هم ببینید مجاز به قیمتگذاری در چه ارزهایی هستید:
curl https://my.coinlandexchange.com/api/pay/v1/me \
-H "Authorization: Bearer $COINLAND_PAY_KEY"{
"id": "8f1c9a34-3d2e-4b17-9f0a-2c6d5b8e4a71",
"display_name": "Example Store",
"display_name_fa": "فروشگاه نمونه",
"logo_url": "https://my.coinlandexchange.com/media/merchants/8f1c9a34.png",
"status": "active",
"accepted_currencies": ["usdt", "btc"],
"webhook_url": "https://example.com/webhooks/coinland"
}accepted_currencies را در زمان راهاندازی برنامه بخوانید و فهرست ارزها را در کد ثابت نکنید: این فهرست
در کنسول و بدون نیاز به انتشار نسخه جدید از سمت شما تغییر میکند.
۳. یک جلسه پرداخت بسازید
یک فراخوانی برای هر سفارش، از سرور خودتان، در همان لحظهای که مشتری پرداخت را انتخاب میکند.
const API = "https://my.coinlandexchange.com";
const apiKey = process.env.COINLAND_PAY_KEY!; // clpay_live_...
interface Amount {
currency: string;
amount: string; // decimal STRING, never a float
}
export async function createSession(order: {
id: string;
number: string;
amounts: Amount[];
}) {
const res = await fetch(`${API}/api/pay/v1/sessions`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
reference_id: order.id, // your order id, and your idempotency key
title: `Order ${order.number}`,
amounts: order.amounts,
return_url: "https://example.com/checkout/done",
cancel_url: "https://example.com/cart",
}),
});
if (!res.ok) {
const body = await res.json().catch(() => null);
const code = body?.errors?.error?.[0] ?? "UNKNOWN";
throw new Error(`coinland pay ${res.status}: ${code}`);
}
return res.json();
}{
"id": "3a7f21e8-9c04-4d6b-8e15-7b2a9f3c1d60",
"reference_id": "order-10492",
"status": "open",
"title": "Order 10492",
"description": "2 items",
"amounts": [
{ "currency": "usdt", "amount": "24.90" },
{ "currency": "btc", "amount": "0.00027" }
],
"checkout_url": "https://my.coinlandexchange.com/pay/3a7f21e8-9c04-4d6b-8e15-7b2a9f3c1d60",
"return_url": "https://example.com/checkout/done",
"cancel_url": "https://example.com/cart",
"metadata": { "cart_id": "c_88213" },
"payment": null,
"expires_at": "2026-08-11T12:30:00Z",
"created_at": "2026-08-11T12:00:00Z"
}سه نکته که ارزش دارد بار اول درست انجام شوند:
- مبالغ رشته اعشاری هستند.
"24.90"، نه24.9. یک عدد اعشاری شناور نمیتواند هر مبلغ دهدهی را دقیق نگه دارد، و خطای گردکردن در قیمت یعنی خطای گردکردن در آنچه دریافت میکنید. reference_idکلید ایدمپوتنسی شماست. ارسال مجدد با همان شناسه و همان محتوا، جلسه اصلی را برمیگرداند و جلسه دومی نمیسازد. جلسههای پرداخت را ببینید.session.idرا روی سفارشتان ذخیره کنید، پیش از آنکه مشتری را جایی بفرستید. این همان چیزی است که بعداً با آن وبهوک را به سفارش وصل میکنید.
۴. مشتری را به ویجت بفرستید
یا ویجت را در یک پنجره بازشو روی صفحه خودتان باز کنید، که تجربه بهتری است:
<script src="https://my.coinlandexchange.com/pay/v1.js"></script>
<button id="pay">پرداخت با کوینلند</button>
<script>
document.getElementById("pay").addEventListener("click", () => {
CoinlandPay.open({
sessionId: "3a7f21e8-9c04-4d6b-8e15-7b2a9f3c1d60",
onComplete({ payment_id }) {
window.location.href = `/checkout/done?payment_id=${payment_id}`;
},
onCancel() {
// مشتری ویجت را بست. جلسه تا زمان انقضا باز میماند، پس همان
// checkout_url دوباره کار میکند.
},
});
});
</script>یا بدون هیچ جاوااسکریپتی هدایت کنید:
302 Location: https://my.coinlandexchange.com/pay/3a7f21e8-9c04-4d6b-8e15-7b2a9f3c1d60بعد از پرداخت، مشتری روی return_url شما فرود میآید و ?receipt=<token>&payment_id=<id> به آن اضافه
شده است. جزئیات کامل، از جمله حالت بستهشدن پنجره بازشو، در ویجت است.
بازگشت به return_url تأییدیه پرداخت نیست
هدایت کاری است که مرورگر انجام میدهد. میتواند با بستن تب، شبکه ناپایدار یا خاموش شدن گوشی از دست برود، و میتواند دستی توسط کسی تایپ شود که هرگز پرداختی نکرده است. صفحه فرود را نشانهای برای نمایش حالت انتظار و استعلام بگیرید، نه اثبات. تحویل در مرحله ۵ انجام میشود.
۵. وبهوک را دریافت کنید و سفارش را تحویل دهید
کوینلند به نشانی ثبتشده شما درخواست POST میفرستد که با کلید مخفی وبهوک شما امضا شده است:
{
"event_id": "d41f8c62-5a10-4e93-b7d8-0c2a5f6e1b34",
"type": "payment.completed",
"session_id": "3a7f21e8-9c04-4d6b-8e15-7b2a9f3c1d60",
"reference_id": "order-10492",
"payment_id": "b92e4d17-6c38-4a05-9f2b-1e7d3c8a5049",
"status": "completed"
}امضا را بررسی کنید، بر اساس event_id تکراریها را حذف کنید، سپس رکورد معتبر را بخوانید و تحویل دهید:
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.COINLAND_PAY_WEBHOOK_SECRET;
// بدنه خام همان چیزی است که امضا شده. هر پارسر JSON که آن را دوباره سریالایز
// کند امضا را خراب میکند، پس پارس کردن بعد از بررسی انجام میشود، نه قبل.
app.post(
"/webhooks/coinland",
express.raw({ type: "application/json" }),
async (req, res) => {
const header = req.get("x-pay-signature") ?? "";
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=").map((s) => s.trim())),
);
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!Number.isFinite(age) || age > 300) return res.sendStatus(400);
const expected = crypto
.createHmac("sha256", SECRET)
.update(`${parts.t}.${req.body.toString("utf8")}`)
.digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(parts.v1 ?? "", "utf8");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.sendStatus(400);
}
// امضا درست است. سریع پاسخ بدهید و بعد کار را انجام دهید: یک هندلر کند
// هندلری است که دوباره فراخوانی میشود.
res.sendStatus(200);
try {
const event = JSON.parse(req.body.toString("utf8"));
if (await alreadyHandled(event.event_id)) return;
if (event.type !== "payment.completed") return;
// محتوای وبهوک یک سرنخ است. این مرجع است.
const payment = await fetch(
`https://my.coinlandexchange.com/api/pay/v1/payments/${event.payment_id}`,
{ headers: { Authorization: `Bearer ${process.env.COINLAND_PAY_KEY}` } },
).then((r) => r.json());
await fulfilOrder(payment.reference_id, {
currency: payment.currency,
amount: payment.amount, // آنچه مشتری پرداخت کرد
net: payment.net_amount, // آنچه به حساب شما نشست
receiptNo: payment.receipt_no,
});
await markHandled(event.event_id);
} catch (err) {
// شما همین حالا ۲۰۰ پاسخ دادهاید، پس کوینلند دوباره تلاش نمیکند.
// خطا را لاگ کنید و بگذارید چرخه مغایرتگیری روزانهتان سفارش را پیدا کند.
console.error("coinland pay fulfilment failed", err);
}
},
);این یک یکپارچهسازی کامل است. وبهوکها تلاشهای مجدد، رویداد session.expired و پیامد
پاسخ غیر ۲xx از سمت شما را پوشش میدهد.
پیش از انتشار چه چیزی را بررسی کنید
- نشانی وبهوک شما https است و از اینترنت عمومی قابل دسترسی است.
- هندلر شما در چند ثانیه پاسخ ۲xx میدهد و کارش را بعد از آن انجام میدهد.
- بر اساس
event_idتکراریها را حذف میکنید، چون ارسال مجدد یک رفتار عادی است نه خطا. - تحویل را بر پایه وبهوک یا API انجام میدهید، هرگز فقط بر پایه
return_url. - کلید شما فقط سمت سرور است.
فهرست کامل در آماده انتشار است.