API REFERENCE

مستندات API

هر چیزی که برای وصل‌کردن یک فروشگاه لازم است: ساخت پرداخت، دریافت پیامک بانک، بررسی وب‌هوک و معنای هر کد خطا.

شروع سریع

آدرس پایه: https://steve-gate.ir — همین آدرسی که این صفحه روی آن باز شده است. همه پاسخ‌ها JSON هستند و هر پاسخ، موفق یا ناموفق، هدر X-Request-Id دارد. اگر خطایی دیدید که خودتان حل نکردید، همین شناسه را برای پشتیبانی بفرستید: با آن، لاگ و رکورد ممیزی همان درخواست پیدا می‌شود.

چهار کار تا اولین پرداخت تأییدشده فاصله دارید. ترتیب مهم است: پیامک بانک شاهدِ پرداخت است، پس تا فورواردر وصل نشود هیچ فاکتوری خودکار تأیید نمی‌شود.

  1. ۰۱

    حساب پذیرنده بسازید

    در صفحه ثبت‌نام شماره موبایل و گذرواژه را وارد کنید. حساب در وضعیت «در انتظار تأیید» ساخته می‌شود و تا تأیید مدیر، درخواست‌های API با کد ACCOUNT_PENDING_APPROVAL رد می‌شوند.

  2. ۰۲

    کلید API بگیرید

    پس از تأیید، از پنل پذیرنده → کلیدهای API کلید بسازید. کلید یک‌بار و فقط یک‌بار نشان داده می‌شود؛ فقط هش آن ذخیره است. اگر گم شد، همان کلید را بچرخانید.

  3. ۰۳

    کارت مقصد را ثبت کنید

    در پنل پذیرنده → کارت‌ها شماره کارتی که پول به آن واریز می‌شود را اضافه کنید. بدون کارت فعال، ساخت فاکتور با کد CARD_REQUIRED رد می‌شود. شماره کارت مشتری هرگز ذخیره نمی‌شود.

  4. ۰۴

    فورواردر پیامک را وصل کنید

    روی گوشی اندرویدی که پیامک بانک را می‌گیرد، برنامه فورواردر را به POST /sms وصل کنید — راهنمای کامل. تا این مرحله انجام نشود، مبلغ واریز می‌شود ولی فاکتور تأیید نمی‌شود.

احراز هویت

کلید را در هدر X-API-Key بفرستید. هدر Authorization: Bearer <key> هم پذیرفته می‌شود، چون بعضی برنامه‌های فورواردر فقط یکی از این دو را می‌توانند تنظیم کنند.

هدرالزامتوضیح
X-API-Keyبلهکلید کامل با پیشوند محیط: sk_live_… یا sk_test_…
Content-Typeبلهapplication/json
Idempotency-Keyبرای پرداخت توصیه‌شدههر رشته یکتا با حداکثر ۲۵۵ نویسه — کلید یکتاسازی.
AuthorizationجانشینBearer sk_live_…

شکل کلید

کلید از پیشوند محیط، یک شناسه ۱۲ نویسه‌ای و یک راز ۳۲ نویسه‌ای ساخته می‌شود: sk_live_ به‌علاوه ۴۴ نویسه. کلید کامل تنها در لحظه ساخت نمایش داده می‌شود و در پایگاه‌داده فقط HMAC آن با یک فلفل نگه داشته می‌شود — یعنی حتی مدیر سامانه هم نمی‌تواند کلید شما را بخواند.

اگر کلید در جای عمومی منتشر شد، در پنل روی «چرخش کلید» بزنید. چرخش در یک عملیات، کلید تازه می‌سازد و کلید قدیمی را باطل می‌کند، پس هیچ پنجره‌ای با دو کلید زنده باقی نمی‌ماند.

دو محیط

پیشوندمحیطاثر
sk_live_عملیاتیفاکتور واقعی می‌سازد و کیف پول را کم می‌کند.
sk_test_آزمایشیفاکتور با testMode: true ساخته می‌شود، کیف پول دست نمی‌خورد، و پیامک آزمایشی هیچ فاکتور عملیاتی را تأیید نمی‌کند.
فرستادن کلید آزمایشی به مسیر عملیاتی، یا برعکس، با کد API_KEY_ENVIRONMENT_MISMATCH رد می‌شود. عوض‌کردن پیشوند کلید تفاوتی در آن پاسخ ایجاد نمی‌کند؛ چرا که بررسی روی محیط ذخیره‌شده انجام می‌شود، نه روی متن کلید.

دسترسی‌ها

هر کلید فهرست دسترسی مشخصی دارد. اگر کلیدی دسترسی یک اندپوینت را نداشته باشد، پاسخ INSUFFICIENT_SCOPE با کد ۴۰۳ است.

دسترسیکدام اندپوینت
payments:createPOST /api/v1/payments
payments:readGET /api/v1/status
transactions:readGET /api/v1/transactions/count
wallet:readGET /api/v1/wallet
cards:readGET /api/v1/cards
sms:writePOST /sms

هر کلید می‌تواند فهرست IP مجاز داشته باشد. اگر فهرست خالی نباشد، درخواست از هر IP دیگری با IP_NOT_ALLOWED رد می‌شود — و این بررسی پیش از هر کار دیگری انجام می‌شود.

ساخت پرداخت

POST /api/v1/payments payments:create

