مستندات SamanPay API
API-first payment infrastructure — نسخه v1
Base URL
https://api.payiran.org/v1
Authentication — HMAC v2
درخواستها با API Key و Secret امضا میشوند. Timestamp حداکثر ۵ دقیقه اختلاف دارد و Nonce فقط یکبار قابل استفاده است.
X-PIS-Key: YOUR_KEY
X-PIS-Timestamp: 1791400000
X-PIS-Nonce: UNIQUE_RANDOM_8_TO_128_CHARS
X-PIS-Signature-Version: 2
X-PIS-Signature: HEX_HMAC_SHA256
body_hash = SHA256(raw_body)
canonical = timestamp + "\n" + nonce + "\n" + METHOD + "\n" + PATH + "\n" + body_hash
signature = HMAC_SHA256(canonical, API_SECRET)
PATH شامل اسلش ابتدایی است؛ مثال: /v1/payments. Query String داخل امضا قرار نمیگیرد؛ مثلاً برای /v1/transactions?status=1 فقط /v1/transactions امضا میشود. Signature v1 قدیمی نیز برای سازگاری پشتیبانی میشود.
Request ID و Rate Limit
میتوانید X-Request-ID بفرستید؛ در غیر این صورت SamanPay تولید میکند. پاسخ همیشه همان ID را در Header و JSON برمیگرداند.
X-Request-ID: req_...
X-PayIran-API-Version: v1
X-RateLimit-Limit: 120
ساخت پرداخت
POST /v1/payments
Idempotency-Key: payment-order-123
{
"order_id": "ORDER-123",
"amount": 2500000,
"card": "6037991234567890",
"callback": "https://api.example.com/payiran/webhook",
"return_url": "https://shop.example.com/payment/result"
}
Idempotency-Key اجباری است و ۲۴ ساعت نگهداری میشود. Retry با همان Key و همان Payload همان Response را برمیگرداند و پرداخت جدید نمیسازد.
HTTP 201
{
"ok": true,
"data": {
"payment_id": "01J...",
"order_id": "ORDER-123",
"amount": 2500000,
"status_code": 0,
"status": "pending",
"payment_url": "https://checkout.payiran.org/pay/..."
},
"request_id": "req_..."
}
وضعیت و Verify
GET /v1/payments/{order_id}
POST /v1/payments/verify
{"order_id":"ORDER-123"}
مقادیر وضعیت:
| Code | status | معنی |
| 0 | pending | در انتظار |
| 1 | succeeded | موفق |
| 2 | failed | ناموفق/منقضی |
| 3 | canceled | لغوشده |
| 4 | under_review | نیازمند بررسی؛ مثل عدم تطابق کارت |
| 6 | rejected | ردشده |
| 7 | blocked_card | کارت مسدود |
| 8 | blocked_ip | IP مسدود |
تراکنشها
GET /v1/transactions?status=1&from=2026-10-01&to=2026-10-07&min_amount=100000&per_page=50
Filterهای پشتیبانیشده: status, order_id, reference, environment, from, to, min_amount, max_amount, per_page.
Balance و حسابداری
GET /v1/balance
GET /v1/daily-summary?date=2026-10-07
GET /v1/settlements
Balance شامل فروش ناخالص، کارمزد، خالص، مبلغ تسویهشده، مانده Ledger، درخواستهای در انتظار و موجودی قابل درخواست است.
Payout / Settlement
GET /v1/payout-accounts
POST /v1/payout-accounts
DELETE /v1/payout-accounts/{id}
POST /v1/payout-accounts
{"type":"usdt_trc20","label":"Main wallet","address":"T..."}
GET /v1/payout-requests
POST /v1/payout-requests
{"method":"usdt_trc20","amount_toman":50000000,"payout_account_id":12}
شبکههای فعلی: usdt_trc20 و usdt_bep20. برای تسویه ریالی مقدار method=irt ارسال میشود.
Webhook v1
برای پرداختهایی که از /v1/payments ساخته شدهاند، Webhook بهصورت POST JSON ارسال میشود.
X-PayIran-Event-ID: evt_...
X-PayIran-Event: payment.succeeded
X-PayIran-Timestamp: 1791400000
X-PayIran-Signature: sha256=...
signature = HMAC_SHA256(
timestamp + "\n" + event_id + "\n" + raw_json_body,
WEBHOOK_SECRET
)
{
"id": "evt_...",
"type": "payment.under_review",
"created_at": "...",
"data": {
"order_id": "ORDER-123",
"status": "under_review",
"status_code": 4,
"expected_card": "603799******7890",
"paid_card": "621986******4312",
"card_match": false,
"review_reason": "card_mismatch"
}
}
Webhook Secret از API Key مستقل است و از بخش توسعهدهندگان پذیرنده قابل ساخت/تعویض است.
Retry خودکار تا ۸ مرتبه با Backoff انجام میشود. Event ID در Retry ثابت میماند؛ سمت Merchant باید Webhook را با Event ID بهصورت idempotent پردازش کند.
GET /v1/webhooks/deliveries?order_id=ORDER-123
POST /v1/webhooks/retry
{"order_id":"ORDER-123"}
Sandbox
POST /v1/sandbox/payments
{
"order_id":"SANDBOX-001",
"amount":100000,
"card":"6037991234567890",
"paid_card":"6219861111111111",
"callback":"https://api.example.com/payiran/webhook",
"scenario":"card_mismatch"
}
سناریوها: success, card_mismatch, failed, canceled, pending. تراکنش Sandbox وارد حسابداری و تسویه واقعی نمیشود ولی Webhook واقعی v1 ارسال میکند.
Error Contract
{
"ok": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Requested payout exceeds available balance."
},
"request_id": "req_..."
}
کدهای متداول: INVALID_SIGNATURE, REPLAY_DETECTED, TIMESTAMP_OUT_OF_RANGE, INSUFFICIENT_SCOPE, IDEMPOTENCY_CONFLICT, DUPLICATE_ORDER_ID, PAYMENT_NOT_FOUND, CARD_BLOCKED, INVALID_PAYOUT_ACCOUNT, INSUFFICIENT_BALANCE, GATEWAY_UNAVAILABLE.
نمونه کد SDK
برای Production بهتر است منطق امضا را در یک Client مشترک نگه دارید؛ Timestamp و Nonce برای هر درخواست باید جدید باشند.
PHP
<?php
$method='GET';
$path='/v1/balance';
$body='';
$ts=(string)time();
$nonce=bin2hex(random_bytes(12));
$bodyHash=hash('sha256',$body);
$canonical=$ts."\n".$nonce."\n".$method."\n".$path."\n".$bodyHash;
$signature=hash_hmac('sha256',$canonical,$apiSecret);
$headers=[
'X-PIS-Key: '.$apiKey,
'X-PIS-Timestamp: '.$ts,
'X-PIS-Nonce: '.$nonce,
'X-PIS-Signature-Version: 2',
'X-PIS-Signature: '.$signature,
'Accept: application/json',
];
Node.js
import crypto from 'node:crypto';
function sign(method, path, body, apiSecret) {
const timestamp = Math.floor(Date.now()/1000).toString();
const nonce = crypto.randomBytes(12).toString('hex');
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
const canonical = [timestamp, nonce, method.toUpperCase(), path, bodyHash].join('\n');
const signature = crypto.createHmac('sha256', apiSecret).update(canonical).digest('hex');
return { timestamp, nonce, signature };
}
Python
import time, secrets, hashlib, hmac
def sign(method, path, body, api_secret):
timestamp = str(int(time.time()))
nonce = secrets.token_hex(12)
body_hash = hashlib.sha256(body.encode()).hexdigest()
canonical = "\n".join([timestamp, nonce, method.upper(), path, body_hash])
signature = hmac.new(api_secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
return timestamp, nonce, signature