479 lines
15 KiB
Markdown
479 lines
15 KiB
Markdown
# راهنمای اتصال پرداخت خارجی (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/initiate`
|
|||
|
|
- `BODY_HASH`: `SHA256` از `stableStringify(body)` به صورت hex
|
|||
|
|
برای `GET` بدون body: hash روی رشته خالی `""`
|
|||
|
|
- `TIMESTAMP` و `nonce`: همان مقادیر هدر
|
|||
|
|
|
|||
|
|
### ۳.۲. stableStringify
|
|||
|
|
|
|||
|
|
قبل از hash کردن body، JSON را با کلیدهای **مرتبشده** بسازید:
|
|||
|
|
|
|||
|
|
- object: کلیدها به ترتیب الفبایی
|
|||
|
|
- array: ترتیب حفظ شود
|
|||
|
|
- null / number / string / boolean: مثل `JSON.stringify`
|
|||
|
|
|
|||
|
|
مثال:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{"amount":1000,"gateway":"zarinpall","reference_id":"ORD-1","return_url":"https://yoursite.com/ok"}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
نه:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{"return_url":"...","amount":1000,...}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ۳.۳. Signature
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
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 | خیر | داده دلخواه شما |
|
|||
|
|
|
|||
|
|
**نمونه درخواست:**
|
|||
|
|
|
|||
|
|
```http
|
|||
|
|
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`:**
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"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>`
|
|||
|
|
|
|||
|
|
**پاسخ نمونه:**
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"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
|
|||
|
|
|
|||
|
|
1. همه پارامترها بهجز `signature` را بگیرید.
|
|||
|
|
2. یک object بسازید (مقادیر string هستند).
|
|||
|
|
3. `stableStringify` کنید.
|
|||
|
|
4. `HMAC-SHA256(..., client_secret)` بگیرید و با `signature` مقایسه کنید.
|
|||
|
|
5. `timestamp` را چک کنید (±۵ دقیقه توصیه میشود).
|
|||
|
|
6. مبلغ و `reference_id` را با سفارش خودتان تطبیق دهید.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ۶. Webhook سرور به سرور
|
|||
|
|
|
|||
|
|
Montify بعد از پرداخت موفق یک `POST` به `webhook_url` میزند.
|
|||
|
|
|
|||
|
|
**Headers:**
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Content-Type: application/json
|
|||
|
|
X-Client-Id: ptr_xxxxxxxx
|
|||
|
|
X-Timestamp: 1720000123
|
|||
|
|
X-Signature: <hex>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Body:**
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"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
|
|||
|
|
|
|||
|
|
```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
|
|||
|
|
<?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);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ۹. چکلیست پیادهسازی سایت شما
|
|||
|
|
|
|||
|
|
1. `client_id` / `client_secret` را فقط در env سرور نگه دارید.
|
|||
|
|
2. قبل از پرداخت، سفارش خود را با وضعیت «در انتظار پرداخت» ذخیره کنید.
|
|||
|
|
3. از سرور `initiate` بزنید و کاربر را به `payment_url` بفرستید.
|
|||
|
|
4. صفحه `return_url` را بسازید و signature را verify کنید.
|
|||
|
|
5. endpoint webhook بسازید و signature را verify کنید؛ سفارش را آنجا `paid` کنید.
|
|||
|
|
6. برای اطمینان، در صورت نیاز `GET /status` هم بزنید (idempotent).
|
|||
|
|
7. `reference_id` را یکتا و قابل ردیابی نگه دارید.
|
|||
|
|
8. `return_url` را دقیقاً زیر پیشوند whitelist ثبتشده بفرستید.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ۱۰. امنیت — خلاصه قوانین
|
|||
|
|
|
|||
|
|
| کار درست | کار غلط |
|
|||
|
|
|----------|---------|
|
|||
|
|
| initiate فقط از سرور | ساخت لینک پرداخت در مرورگر کاربر |
|
|||
|
|
| امضا با `client_secret` | گذاشتن secret در JS فرانت |
|
|||
|
|
| verify امضای redirect و webhook | اعتماد کور به query بدون signature |
|
|||
|
|
| تطبیق `amount` با سفارش | قبول هر مبلغ برگشتی |
|
|||
|
|
| whitelist برای `return_url` | قبول هر URL دلخواه |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ۱۱. پشتیبانی
|
|||
|
|
|
|||
|
|
در صورت خطای مداوم `invalid_signature`:
|
|||
|
|
|
|||
|
|
1. ساعت سرور (NTP) را چک کنید.
|
|||
|
|
2. `stableStringify` و ترتیب کلیدها را بررسی کنید.
|
|||
|
|
3. `PATH` دقیقاً `/external-payment/initiate` باشد (بدون domain و بدون slash اضافه).
|
|||
|
|
4. body ارسالی دقیقاً همان چیزی باشد که hash شده (نه serialize متفاوت axios/curl).
|
|||
|
|
|
|||
|
|
برای فعالسازی partner جدید یا افزودن دامنه به whitelist با ادمین Montify هماهنگ کنید.
|