یک فاکتور می‌سازد و مبلغی یکتا برای آن رزرو می‌کند. پاسخ شامل آدرس صفحه پرداخت است که باید به مشتری بدهید.

REQUESTapplication/json
{
  "amount": 359000,                    // تومان — مبلغ سفارش، عدد صحیح
  "description": "سفارش ۱۲۳۴",          // روی صفحه پرداخت دیده می‌شود
  "customCallback": "https://shop.example.com/pay/cb",
  "returnUrl": "https://shop.example.com/orders/1234",
  "metadata": { "orderId": "1234" },      // در وب‌هوک برمی‌گردد
  "expiresInMinutes": 30,
  "feeMode": "CUSTOMER",               // چه کسی کارمزد را می‌دهد
  "cardId": "card_01J8XK..."           // اختیاری
}

فیلدها

فیلدالزامتوضیح
amountبلهعدد صحیح به تومان. حداقل ۱٬۰۰۰ و حداکثر ۵۰۰٬۰۰۰٬۰۰۰ به‌صورت پیش‌فرض. عدد اعشاری یا رشته پذیرفته نمی‌شود.
currencyخیرفقط IRT. واحد دیگر با CURRENCY_NOT_SUPPORTED رد می‌شود.
descriptionخیرروی صفحه پرداخت به مشتری نشان داده می‌شود.
customCallbackخیرآدرس وب‌هوک فقط برای همین پرداخت. در محیط عملیاتی باید https باشد.
returnUrlخیرپس از پرداخت مشتری به این آدرس برمی‌گردد. باید روی دامنه ثبت‌شده خودتان باشد.
metadataخیرداده دلخواه کلید/مقدار؛ دست‌نخورده در وب‌هوک برمی‌گردد.
expiresInMinutesخیربین ۱۵ و ۶۰ دقیقه. خارج از این بازه INVALID_EXPIRY.
feeModeخیرCUSTOMER (پیش‌فرض) یا MERCHANT. تعیین می‌کند کارمزد را چه کسی می‌دهد، نه اینکه چقدر باشد.
cardIdخیرکدام کارت مقصد. اگر نفرستید، کارت پیش‌فرض استفاده می‌شود.
فیلد ناشناخته رد می‌شود، نه نادیده گرفته. اگر ammount بنویسید، درخواست با VALIDATION_FAILED رد می‌شود. جایگزین، ساختن فاکتوری با مبلغ اشتباه بود — و کسی که این اشتباه را می‌فهمید، مشتری بود.
RESPONSE200 OK
{
  "success": true,
  "paymentId": "pay_01J8XK4M2Q",
  "invoiceId": "inv_01J8XK4M2Q",
  "paymentUrl": "https://steve-gate.ir/pay/inv_01J8XK4M2Q",
  "amount": 363706,          // مبلغ قابل پرداخت، نه مبلغ سفارش
  "amountRial": 3637060,
  "status": "pending",
  "expiresAt": "2026-09-22T09:41:07.000Z",
  "payment": {
    "originalAmount": 359000,     // همان مبلغی که فرستادید
    "fee": 3000,
    "baseAmount": 362000,       // originalAmount + fee
    "uniqueSuffix": 1706,       // پسوند یکتا
    "payableAmount": 363706,     // baseAmount + uniqueSuffix
    "payableAmountRial": 3637060,
    "testMode": false
  }
}

چرا مبلغ با مبلغ سفارش یکی نیست

سه عدد در پاسخ هست و رابطه‌شان همیشه برقرار است:

baseAmount + uniqueSuffix = payableAmount
originalAmount + fee = baseAmount

پسوند یکتا همان چیزی است که این فاکتور را از هر فاکتور زنده دیگری جدا می‌کند. یک ایندکس یکتا در پایگاه‌داده تضمین می‌کند در هر لحظه فقط یک فاکتور فعال هر مبلغی را داشته باشد؛ پس اگر دو درخواست همزمان بیایند، یکی پسوند دیگری را نمی‌گیرد. به همین دلیل پسوند در پاسخ برگردانده می‌شود: همان رقمی است که روی صفحه پرداخت و در پیامک بانک باید دیده شود.

خطاها

کدHTTPمعنی
UNAUTHENTICATED401هیچ کلیدی فرستاده نشده
INVALID_API_KEY401کلید ناشناخته است یا راز آن درست نیست
API_KEY_REVOKED401کلید باطل شده است
API_KEY_EXPIRED401مهلت کلید گذشته است
API_KEY_ENVIRONMENT_MISMATCH401کلید آزمایشی روی مسیر عملیاتی یا برعکس
INSUFFICIENT_SCOPE403کلید دسترسی payments:create ندارد
IP_NOT_ALLOWED403درخواست از خارج فهرست IP کلید آمده است
ACCOUNT_PENDING_APPROVAL403حساب در انتظار تأیید مدیر است
ACCOUNT_SUSPENDED403حساب معلق شده است
ACCOUNT_BANNED403حساب مسدود شده است
VALIDATION_FAILED422فیلدی ناقص، بدشکل، یا ناشناخته بود
INVALID_AMOUNT400مبلغ عدد صحیح به تومان نبود
AMOUNT_BELOW_MINIMUM400کمتر از حداقل مجاز
AMOUNT_ABOVE_MAXIMUM400بیشتر از حداکثر مجاز
CURRENCY_NOT_SUPPORTED400واحد پول پشتیبانی نمی‌شود
INVALID_FEE_MODE400CUSTOMER یا MERCHANT نبود
INVALID_EXPIRY400مدت اعتبار خارج از بازه مجاز بود
CARD_REQUIRED409هیچ کارت مقصد فعالی ثبت نشده است
INSUFFICIENT_WALLET_BALANCE402حالت کارمزد MERCHANT است و موجودی کیف پول کافی نیست
IDEMPOTENCY_CONFLICT409همین کلید با بدنه دیگری استفاده شده است
AMOUNT_SPACE_EXHAUSTED503همه پسوندهای فضای مبلغ در این لحظه اشغال است
MAINTENANCE_MODE503ساخت فاکتور توسط مدیر غیرفعال شده است
RATE_LIMITED429محدودیت نرخ؛ هدر Retry-After را ببینید
DATABASE_ERROR500خطای داخلی. با همان کلید یکتاسازی دوباره تلاش کنید

