15 KiB
راهنمای اتصال پرداخت خارجی (Partner Integration)
این سند برای تیم فنی سایتهایی است که میخواهند کاربر را به Montify بفرستند، پرداخت را از درگاه انجام دهند، و نتیجه را امن دریافت کنند.
۱. خلاصه جریان
سایت شما (سرور)
│ POST /external-payment/initiate (امضای HMAC)
▼
Montify → payment_url
│
▼ redirect کاربر
/pay/external/{token} → درگاه پرداخت
│
▼ برگشت از درگاه
Montify verify
│
├─► redirect مرورگر به return_url شما (با signature)
└─► POST webhook به سرور شما (با signature)
نکات مهم:
- کاربر نهایی نیاز به لاگین در Montify ندارد.
- مبلغ و آدرس برگشت فقط از سرور شما (با امضا) پذیرفته میشود؛ لینک دستی/جعلی کار نمیکند.
client_secretفقط روی سرور شما بماند؛ هرگز در فرانتاند قرار ندهید.
۲. پیشنیازها
از ادمین Montify این اطلاعات را دریافت کنید:
| مقدار | توضیح |
|---|---|
BASE_URL |
آدرس API نود، مثلاً https://node.example.com |
APP_URL |
آدرس فرانت Montify (برای payment_url) |
client_id |
شناسه partner شما |
client_secret |
رمز مخفی (فقط یکبار نمایش داده میشود) |
همچنین این موارد باید در پنل partner ثبت شده باشد:
allowed_return_urls— پیشوند آدرسهای مجاز برگشت (مثلاًhttps://yoursite.com/payment/)webhook_url— اختیاری؛ آدرس پیشفرض webhook شماallowed_ips— اختیاری؛ اگر پر باشد فقط همان IPها مجازند
۳. احراز هویت درخواستهای سرور به سرور
برای initiate و status این هدرها الزامی است:
| Header | توضیح |
|---|---|
X-Client-Id |
همان client_id |
X-Timestamp |
Unix time به ثانیه |
X-Nonce |
یکتا برای هر درخواست (مثلاً UUID) |
X-Signature |
HMAC-SHA256 روی canonical string |
Content-Type |
application/json |
۳.۱. ساخت Canonical String
METHOD
PATH
BODY_HASH
TIMESTAMP
NONCE
با \n بین خطوط. بدون \n انتهایی.
METHOD: حروف بزرگ (POSTیاGET)PATH: فقط path بدون query، مثلاً/external-payment/initiateBODY_HASH:SHA256ازstableStringify(body)به صورت hex
برایGETبدون body: hash روی رشته خالی""TIMESTAMPوnonce: همان مقادیر هدر
۳.۲. stableStringify
قبل از hash کردن body، JSON را با کلیدهای مرتبشده بسازید:
- object: کلیدها به ترتیب الفبایی
- array: ترتیب حفظ شود
- null / number / string / boolean: مثل
JSON.stringify
مثال:
{"amount":1000,"gateway":"zarinpall","reference_id":"ORD-1","return_url":"https://yoursite.com/ok"}
نه:
{"return_url":"...","amount":1000,...}
۳.۳. Signature
X-Signature = HMAC_SHA256(canonical, client_secret) → hex
۳.۴. محدودیتها
- اختلاف ساعت مجاز: ±۵ دقیقه
- هر
nonceفقط یکبار قابل استفاده است (ضد replay)
۴. APIها
۴.۱. شروع پرداخت — POST /external-payment/initiate
Auth: HMAC (الزامی)
Body:
| فیلد | نوع | الزامی | توضیح |
|---|---|---|---|
amount |
number | بله | مبلغ به ریال (حداقل ۱۰۰۰) |
reference_id |
string | بله | شناسه سفارش شما (یکتا در وضعیت فعال) |
gateway |
string | بله | یکی از: zarinpall ، zibal ، payping ، sepal |
return_url |
string | بله | باید با یکی از allowed_return_urls شروع شود |
webhook_url |
string | خیر | اگر نباشد از webhook پیشفرض partner استفاده میشود |
description |
string | خیر | توضیح |
metadata |
object | خیر | داده دلخواه شما |
نمونه درخواست:
POST /external-payment/initiate
Content-Type: application/json
X-Client-Id: ptr_xxxxxxxx
X-Timestamp: 1720000000
X-Nonce: 550e8400-e29b-41d4-a716-446655440000
X-Signature: <hex>
{
"amount": 500000,
"reference_id": "ORDER-1001",
"gateway": "zarinpall",
"return_url": "https://yoursite.com/payment/result",
"webhook_url": "https://yoursite.com/api/payment/webhook",
"description": "پرداخت سفارش ۱۰۰۱"
}
پاسخ موفق 201:
{
"session_id": "a1b2c3...",
"payment_url": "https://shop.example.com/pay/external/a1b2c3...",
"expires_at": "2026-07-21T10:15:00.000Z",
"reference_id": "ORDER-1001"
}
کاربر را به payment_url هدایت کنید. جلسه حدود ۱۵ دقیقه اعتبار دارد.
خطاهای رایج:
| error | معنی |
|---|---|
invalid_signature |
امضا اشتباه است |
expired_timestamp |
ساعت سرور نامعتبر |
replay_detected |
nonce تکراری |
invalid_return_url |
return_url در whitelist نیست |
invalid_gateway |
درگاه نامعتبر |
amount_too_low |
مبلغ کمتر از ۱۰۰۰ ریال |
duplicate_reference |
reference_id قبلاً در وضعیت فعال استفاده شده |
gateway_not_configured |
درگاه در Montify پیکربندی نشده |
۴.۲. استعلام وضعیت — GET /external-payment/status
Auth: HMAC (الزامی)
یکی از queryها لازم است:
?reference_id=ORDER-1001?session_id=<session_id>
پاسخ نمونه:
{
"token": "a1b2c3...",
"reference_id": "ORDER-1001",
"amount": 500000,
"gateway": "zarinpall",
"status": "paid",
"track_number": "123456789",
"expires_at": "...",
"paid_at": "...",
"created_at": "..."
}
وضعیتها (status):
| status | معنی |
|---|---|
pending |
ساخته شده، هنوز به درگاه نرفته |
processing |
به درگاه هدایت شده |
paid |
پرداخت موفق |
failed |
ناموفق |
expired |
منقضی |
cancelled |
لغو |
منبع حقیقت نهایی برای علامتگذاری سفارش: Webhook یا استعلام status. فقط به redirect مرورگر اکتفا نکنید.
۵. برگشت کاربر (Browser Redirect)
بعد از پرداخت، Montify کاربر را به return_url شما میفرستد با queryهای امضاشده:
https://yoursite.com/payment/result?
status=success
&reference_id=ORDER-1001
&session_id=a1b2c3...
&amount=500000
&track_number=123456789
×tamp=1720000123
&signature=<hex>
| پارامتر | توضیح |
|---|---|
status |
success یا failed |
reference_id |
همان شناسه شما |
session_id |
توکن جلسه |
amount |
مبلغ (ریال) |
track_number |
کد پیگیری درگاه (ممکن است خالی باشد) |
timestamp |
زمان امضا |
signature |
HMAC روی payload |
تأیید امضای redirect
- همه پارامترها بهجز
signatureرا بگیرید. - یک object بسازید (مقادیر string هستند).
stableStringifyکنید.HMAC-SHA256(..., client_secret)بگیرید و باsignatureمقایسه کنید.timestampرا چک کنید (±۵ دقیقه توصیه میشود).- مبلغ و
reference_idرا با سفارش خودتان تطبیق دهید.
۶. Webhook سرور به سرور
Montify بعد از پرداخت موفق یک POST به webhook_url میزند.
Headers:
Content-Type: application/json
X-Client-Id: ptr_xxxxxxxx
X-Timestamp: 1720000123
X-Signature: <hex>
Body:
{
"status": "success",
"reference_id": "ORDER-1001",
"session_id": "a1b2c3...",
"amount": 500000,
"track_number": "123456789",
"gateway": "zarinpall",
"timestamp": "1720000123"
}
تأیید امضای webhook
X-Signature = HMAC-SHA256(stableStringify(body), client_secret)
پاسخ 2xx برگردانید. در غیر این صورت ممکن است ارسال ناموفق ثبت شود.
Webhook فقط وقتی session به
paidبرسد ارسال میشود.
۷. نمونه کد Node.js
const crypto = require('crypto');
const axios = require('axios');
const BASE_URL = 'https://node.example.com';
const CLIENT_ID = process.env.MONTIFY_CLIENT_ID;
const CLIENT_SECRET = process.env.MONTIFY_CLIENT_SECRET;
function stableStringify(obj) {
if (obj === null || typeof obj !== 'object') return JSON.stringify(obj);
if (Array.isArray(obj)) return `[${obj.map(stableStringify).join(',')}]`;
const keys = Object.keys(obj).sort();
return `{${keys.map((k) => `"${k}":${stableStringify(obj[k])}`).join(',')}}`;
}
function signRequest(method, path, body) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomUUID();
const bodyString = body ? stableStringify(body) : '';
const bodyHash = crypto.createHash('sha256').update(bodyString).digest('hex');
const canonical = [method.toUpperCase(), path, bodyHash, timestamp, nonce].join('\n');
const signature = crypto.createHmac('sha256', CLIENT_SECRET).update(canonical).digest('hex');
return {
'Content-Type': 'application/json',
'X-Client-Id': CLIENT_ID,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
};
}
async function initiatePayment({ amount, referenceId, gateway, returnUrl, webhookUrl, description }) {
const path = '/external-payment/initiate';
const body = {
amount,
reference_id: referenceId,
gateway,
return_url: returnUrl,
webhook_url: webhookUrl,
description,
};
const headers = signRequest('POST', path, body);
const { data } = await axios.post(`${BASE_URL}${path}`, body, { headers });
return data; // { session_id, payment_url, expires_at, reference_id }
}
function verifyReturnSignature(query) {
const { signature, ...rest } = query;
const expected = crypto
.createHmac('sha256', CLIENT_SECRET)
.update(stableStringify(rest))
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'));
}
function verifyWebhook(body, signatureHeader) {
const expected = crypto
.createHmac('sha256', CLIENT_SECRET)
.update(stableStringify(body))
.digest('hex');
const a = Buffer.from(signatureHeader, 'hex');
const b = Buffer.from(expected, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// مثال استفاده:
// const result = await initiatePayment({
// amount: 500000,
// referenceId: 'ORDER-1001',
// gateway: 'zarinpall',
// returnUrl: 'https://yoursite.com/payment/result',
// webhookUrl: 'https://yoursite.com/api/payment/webhook',
// description: 'خرید سفارش',
// });
// res.redirect(result.payment_url);
۸. نمونه کد PHP
<?php
function stable_stringify($obj) {
if (is_null($obj) || !is_array($obj)) {
return json_encode($obj, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
// تشخیص array لیستی vs associative
if (array_is_list($obj)) {
$parts = array_map('stable_stringify', $obj);
return '[' . implode(',', $parts) . ']';
}
ksort($obj);
$parts = [];
foreach ($obj as $k => $v) {
$parts[] = '"' . $k . '":' . stable_stringify($v);
}
return '{' . implode(',', $parts) . '}';
}
function sign_request(string $method, string $path, ?array $body, string $clientId, string $secret): array {
$timestamp = (string) time();
$nonce = bin2hex(random_bytes(16));
$bodyString = $body ? stable_stringify($body) : '';
$bodyHash = hash('sha256', $bodyString);
$canonical = strtoupper($method) . "\n" . $path . "\n" . $bodyHash . "\n" . $timestamp . "\n" . $nonce;
$signature = hash_hmac('sha256', $canonical, $secret);
return [
'Content-Type: application/json',
'X-Client-Id: ' . $clientId,
'X-Timestamp: ' . $timestamp,
'X-Nonce: ' . $nonce,
'X-Signature: ' . $signature,
];
}
function initiate_payment(array $payload, string $baseUrl, string $clientId, string $secret): array {
$path = '/external-payment/initiate';
$headers = sign_request('POST', $path, $payload, $clientId, $secret);
$ch = curl_init(rtrim($baseUrl, '/') . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
CURLOPT_RETURNTRANSFER => true,
]);
$raw = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($raw, true);
if ($code >= 400) {
throw new RuntimeException($data['message'] ?? 'initiate failed');
}
return $data;
}
function verify_return_signature(array $query, string $secret): bool {
$signature = $query['signature'] ?? '';
unset($query['signature']);
$expected = hash_hmac('sha256', stable_stringify($query), $secret);
return hash_equals($expected, $signature);
}
۹. چکلیست پیادهسازی سایت شما
client_id/client_secretرا فقط در env سرور نگه دارید.- قبل از پرداخت، سفارش خود را با وضعیت «در انتظار پرداخت» ذخیره کنید.
- از سرور
initiateبزنید و کاربر را بهpayment_urlبفرستید. - صفحه
return_urlرا بسازید و signature را verify کنید. - endpoint webhook بسازید و signature را verify کنید؛ سفارش را آنجا
paidکنید. - برای اطمینان، در صورت نیاز
GET /statusهم بزنید (idempotent). reference_idرا یکتا و قابل ردیابی نگه دارید.return_urlرا دقیقاً زیر پیشوند whitelist ثبتشده بفرستید.
۱۰. امنیت — خلاصه قوانین
| کار درست | کار غلط |
|---|---|
| initiate فقط از سرور | ساخت لینک پرداخت در مرورگر کاربر |
امضا با client_secret |
گذاشتن secret در JS فرانت |
| verify امضای redirect و webhook | اعتماد کور به query بدون signature |
تطبیق amount با سفارش |
قبول هر مبلغ برگشتی |
whitelist برای return_url |
قبول هر URL دلخواه |
۱۱. پشتیبانی
در صورت خطای مداوم invalid_signature:
- ساعت سرور (NTP) را چک کنید.
stableStringifyو ترتیب کلیدها را بررسی کنید.PATHدقیقاً/external-payment/initiateباشد (بدون domain و بدون slash اضافه).- body ارسالی دقیقاً همان چیزی باشد که hash شده (نه serialize متفاوت axios/curl).
برای فعالسازی partner جدید یا افزودن دامنه به whitelist با ادمین Montify هماهنگ کنید.