SamanPay سه‌شنبه ۵ آبان ۱۴۰۵

مستندات 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"}

مقادیر وضعیت:

Codestatusمعنی
0pendingدر انتظار
1succeededموفق
2failedناموفق/منقضی
3canceledلغوشده
4under_reviewنیازمند بررسی؛ مثل عدم تطابق کارت
6rejectedردشده
7blocked_cardکارت مسدود
8blocked_ipIP مسدود

تراکنش‌ها

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