یکتاسازی درخواست

کلید یکتاسازی از شناسه سفارش خودتان بسازید — نه یک رشته تصادفی. اگر پاسخ گم شود، تایم‌اوت بخورد یا سرور شما وسط کار ری‌استارت شود، ارسال دوباره همان فاکتور اول را برمی‌گرداند، نه فاکتور دوم. پاسخ تکرارشده هدر Idempotent-Replay: true دارد.

HEADERSدو درخواست، یک فاکتور
Idempotency-Key: order_1234
# ارسال دوباره همان درخواست → همان invoiceId
Idempotent-Replay: true
وضعیتنتیجه
کلید تازهپرداخت ساخته می‌شود.
کلید تکراری با بدنه یکسانفاکتور اصلی برگردانده می‌شود و هدر Idempotent-Replay: true ست می‌شود.
کلید تکراری با بدنه متفاوتIDEMPOTENCY_CONFLICT با کد ۴۰۹. این اشکال در سمت شماست و پاسخ‌دادن با نتیجه اول، آن را پنهان می‌کرد.
درخواست ناموفقکلید آزاد می‌شود، پس تلاش دوباره واقعاً کار را انجام می‌دهد.

کلیدها ۲۴ ساعت نگه داشته می‌شوند. اگر کلید نفرستید، درخواست کار می‌کند اما هر بار فاکتور تازه‌ای می‌سازد — که با یک دکمه «تلاش دوباره» در سمت شما به دو فاکتور زنده ختم می‌شود.

کارت‌های مقصد

GET /api/v1/cards cards:read

کارت‌هایی که پول به آن‌ها واریز می‌شود. فقط شکل ماسک‌شده برگردانده می‌شود؛ شماره کامل هیچ‌وقت از این اندپوینت بیرون نمی‌آید.

RESPONSE200 OK
{
  "success": true,
  "cards": [{
    "id": "card_01J8XK...",
    "masked": "6104-****-****-3456",
    "title": "کارت اصلی",
    "bankName": "بانک ملت",
    "isDefault": true
  }]
}
شماره کارت پذیرنده در پایگاه‌داده رمزنگاری‌شده ذخیره می‌شود، چون برای ساختن صفحه پرداخت باید کامل نمایش داده شود. هیچ توکن کارت مشتری، هیچ CVV و هیچ رمز دوم ذخیره نمی‌شود: پول با انتقال کارت‌به‌کارت معمولی می‌آید.

وضعیت حساب

GET /api/v1/status payments:read

دفعه اول این را صدا بزنید. اگر جواب می‌دهد، احراز هویت، دسترسی‌ها و وضعیت حساب درست است و آنچه مانده از فهرست کارهای باقی‌مانده پیداست.

RESPONSE200 OK
{
  "success": true,
  "merchant": {
    "merchantCode": "SP-1042",
    "displayName": "فروشگاه نمونه",
    "accountStatus": "ACTIVE",
    "environment": "live"
  },
  "sms": {
    "pipelineConnected": true,      // آیا پیامکی در ۲۴ ساعت گذشته رسیده
    "verifiedAt": "2026-09-20T09:12:00.000Z",
    "webhookUrl": "https://steve-gate.ir/sms"
  },
  "wallet": { "availableBalance": 147000, "reservedBalance": 0 },
  "setup": {
    "completionPercent": 87,
    "ready": false,
    "remainingSteps": [
      { "key": "sms_test", "title": "آزمایش خط پیامک" }
    ]
  }
}

setup.remainingSteps نام کارهای باقی‌مانده را می‌دهد، نه تعدادشان. پذیرنده‌ای که به او بگویید «۳ کار مانده» تازه باید برود پیدایشان کند.

کیف پول

GET /api/v1/wallet wallet:read

موجودی کیف پول. کارمزد از اینجا کسر می‌شود، پس اگر حالت کارمزد MERCHANT باشد موجودی، سقف تعداد فاکتورهایی است که می‌توانید بسازید.

RESPONSE200 OK
{
  "wallet": {
    "balance": 147000,
    "availableBalance": 147000,   // balance - reservedBalance
    "reservedBalance": 0,       // رزرو‌شده برای فاکتورهای در جریان
    "totalDeposited": 150000,
    "totalFeesPaid": 3000,
    "currency": "IRT"
  }
}

availableBalance = balance - reservedBalance و در زمان خواندن محاسبه می‌شود. پولی که برای فاکتور زنده MERCHANT رزرو شده، دو بار خرج نمی‌شود.

