هر چیزی که برای وصلکردن یک فروشگاه لازم است: ساخت پرداخت، دریافت پیامک بانک، بررسی وبهوک و معنای هر کد خطا.
https://steve-gate.ir — همین آدرسی که این صفحه روی آن باز شده است. همه پاسخها JSON هستند و هر پاسخ، موفق یا ناموفق، هدر X-Request-Id دارد. اگر خطایی دیدید که خودتان حل نکردید، همین شناسه را برای پشتیبانی بفرستید: با آن، لاگ و رکورد ممیزی همان درخواست پیدا میشود.چهار کار تا اولین پرداخت تأییدشده فاصله دارید. ترتیب مهم است: پیامک بانک شاهدِ پرداخت است، پس تا فورواردر وصل نشود هیچ فاکتوری خودکار تأیید نمیشود.
در صفحه ثبتنام شماره موبایل و گذرواژه را وارد کنید. حساب در وضعیت «در انتظار تأیید» ساخته میشود و تا تأیید مدیر، درخواستهای API با کد ACCOUNT_PENDING_APPROVAL رد میشوند.
پس از تأیید، از پنل پذیرنده → کلیدهای API کلید بسازید. کلید یکبار و فقط یکبار نشان داده میشود؛ فقط هش آن ذخیره است. اگر گم شد، همان کلید را بچرخانید.
در پنل پذیرنده → کارتها شماره کارتی که پول به آن واریز میشود را اضافه کنید. بدون کارت فعال، ساخت فاکتور با کد CARD_REQUIRED رد میشود. شماره کارت مشتری هرگز ذخیره نمیشود.
روی گوشی اندرویدی که پیامک بانک را میگیرد، برنامه فورواردر را به 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:create | POST /api/v1/payments |
payments:read | GET /api/v1/status |
transactions:read | GET /api/v1/transactions/count |
wallet:read | GET /api/v1/wallet |
cards:read | GET /api/v1/cards |
sms:write | POST /sms |
هر کلید میتواند فهرست IP مجاز داشته باشد. اگر فهرست خالی نباشد، درخواست از هر IP دیگری با IP_NOT_ALLOWED رد میشود — و این بررسی پیش از هر کار دیگری انجام میشود.
یک فاکتور میسازد و مبلغی یکتا برای آن رزرو میکند. پاسخ شامل آدرس صفحه پرداخت است که باید به مشتری بدهید.
{
"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 رد میشود. جایگزین، ساختن فاکتوری با مبلغ اشتباه بود — و کسی که این اشتباه را میفهمید، مشتری بود.{
"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
}
}
سه عدد در پاسخ هست و رابطهشان همیشه برقرار است:
پسوند یکتا همان چیزی است که این فاکتور را از هر فاکتور زنده دیگری جدا میکند. یک ایندکس یکتا در پایگاهداده تضمین میکند در هر لحظه فقط یک فاکتور فعال هر مبلغی را داشته باشد؛ پس اگر دو درخواست همزمان بیایند، یکی پسوند دیگری را نمیگیرد. به همین دلیل پسوند در پاسخ برگردانده میشود: همان رقمی است که روی صفحه پرداخت و در پیامک بانک باید دیده شود.
| کد | HTTP | معنی |
|---|---|---|
UNAUTHENTICATED | 401 | هیچ کلیدی فرستاده نشده |
INVALID_API_KEY | 401 | کلید ناشناخته است یا راز آن درست نیست |
API_KEY_REVOKED | 401 | کلید باطل شده است |
API_KEY_EXPIRED | 401 | مهلت کلید گذشته است |
API_KEY_ENVIRONMENT_MISMATCH | 401 | کلید آزمایشی روی مسیر عملیاتی یا برعکس |
INSUFFICIENT_SCOPE | 403 | کلید دسترسی payments:create ندارد |
IP_NOT_ALLOWED | 403 | درخواست از خارج فهرست IP کلید آمده است |
ACCOUNT_PENDING_APPROVAL | 403 | حساب در انتظار تأیید مدیر است |
ACCOUNT_SUSPENDED | 403 | حساب معلق شده است |
ACCOUNT_BANNED | 403 | حساب مسدود شده است |
VALIDATION_FAILED | 422 | فیلدی ناقص، بدشکل، یا ناشناخته بود |
INVALID_AMOUNT | 400 | مبلغ عدد صحیح به تومان نبود |
AMOUNT_BELOW_MINIMUM | 400 | کمتر از حداقل مجاز |
AMOUNT_ABOVE_MAXIMUM | 400 | بیشتر از حداکثر مجاز |
CURRENCY_NOT_SUPPORTED | 400 | واحد پول پشتیبانی نمیشود |
INVALID_FEE_MODE | 400 | CUSTOMER یا MERCHANT نبود |
INVALID_EXPIRY | 400 | مدت اعتبار خارج از بازه مجاز بود |
CARD_REQUIRED | 409 | هیچ کارت مقصد فعالی ثبت نشده است |
INSUFFICIENT_WALLET_BALANCE | 402 | حالت کارمزد MERCHANT است و موجودی کیف پول کافی نیست |
IDEMPOTENCY_CONFLICT | 409 | همین کلید با بدنه دیگری استفاده شده است |
AMOUNT_SPACE_EXHAUSTED | 503 | همه پسوندهای فضای مبلغ در این لحظه اشغال است |
MAINTENANCE_MODE | 503 | ساخت فاکتور توسط مدیر غیرفعال شده است |
RATE_LIMITED | 429 | محدودیت نرخ؛ هدر Retry-After را ببینید |
DATABASE_ERROR | 500 | خطای داخلی. با همان کلید یکتاسازی دوباره تلاش کنید |
کلید یکتاسازی از شناسه سفارش خودتان بسازید — نه یک رشته تصادفی. اگر پاسخ گم شود، تایماوت بخورد یا سرور شما وسط کار ریاستارت شود، ارسال دوباره همان فاکتور اول را برمیگرداند، نه فاکتور دوم. پاسخ تکرارشده هدر Idempotent-Replay: true دارد.
Idempotency-Key: order_1234 # ارسال دوباره همان درخواست → همان invoiceId Idempotent-Replay: true
| وضعیت | نتیجه |
|---|---|
| کلید تازه | پرداخت ساخته میشود. |
| کلید تکراری با بدنه یکسان | فاکتور اصلی برگردانده میشود و هدر Idempotent-Replay: true ست میشود. |
| کلید تکراری با بدنه متفاوت | IDEMPOTENCY_CONFLICT با کد ۴۰۹. این اشکال در سمت شماست و پاسخدادن با نتیجه اول، آن را پنهان میکرد. |
| درخواست ناموفق | کلید آزاد میشود، پس تلاش دوباره واقعاً کار را انجام میدهد. |
کلیدها ۲۴ ساعت نگه داشته میشوند. اگر کلید نفرستید، درخواست کار میکند اما هر بار فاکتور تازهای میسازد — که با یک دکمه «تلاش دوباره» در سمت شما به دو فاکتور زنده ختم میشود.
کارتهایی که پول به آنها واریز میشود. فقط شکل ماسکشده برگردانده میشود؛ شماره کامل هیچوقت از این اندپوینت بیرون نمیآید.
{
"success": true,
"cards": [{
"id": "card_01J8XK...",
"masked": "6104-****-****-3456",
"title": "کارت اصلی",
"bankName": "بانک ملت",
"isDefault": true
}]
}
دفعه اول این را صدا بزنید. اگر جواب میدهد، احراز هویت، دسترسیها و وضعیت حساب درست است و آنچه مانده از فهرست کارهای باقیمانده پیداست.
{
"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 نام کارهای باقیمانده را میدهد، نه تعدادشان. پذیرندهای که به او بگویید «۳ کار مانده» تازه باید برود پیدایشان کند.
موجودی کیف پول. کارمزد از اینجا کسر میشود، پس اگر حالت کارمزد MERCHANT باشد موجودی، سقف تعداد فاکتورهایی است که میتوانید بسازید.
{
"wallet": {
"balance": 147000,
"availableBalance": 147000, // balance - reservedBalance
"reservedBalance": 0, // رزروشده برای فاکتورهای در جریان
"totalDeposited": 150000,
"totalFeesPaid": 3000,
"currency": "IRT"
}
}
availableBalance = balance - reservedBalance و در زمان خواندن محاسبه میشود. پولی که برای فاکتور زنده MERCHANT رزرو شده، دو بار خرج نمیشود.
خروجی این اندپوینت همان اعدادی است که در پنل پذیرنده میبینید، پس اگر عددی در پنل و در گزارش داخلی شما یکی نیست، یکی از این دو مرز بازه را جور دیگری حساب میکند.
| range | معنی |
|---|---|
today | از نیمهشب به وقت تهران تا الان (پیشفرض) |
yesterday | روز کامل قبل، به وقت تهران |
last7days | هفت روز شمسی گذشته |
last30days | سی روز شمسی گذشته |
custom | بازه دلخواه؛ from الزامی است و to اختیاری. هر دو شامل میشوند. |
{
"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 بانکی وصل نیست — شاهدِ پرداخت، پیامکی است که بانک به گوشی شما میفرستد. پس اگر این مرحله وصل نشود، هیچ پرداختی خودکار تأیید نمیشود.
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» همان دکمه پایین صفحه در برنامه است.
در برنامه به بخش Filters بروید و یک فیلتر جدید بسازید. هر فیلتر در این برنامه یعنی «اگر پیامکی با این شرط رسید، این کار را بکن» — پس این فیلتر نقش «قانون واریز بانکی» را بازی میکند.
در بخش شرط فیلتر، گزینه Incoming SMS/RCS را انتخاب کنید تا فیلتر فقط روی پیامکهای رسیده کار کند، نه پیامکهای ارسالی خودتان.
در بخش Where to forward روی Add بزنید و از گزینههای موجود URL را انتخاب کنید. اینجاست که برنامه یاد میگیرد پیامک را به کجا بفرستد.
گزینه Request Type را روی POST بگذارید و در فیلد Enter URL این آدرس را وارد کنید:https://steve-gate.ir/sms
در همان صفحه، بخش Header را باز کنید، روی Add بزنید و کلید API خودتان را بگذارید:
نام هدر: X-Api-Key — مقدار: کلید کامل sk_live_… یا sk_test_… از پنل شما.
بدون این هدر درخواست با کد UNAUTHENTICATED رد میشود.
بخش Body را روی Json قرار دهید و دقیقاً همین مقدار را کپی و پیست کنید. آکولادهای داخل رشتهها متغیرهای خود برنامهاند و هنگام ارسال با متن واقعی پیامک جایگزین میشوند — ترجمه یا فارسیکردن آنها را انجام ندهید:
{
"msg": "{msg}",
"time": "{time}",
"filter-name": "{filter-name}",
"in-number": "{in-number}",
"in-sim": "{in-sim}"
}
آیکون سیو را در بالای صفحه، سمت راست، بزنید. تا این مرحله تعریف وبهوک تمام شده است.
صفحهها را دانهدانه با Next جلو بروید تا به صفحه Forwarding Conditions 2/2 برسید. آنجا سیمکارتی را که پیامک بانکی به آن میرسد انتخاب کنید. اگر هر دو سیمکارتتان پیامک بانکی میگیرند، گزینه All Numbers را انتخاب کنید.
به هیچ چیز دیگری دست نزنید و فقط 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_PAYLOAD | 400 | ساختار ارسالی نامعتبر است، مثلاً فیلد message خالی است |
SMS_TOO_LARGE | 413 | متن پیامک بیش از حد مجاز طولانی است |
SMS_DUPLICATE | 200 | همین پیامک قبلاً رسیده بود. بیاثر و بیخطر است |
SMS_UNPARSEABLE | 200 | پیامک رسید اما الگوی هیچ بانک شناختهشدهای را نخواند؛ در پنل با وضعیت «تجزیهنشده» دیده میشود |
SMS_TEST_TOKEN_INVALID | 400 | توکن آزمایشی نامعتبر یا منقضی است |
RATE_LIMITED | 429 | سقف پیامک در دقیقه پر شده است |
قالب ناشناخته یک خطای سرور نیست و درگاه را متوقف نمیکند: پیامک ذخیره میشود، در لاگ پنل با متن اصلی دیده میشود و دستی قابل بررسی است. اگر پیامکهای واقعیتان تجزیهنشده ماندند، متن کامل را برای پشتیبانی بفرستید تا قالب بانک اضافه شود.
ترتیب این فهرست همان ترتیبی است که یک پیامک در آن حرکت میکند. هر مرحله را جدا بسنجید؛ اگر یکی درست کار نکند، مرحله بعد اصلاً اتفاق نمیافتد و پنل هیچ خطایی نشان نمیدهد، چون هیچ درخواستی به سامانه نرسیده است.
| نشانه | جایی که باید بگردید |
|---|---|
| در پنل پیامک هیچ رکوردی نیست | فیلتر در برنامه فورواردر خاموش یا غیرفعال است، یا انتخاب سیمکارت در مرحله Forwarding Conditions اشتباه است |
| رکورد هست ولی «تجزیهنشده» است | متن پیامک با هیچ قالب بانکی خوانده نشده؛ متن کامل را برای پشتیبانی بفرستید تا الگوی همان بانک اضافه شود |
| رکورد «تکراری» است | همین پیامک از قبل رسیده. بیخطر است و کاری لازم نیست |
| پاسخ ۴۰۱ گرفتهاید | هدر نام درست تنظیم نشده، یا کلید باطل شده است؛ کلید را در پنل بچرخانید و فورواردر را بهروز کنید |
| پاسخ ۴۲۹ گرفتهاید | سقف پیامک در دقیقه پر شده است؛ در تنظیمات سامانه قابل تغییر است |
| پرداخت تأیید نشد | مبلغ پیامک با هیچ مبلغ قابل پرداخت فاکتور زندهای یکی نبوده؛ دقیقاً مبلغی را واریز کنید که روی صفحه پرداخت نوشته شده |
وقتی پرداختی قطعی شد، رویداد امضاشده به آدرس شما میرود. وبهوک تأیید پرداخت نیست — پرداخت پیش از آن تأیید شده است. اگر سرور شما پایین باشد، پول برنمیگردد؛ فقط تحویل دوباره تلاش میشود.
| رویداد | چه وقت |
|---|---|
payment.created | فاکتور ساخته شد |
payment.pending | واریز شناسایی شد ولی هنوز قطعی نشده است |
payment.success | پرداخت تأیید و تسویه شد — رویداد اصلی همین است |
payment.failed | پرداخت رد شد |
payment.expired | مهلت فاکتور تمام شد بدون واریز |
payment.manual_review | پرداخت مشکوک است و منتظر تصمیم مدیر |
wallet.low_balance | موجودی کیف پول کم شده است |
test.pipeline | رویداد آزمایشی برای سنجش اتصال |
| هدر | محتوا |
|---|---|
X-SteveGate-Signature | v1= — امضای HMAC-SHA256 |
X-SteveGate-Timestamp | زمان ارسال به میلیثانیه |
X-SteveGate-Event | نام رویداد |
X-SteveGate-Delivery | شناسه یکتای این تحویل؛ برای تشخیص دریافت تکراری |
X-SteveGate-Attempt | شماره تلاش، از ۱ |
X-StevePay-…) هم فرستاده میشود و مقدارش دقیقاً برابر نام تازه است، تا کدی که از قبل نوشتهاید از کار نیفتد. خواندن هر کدام کافی است؛ امضای دومی برای بررسی وجود ندارد. برای کد تازه از نام بالا استفاده کنید.رشتهای که امضا میشود دقیقاً این است: timestamp + "." + deliveryId + "." + body. بدنه باید خام باشد — اگر آن را پارس و دوباره سریالایز کنید، فاصلهها و ترتیب کلیدها عوض میشود و بررسی شکست میخورد.
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)); }
$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 یعنی تحویل موفق. هر چیز دیگر — از جمله تغییر مسیر و تایماوت دهثانیهای — ناموفق است و دوباره تلاش میشود، با این فاصلهها:
هندلر شما باید ایدمپوتنت باشد: یک رویداد ممکن است بیش از یک بار برسد. با X-SteveGate-Delivery دریافتهای تکراری را تشخیص دهید و بیصدا ۲۰۰ برگردانید — برگرداندن خطا، تلاش دوباره را طولانیتر میکند.
وضعیت هر تحویل، تعداد تلاشها و متن پاسخ سرور شما در پنل پذیرنده → وبهوک دیده میشود و از همانجا میتوانید تحویل ناموفق را دوباره بفرستید.
هر خطا یک شکل دارد و کد آن پایدار است: کدها هرگز بازاستفاده نمیشوند، پس میتوانید روی code شرط بگذارید. متن فارسی خواندنی است و ممکن است بهتر شود؛ روی متن شرط نگذارید.
{
"success": false,
"code": "INSUFFICIENT_WALLET_BALANCE",
"message": "موجودی کیف پول برای ساخت فاکتور کافی نیست.",
"requestId": "req_01J8XM...",
"details": { "required": 3000, "available": 0 }
}
details فقط جایی میآید که بتوان بر اساسش کاری کرد. هیچوقت متن stack trace، نام جدول یا پیام پایگاهداده در پاسخ نیست.
| کد | HTTP | معنی |
|---|---|---|
VALIDATION_FAILED | 422 | داده ارسالی معتبر نیست |
INVALID_REQUEST | 400 | درخواست نامعتبر است |
NOT_FOUND | 404 | منبع پیدا نشد |
FORBIDDEN | 403 | دسترسی به این بخش ندارید |
PERMISSION_DENIED | 403 | برای این کار مجوز لازم را ندارید |
RATE_LIMITED | 429 | تعداد درخواست بیشتر از حد مجاز؛ Retry-After را بخوانید |
MAINTENANCE_MODE | 503 | سرویس موقتاً در حالت تعمیر است |
INTERNAL_ERROR | 500 | خطای داخلی سرور |
DATABASE_ERROR | 500 | خطای داخلی پایگاهداده. با همان کلید یکتاسازی دوباره تلاش کنید |
| کد | HTTP | معنی |
|---|---|---|
INVOICE_NOT_FOUND | 404 | فاکتور پیدا نشد |
INVOICE_EXPIRED | 410 | مهلت فاکتور تمام شده است |
INVOICE_ALREADY_PAID | 409 | این فاکتور قبلاً پرداخت شده است |
INVOICE_CANCELLED | 410 | فاکتور لغو شده است |
INVOICE_NOT_PAYABLE | 409 | فاکتور در وضعیت قابل پرداخت نیست |
INVOICE_UNDER_REVIEW | 409 | پرداخت فاکتور در حال بررسی دستی است |
AMOUNT_SPACE_EXHAUSTED | 503 | ظرفیت مبلغ یکتا در این لحظه پر است. چند لحظه بعد دوباره تلاش کنید |
STATE_TRANSITION_INVALID | 409 | این تغییر وضعیت مجاز نیست |
DUPLICATE_TRANSACTION | 409 | این تراکنش بانکی قبلاً ثبت و تسویه شده است |
| کد | HTTP | معنی |
|---|---|---|
WALLET_NOT_FOUND | 404 | کیف پول حساب پیدا نشد |
INSUFFICIENT_WALLET_BALANCE | 402 | موجودی برای کارمزد کافی نیست |
LEDGER_CONFLICT | 409 | تناقض در دفتر کل |
CARD_INVALID | 400 | شماره کارت معتبر نیست |
CARD_DUPLICATE | 409 | این کارت قبلاً ثبت شده است |
CARD_NOT_FOUND | 404 | کارت پیدا نشد |
CARD_LIMIT_REACHED | 409 | تعداد کارتها به سقف رسیده است |
CALLBACK_URL_NOT_ALLOWED | 400 | آدرس بازگشت باید HTTPS و روی دامنه ثبتشده شما باشد |
این مقادیر پیشفرض سامانهاند و مدیر میتواند تغییرشان دهد. محدودیت نرخ در پاسخ ۴۲۹ هدر Retry-After با عدد ثانیه میفرستد.
| محدوده | مقدار پیشفرض |
|---|---|
| حداقل مبلغ فاکتور | ۱,۰۰۰ تومان |
| حداکثر مبلغ فاکتور | ۵۰۰,۰۰۰,۰۰۰ تومان |
| مدت اعتبار فاکتور | ۱۵ تا ۶۰ دقیقه (پیشفرض ۳۰) |
| ساخت پرداخت | ۶۰ درخواست در دقیقه بهازای هر پذیرنده |
| پیامک دریافتی | ۱۲۰ در دقیقه بهازای هر پذیرنده و هر IP |
| کل درخواستهای API | ۳۰۰ در دقیقه بهازای هر کلید |
| ورود | ۱۰ تلاش در ۱۵ دقیقه |
| ثبتنام | ۵ حساب در ساعت بهازای هر IP |
| صفحه عمومی فاکتور | ۱۲۰ بازدید در دقیقه بهازای هر IP |
| تلاش دوباره وبهوک | ۶ تلاش، مهلت ۱۰ ثانیه برای هر تلاش |
| حداکثر فاکتور زنده هر پذیرنده | ۵۰۰ |
| حداکثر کارت هر پذیرنده | ۲۰ |
| طول عمر کلید یکتاسازی | ۲۴ ساعت |
یک مسیر کامل: سفارش ساخته میشود، فاکتور تولید میشود، مشتری به صفحه پرداخت میرود، و وبهوک سفارش را تأیید میکند.
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 -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 پاسخ را بفرستید. آن شناسه در لاگ، در رکورد ممیزی و در تراکنشهای مرتبط ثبت شده است، پس پیگیری بدون پرسیدن «کدام درخواست؟» انجام میشود.