شمارش تراکنش‌ها

GET /api/v1/transactions/count?range=today transactions:read

خروجی این اندپوینت همان اعدادی است که در پنل پذیرنده می‌بینید، پس اگر عددی در پنل و در گزارش داخلی شما یکی نیست، یکی از این دو مرز بازه را جور دیگری حساب می‌کند.

rangeمعنی
todayاز نیمه‌شب به وقت تهران تا الان (پیش‌فرض)
yesterdayروز کامل قبل، به وقت تهران
last7daysهفت روز شمسی گذشته
last30daysسی روز شمسی گذشته
customبازه دلخواه؛ from الزامی است و to اختیاری. هر دو شامل می‌شوند.
RESPONSE200 OK
{
  "range": "today",
  "from": "2026-09-21T20:30:00.000Z",   // نیمه‌شب تهران، به UTC
  "counts": {
    "total": 42, "successful": 38, "pending": 3,
    "expired": 1, "failed": 0, "manualReview": 0
  },
  "amounts": { "volume": 13842000, "fees": 114000 },
  "successRate": 90.5,
  "averagePaymentSeconds": 214,
  "lifetime": { "total": 1840, "successRate": 97.3 }
}
مقدار from همیشه یک لحظه واقعی UTC است، اما مرزِ بازه نیمه‌شب تهران است. پذیرنده‌ای که ساعت ۱ بامداد می‌پرسد «امروز چقدر فروختم» منظورش چند ساعت گذشته است، و یک مرز UTC به او آمار دیروز را می‌داد.

پیامک بانک

این اندپوینت قلب سامانه است: تنها مسیر نوشتن در موتور تطبیق. درگاه کارت را نمی‌خواند و به هیچ API بانکی وصل نیست — شاهدِ پرداخت، پیامکی است که بانک به گوشی شما می‌فرستد. پس اگر این مرحله وصل نشود، هیچ پرداختی خودکار تأیید نمی‌شود.

POST /sms sms:write
CURLآزمایش دستی
curl -X POST https://steve-gate.ir/sms \
  -H "X-API-Key: sk_live_" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "واریز ۳۶۳٬۷۰۶ تومان به کارت 6104337890123456 شماره پیگیری ۸۴۲۱۹۰۳۳۱",
    "sender": "BANKMELLI",
    "deviceId": "shop-phone-1"
  }'

راه‌اندازی فورواردر پیامک روی اندروید

روی گوشی اندرویدی که پیامک بانک را می‌گیرد، برنامه‌ای لازم است که پیامک را هنگام رسیدن به آدرس این سامانه بفرستد. راهنمای زیر برای برنامه SMS Forwarder است؛ هر برنامه دیگری که بتواند درخواست HTTP با هدر دلخواه بفرستد هم کار می‌کند، فقط نام منوها فرق می‌کند.

قبل از شروع، از پنل پذیرنده → کلیدهای API یک کلید بسازید که دسترسی sms:write داشته باشد، و از پنل پذیرنده → پیامک آدرس وبهوک را بردارید. در تمام مراحل زیر کلمه «Next» همان دکمه پایین صفحه در برنامه است.

  1. ۰۱

    یک فیلتر تازه بسازید

    در برنامه به بخش Filters بروید و یک فیلتر جدید بسازید. هر فیلتر در این برنامه یعنی «اگر پیامکی با این شرط رسید، این کار را بکن» — پس این فیلتر نقش «قانون واریز بانکی» را بازی می‌کند.

  2. ۰۲

    شرط فیلتر را روی پیامک‌های دریافتی بگذارید

    در بخش شرط فیلتر، گزینه Incoming SMS/RCS را انتخاب کنید تا فیلتر فقط روی پیامک‌های رسیده کار کند، نه پیامک‌های ارسالی خودتان.

  3. ۰۳

    وبهوک را در بخش Where to forward اضافه کنید

    در بخش Where to forward روی Add بزنید و از گزینه‌های موجود URL را انتخاب کنید. اینجاست که برنامه یاد می‌گیرد پیامک را به کجا بفرستد.

  4. ۰۴

    روش و آدرس را تنظیم کنید

    گزینه Request Type را روی POST بگذارید و در فیلد Enter URL این آدرس را وارد کنید:
    https://steve-gate.ir/sms

  5. ۰۵

    هدر X-Api-Key را اضافه کنید

    در همان صفحه، بخش Header را باز کنید، روی Add بزنید و کلید API خودتان را بگذارید:
    نام هدر: X-Api-Key — مقدار: کلید کامل sk_live_… یا sk_test_… از پنل شما.
    بدون این هدر درخواست با کد UNAUTHENTICATED رد می‌شود.

  6. ۰۶

    بدنه را روی Json بگذارید و این مقدار را کپی کنید

    بخش Body را روی Json قرار دهید و دقیقاً همین مقدار را کپی و پیست کنید. آکولادهای داخل رشته‌ها متغیرهای خود برنامه‌اند و هنگام ارسال با متن واقعی پیامک جایگزین می‌شوند — ترجمه یا فارسی‌کردن آن‌ها را انجام ندهید:

    JSONبدنه فورواردر
    {
      "msg": "{msg}",
      "time": "{time}",
      "filter-name": "{filter-name}",
      "in-number": "{in-number}",
      "in-sim": "{in-sim}"
    }

  7. ۰۷

    ذخیره کنید

    آیکون سیو را در بالای صفحه، سمت راست، بزنید. تا این مرحله تعریف وبهوک تمام شده است.

  8. ۰۸

    تا صفحه Forwarding Conditions 2/2 جلو بروید

    صفحه‌ها را دانه‌دانه با Next جلو بروید تا به صفحه Forwarding Conditions 2/2 برسید. آنجا سیم‌کارتی را که پیامک بانکی به آن می‌رسد انتخاب کنید. اگر هر دو سیم‌کارتتان پیامک بانکی می‌گیرند، گزینه All Numbers را انتخاب کنید.

  9. ۰۹

    بقیه گزینه‌ها را دست نزنید

    به هیچ چیز دیگری دست نزنید و فقط Next بزنید تا فرایند ساخت فیلتر تمام شود. در پایان برنامه یک پیام تستی به وبهوک می‌فرستد تا اتصال را بسنجد.

فقط برنامه فورواردر را روی حساب خودتان تنظیم کنید. متن پیامک بانک ممکن است موجودی حساب شما را داشته باشد. هرچند سامانه پیامک بی‌ربط را جایی نمایش نمی‌دهد و آن را فقط «تجزیه‌نشده» ثبت می‌کند، بهتر است انتخاب سیم‌کارت را هم محدود کنید.

چرا بدنه دقیقاً همین شکل است

هر کلید این JSON به یک مقدار در سامانه نگاشت می‌شود. نام‌ها را عوض نکنید — سامانه نام‌های رایج را می‌شناسد، اما شکل زیر همان چیزی است که خود برنامه تولید می‌کند و کمترین تغییر را لازم دارد:

متغیر برنامهجایگزین می‌شود بادر سامانه
{msg}متن کامل پیامکهمان چیزی که تجزیه و با مبلغ یکتا مقایسه می‌شود
{time}زمان رسیدن پیامک به گوشیذخیره می‌شود، اما مبنای تطبیق نیست
{filter-name}نام همین فیلترشناسه دستگاه؛ نام فیلتر را همان نام گوشی بگذارید
{in-number}شماره فرستندهبرای تشخیص تکراری و ممیزی نگه داشته می‌شود
{in-sim}سیم‌کارتی که پیامک را گرفتهبرای این سامانه معنایی ندارد و نادیده گرفته می‌شود
موقعیت نامه‌ها آزاد است: اگر برنامه شما به جای msg فیلد را message یا text می‌فرستد، همان هم پذیرفته می‌شود. آنچه سامانه لازم دارد فقط متن پیامک است؛ بقیه فیلدها اختیاری‌اند.

نام استاندارد وبهوک

تنظیممقدار
آدرسhttps://steve-gate.ir/sms
روشPOST
هدرX-Api-Key: sk_live_
معادل هدرAuthorization: Bearer — اگر برنامه‌ای فقط یکی از این دو را بتواند بفرستد
بدنهJSON با کلید msg (یا message)
ثابت بودنیک کلید ثابت بفرستید؛ چرخاندن کلید به معنی به‌روزرسانی فورواردر است

تست اتصال

در پنل پذیرنده → پیامک دکمه «تست اتصال» یک پیامک نمونه با یک توکن یک‌ساعته نشان می‌دهد. آن را در برنامه فورواردر به‌جای متن عادی بفرستید؛ اگر رسید، مسیر، کلید و بدنه هر سه درست‌اند و از این پس پیامک‌های واقعی هم می‌رسند. توکن آزمایشی هیچ فاکتوری را تأیید نمی‌کند و فقط اتصال را می‌سنجد.

تطبیق چطور انجام می‌شود

بررسیتوضیح
مبلغ یکتامبلغ پیامک باید دقیقاً برابر مبلغ قابل پرداخت یک فاکتور زنده همان پذیرنده باشد. کم یا زیاد، پرداخت تأیید نمی‌شود.
پنجره زمانیپیامک باید در بازه‌ای حول زمان فاکتور باشد (پیش‌فرض: ۱۰ دقیقه پیش و ۹۰ دقیقه بعد). این بازه با تنطیمات قابل تغییر است.
کارت مقصداگر شماره کارت مقصد در پیامک باشد، با کارت فاکتور مقایسه می‌شود.
شماره پیگیریبرای درج در رسید ثبت می‌شود و برای تشخیص واریز تکراری به کار می‌رود.
تکراریهش متن پیامک یکتاست. فرستادن دوباره همان پیامک، پرداخت دوم نمی‌سازد و فقط بی‌اثر رد می‌شود.
زمان اعلامی برنامه فورواردر پذیرفته نمی‌شود. فیلد receivedAt وجود دارد و ذخیره می‌شود، اما برای تطبیق استفاده نمی‌شود؛ گوشی با ساعت غلط می‌توانست یک پرداخت را داخل پنجره زمانی بگذارد. مبنای تطبیق، زمان سرور است.

آنچه ممکن است پیش بیاید

کدHTTPمعنی
SMS_INVALID_PAYLOAD400ساختار ارسالی نامعتبر است، مثلاً فیلد message خالی است
SMS_TOO_LARGE413متن پیامک بیش از حد مجاز طولانی است
SMS_DUPLICATE200همین پیامک قبلاً رسیده بود. بی‌اثر و بی‌خطر است
SMS_UNPARSEABLE200پیامک رسید اما الگوی هیچ بانک شناخته‌شده‌ای را نخواند؛ در پنل با وضعیت «تجزیه‌نشده» دیده می‌شود
SMS_TEST_TOKEN_INVALID400توکن آزمایشی نامعتبر یا منقضی است
RATE_LIMITED429سقف پیامک در دقیقه پر شده است

قالب ناشناخته یک خطای سرور نیست و درگاه را متوقف نمی‌کند: پیامک ذخیره می‌شود، در لاگ پنل با متن اصلی دیده می‌شود و دستی قابل بررسی است. اگر پیامک‌های واقعی‌تان تجزیه‌نشده ماندند، متن کامل را برای پشتیبانی بفرستید تا قالب بانک اضافه شود.

وقتی پیامک نمی‌رسد

ترتیب این فهرست همان ترتیبی است که یک پیامک در آن حرکت می‌کند. هر مرحله را جدا بسنجید؛ اگر یکی درست کار نکند، مرحله بعد اصلاً اتفاق نمی‌افتد و پنل هیچ خطایی نشان نمی‌دهد، چون هیچ درخواستی به سامانه نرسیده است.

نشانهجایی که باید بگردید
در پنل پیامک هیچ رکوردی نیستفیلتر در برنامه فورواردر خاموش یا غیرفعال است، یا انتخاب سیم‌کارت در مرحله Forwarding Conditions اشتباه است
رکورد هست ولی «تجزیه‌نشده» استمتن پیامک با هیچ قالب بانکی خوانده نشده؛ متن کامل را برای پشتیبانی بفرستید تا الگوی همان بانک اضافه شود
رکورد «تکراری» استهمین پیامک از قبل رسیده. بی‌خطر است و کاری لازم نیست
پاسخ ۴۰۱ گرفته‌ایدهدر نام درست تنظیم نشده، یا کلید باطل شده است؛ کلید را در پنل بچرخانید و فورواردر را به‌روز کنید
پاسخ ۴۲۹ گرفته‌ایدسقف پیامک در دقیقه پر شده است؛ در تنظیمات سامانه قابل تغییر است
پرداخت تأیید نشدمبلغ پیامک با هیچ مبلغ قابل پرداخت فاکتور زنده‌ای یکی نبوده؛ دقیقاً مبلغی را واریز کنید که روی صفحه پرداخت نوشته شده
پیامک را از گوشی دستی نفرستید. فرستادن متن پیامک بانک از گوشی دیگر، همان متن را وارد سامانه می‌کند و اگر مبلغش با فاکتوری یکی باشد، آن فاکتور تأیید می‌شود. برای آزمایش همیشه از توکن آزمایشی استفاده کنید.

وب‌هوک

وقتی پرداختی قطعی شد، رویداد امضاشده به آدرس شما می‌رود. وبهوک تأیید پرداخت نیست — پرداخت پیش از آن تأیید شده است. اگر سرور شما پایین باشد، پول برنمی‌گردد؛ فقط تحویل دوباره تلاش می‌شود.

رویدادها

رویدادچه وقت
payment.createdفاکتور ساخته شد
payment.pendingواریز شناسایی شد ولی هنوز قطعی نشده است
payment.successپرداخت تأیید و تسویه شد — رویداد اصلی همین است
payment.failedپرداخت رد شد
payment.expiredمهلت فاکتور تمام شد بدون واریز
payment.manual_reviewپرداخت مشکوک است و منتظر تصمیم مدیر
wallet.low_balanceموجودی کیف پول کم شده است
test.pipelineرویداد آزمایشی برای سنجش اتصال

هدرها

هدرمحتوا
X-SteveGate-Signaturev1= — امضای HMAC-SHA256
X-SteveGate-Timestampزمان ارسال به میلی‌ثانیه
X-SteveGate-Eventنام رویداد
X-SteveGate-Deliveryشناسه یکتای این تحویل؛ برای تشخیص دریافت تکراری
X-SteveGate-Attemptشماره تلاش، از ۱
سامانه پیش‌تر «Steve Pay» نام داشت. همان پنج هدر با نام قبلی (X-StevePay-…) هم فرستاده می‌شود و مقدارش دقیقاً برابر نام تازه است، تا کدی که از قبل نوشته‌اید از کار نیفتد. خواندن هر کدام کافی است؛ امضای دومی برای بررسی وجود ندارد. برای کد تازه از نام بالا استفاده کنید.

بررسی امضا

رشته‌ای که امضا می‌شود دقیقاً این است: timestamp + "." + deliveryId + "." + body. بدنه باید خام باشد — اگر آن را پارس و دوباره سریالایز کنید، فاصله‌ها و ترتیب کلیدها عوض می‌شود و بررسی شکست می‌خورد.

NODE.JSبررسی امضا
const crypto = require('node:crypto');

// خام، بدون تغییر: بدنه باید همان بایتی باشد که امضا شده است
function verify(rawBody, headers, secret) {
  const timestamp = headers['x-stevepay-timestamp'];
  const delivery  = headers['x-stevepay-delivery'];
  const received  = headers['x-stevepay-signature'];   // "v1="

  const expected = 'v1=' + crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${delivery}.${rawBody}`)
    .digest('hex');

  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}
PHPبررسی امضا

$timestamp = $_SERVER['HTTP_X_STEVEPAY_TIMESTAMP'];
$delivery  = $_SERVER['HTTP_X_STEVEPAY_DELIVERY'];
$received  = $_SERVER['HTTP_X_STEVEPAY_SIGNATURE'];
$raw       = file_get_contents('php://input');   // خام، بدون json_decode

$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $delivery . $raw, $secret);
if (!hash_equals($expected, $received)) {
    http_response_code(400);
    exit;
}
مقایسه را در زمان ثابت انجام دهید (timingSafeEqual در Node، hash_equals در PHP). مقایسه رشته‌ای معمولی با زمان پاسخ، به کسی که هزاران درخواست می‌فرستد اجازه می‌دهد امضا را حدس بزند.

تلاش دوباره

پاسخ ۲xx یعنی تحویل موفق. هر چیز دیگر — از جمله تغییر مسیر و تایم‌اوت ده‌ثانیه‌ای — ناموفق است و دوباره تلاش می‌شود، با این فاصله‌ها:

1m → 5m → 30m → 2h → 12h → 24h

هندلر شما باید ایدمپوتنت باشد: یک رویداد ممکن است بیش از یک بار برسد. با X-SteveGate-Delivery دریافت‌های تکراری را تشخیص دهید و بی‌صدا ۲۰۰ برگردانید — برگرداندن خطا، تلاش دوباره را طولانی‌تر می‌کند.

سرور خود را طوری بنویسید که پیش از پاسخ‌دادن، پرداخت را تأیید شده ثبت کند. اگر کار اصلی را بعد از پاسخ انجام دهید و بعد از پاسخ شکست بخورد، ما ۲۰۰ دیده‌ایم و دیگر تلاش نمی‌کنیم.

وضعیت هر تحویل، تعداد تلاش‌ها و متن پاسخ سرور شما در پنل پذیرنده → وب‌هوک دیده می‌شود و از همان‌جا می‌توانید تحویل ناموفق را دوباره بفرستید.

قالب خطا

هر خطا یک شکل دارد و کد آن پایدار است: کدها هرگز بازاستفاده نمی‌شوند، پس می‌توانید روی code شرط بگذارید. متن فارسی خواندنی است و ممکن است بهتر شود؛ روی متن شرط نگذارید.

ERRORapplication/json
{
  "success": false,
  "code": "INSUFFICIENT_WALLET_BALANCE",
  "message": "موجودی کیف پول برای ساخت فاکتور کافی نیست.",
  "requestId": "req_01J8XM...",
  "details": { "required": 3000, "available": 0 }
}

details فقط جایی می‌آید که بتوان بر اساسش کاری کرد. هیچ‌وقت متن stack trace، نام جدول یا پیام پایگاه‌داده در پاسخ نیست.

خطاهای عمومی

کدHTTPمعنی
VALIDATION_FAILED422داده ارسالی معتبر نیست
INVALID_REQUEST400درخواست نامعتبر است
NOT_FOUND404منبع پیدا نشد
FORBIDDEN403دسترسی به این بخش ندارید
PERMISSION_DENIED403برای این کار مجوز لازم را ندارید
RATE_LIMITED429تعداد درخواست بیشتر از حد مجاز؛ Retry-After را بخوانید
MAINTENANCE_MODE503سرویس موقتاً در حالت تعمیر است
INTERNAL_ERROR500خطای داخلی سرور
DATABASE_ERROR500خطای داخلی پایگاه‌داده. با همان کلید یکتاسازی دوباره تلاش کنید

خطاهای فاکتور و پرداخت

کدHTTPمعنی
INVOICE_NOT_FOUND404فاکتور پیدا نشد
INVOICE_EXPIRED410مهلت فاکتور تمام شده است
INVOICE_ALREADY_PAID409این فاکتور قبلاً پرداخت شده است
INVOICE_CANCELLED410فاکتور لغو شده است
INVOICE_NOT_PAYABLE409فاکتور در وضعیت قابل پرداخت نیست
INVOICE_UNDER_REVIEW409پرداخت فاکتور در حال بررسی دستی است
AMOUNT_SPACE_EXHAUSTED503ظرفیت مبلغ یکتا در این لحظه پر است. چند لحظه بعد دوباره تلاش کنید
STATE_TRANSITION_INVALID409این تغییر وضعیت مجاز نیست
DUPLICATE_TRANSACTION409این تراکنش بانکی قبلاً ثبت و تسویه شده است

خطاهای کیف پول و کارت

کدHTTPمعنی
WALLET_NOT_FOUND404کیف پول حساب پیدا نشد
INSUFFICIENT_WALLET_BALANCE402موجودی برای کارمزد کافی نیست
LEDGER_CONFLICT409تناقض در دفتر کل
CARD_INVALID400شماره کارت معتبر نیست
CARD_DUPLICATE409این کارت قبلاً ثبت شده است
CARD_NOT_FOUND404کارت پیدا نشد
CARD_LIMIT_REACHED409تعداد کارت‌ها به سقف رسیده است
CALLBACK_URL_NOT_ALLOWED400آدرس بازگشت باید HTTPS و روی دامنه ثبت‌شده شما باشد

محدودیت‌ها

این مقادیر پیش‌فرض سامانه‌اند و مدیر می‌تواند تغییرشان دهد. محدودیت نرخ در پاسخ ۴۲۹ هدر Retry-After با عدد ثانیه می‌فرستد.

محدودهمقدار پیش‌فرض
حداقل مبلغ فاکتور۱,۰۰۰ تومان
حداکثر مبلغ فاکتور۵۰۰,۰۰۰,۰۰۰ تومان
مدت اعتبار فاکتور۱۵ تا ۶۰ دقیقه (پیش‌فرض ۳۰)
ساخت پرداخت۶۰ درخواست در دقیقه به‌ازای هر پذیرنده
پیامک دریافتی۱۲۰ در دقیقه به‌ازای هر پذیرنده و هر IP
کل درخواست‌های API۳۰۰ در دقیقه به‌ازای هر کلید
ورود۱۰ تلاش در ۱۵ دقیقه
ثبت‌نام۵ حساب در ساعت به‌ازای هر IP
صفحه عمومی فاکتور۱۲۰ بازدید در دقیقه به‌ازای هر IP
تلاش دوباره وب‌هوک۶ تلاش، مهلت ۱۰ ثانیه برای هر تلاش
حداکثر فاکتور زنده هر پذیرنده۵۰۰
حداکثر کارت هر پذیرنده۲۰
طول عمر کلید یکتاسازی۲۴ ساعت
سقف وب‌هوک معتبر است: پس از ۲۵ شکست پیاپی، مسیر وب‌هوک به‌طور خودکار غیرفعال می‌شود تا صف پشت یک آدرس خراب گیر نکند. دلیل غیرفعال‌شدن روی همان صفحه دیده می‌شود و با اصلاح آدرس، دوباره فعال می‌شود.

نمونه کامل

یک مسیر کامل: سفارش ساخته می‌شود، فاکتور تولید می‌شود، مشتری به صفحه پرداخت می‌رود، و وب‌هوک سفارش را تأیید می‌کند.

NODE.JSساخت پرداخت
const BASE = 'https://steve-gate.ir/api/v1';
const key  = process.env.STEVE_GATE_KEY;

async function createPayment(order) {
  const response = await fetch(BASE + '/payments', {
    method: 'POST',
    headers: {
      'X-API-Key': key,
      'Content-Type': 'application/json',
      // کلید یکتاسازی از شناسه سفارش خودتان: ارسال دوباره فاکتور دوم نمی‌سازد
      'Idempotency-Key': 'order-' + order.id,
    },
    body: JSON.stringify({
      amount: order.total,
      description: 'سفارش ' + order.number,
      returnUrl: 'https://shop.example.com/orders/' + order.id,
      metadata: { orderId: order.id },
    }),
  });

  if (!response.ok) {
    const error = await response.json();
    // کد خطا پایدار است؛ پیام فارسی است و ممکن است تغییر کند
    throw new Error(error.code + ': ' + error.message);
  }

  const data = await response.json();
  // همین آدرس را به مشتری بدهید
  return data.paymentUrl;
}
CURLکل مسیر در یک خط
curl -X POST https://steve-gate.ir/api/v1/payments \
  -H "X-API-Key: sk_live_" \
  -H "Idempotency-Key: order-1234" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 359000, "description": "سفارش ۱۲۳۴" }'
گامکار سمت شما
۱سفارش را در پایگاه‌داده خودتان با وضعیت «در انتظار پرداخت» بسازید.
۲کلید یکتاسازی را از شناسه سفارش بسازید و پرداخت را ثبت کنید.
۳paymentUrl پاسخ را به مشتری بدهید یا ریدایرکت کنید.
۴در وب‌هوک payment.success امضا را بررسی کنید و سفارش را تأیید کنید.
۵با همان invoiceId وضعیت را در پایگاه‌داده خودتان به‌روز کنید — نه با شماره سفارش، که سمت شماست و ما نمی‌شناسیمش.
برای خودآزمایی، returnUrl را به صفحه‌ای بفرستید که وضعیت را از API خودمان می‌خواند، نه به صفحه‌ای که فرض می‌کند پرداخت موفق بوده است. مشتری می‌تواند آدرس بازگشت را دستی باز کند.

محیط آزمایشی

کلید sk_test_ و کلید sk_live_ را از هم جدا نگه دارید. حساب آزمایشی، فاکتور آزمایشی می‌سازد: کیف پول دست نمی‌خورد و پیامک‌های آزمایشی هرگز فاکتور عملیاتی را تأیید نمی‌کنند.

کارروش درست
ساخت فاکتور نمونهبا کلید sk_test_ و برنامه فورواردر آزمایشی روی یک گوشی جدا.
سنجش اتصال وب‌هوکرویداد test.pipeline از پنل پذیرنده بفرستید.
سنجش اتصال پیامکدر پنل پذیرنده → پیامک یک توکن تست بسازید و در فورواردر بگذارید.
پاک‌کردن داده آزمایشیفاکتورهای آزمایشی در آمار عملیاتی حساب نمی‌شوند و روی کیف پول اثری ندارند.
هیچ گاه برنامه فورواردر را روی گوشی‌ای که پیامک بانکی واقعی می‌گیرد، با کلید آزمایشی تنظیم نکنید: پیامک واقعی به مسیر آزمایشی می‌رود، تجزیه می‌شود، اما پرداخت عملیاتی تأیید نمی‌شود و شما فکر می‌کنید سامانه از کار افتاده است.

پشتیبانی

در هر تماس، requestId پاسخ را بفرستید. آن شناسه در لاگ، در رکورد ممیزی و در تراکنش‌های مرتبط ثبت شده است، پس پیگیری بدون پرسیدن «کدام درخواست؟» انجام می‌شود.