کد تأیید (OTP)
verifyپیامپال بین پروژهی شما و مخابرات میایستد: شما یک قالب و چند پارامتر میفرستید، ما هزینهی واقعیاش را از کیفپول کم میکنیم و پیامک را تحویل میدهیم. این صفحه همهی چیزی است که برای اتصال لازم دارید.
اگر فقط ۳۰ ثانیه وقت دارید، همین چهار خط کافی است.
پاسخ 201 یعنی «پذیرفته شد و از کیفپول کم شد» — نه «به دست گیرنده رسید». تحویل بعداً و بهصورت غیرهمزمان انجام میشود، و اگر ارسال برای همیشه شکست بخورد مبلغ خودکار برمیگردد. جزئیات در چرخهی عمر پیام.
این پنج قدم بهترتیباند؛ جابهجا کردنشان یعنی اولین ارسال شما شکست میخورد.
insufficient balance میگیرید.کلید در هدر x-api-key میرود — نه در Authorization، نه در query string.
x-api-key: pp_OzZk6QvR2mNbXwT8yLhP4dCsJfA1gEuKpp_ شروع میشود و در مجموع ۳۵ کاراکتر است.هر کسی که کلید شما را داشته باشد میتواند از کیفپول شما پیامک بفرستد. کلید فقط باید روی سرور بماند — هرگز داخل اپلیکیشن موبایل، جاوااسکریپت سمت مرورگر یا کد فرانتاند نگذاریدش.
POST /sms/send — تنها اندپوینتی که با کلید API باز میشود.
| فیلد | نوع | الزامی | توضیح |
|---|---|---|---|
| template | string | بله | کد قالب، مثل verify. بین ۱ تا ۶۴ کاراکتر. |
| mobile | string | بله | دقیقاً با الگوی 09XXXXXXXXX — یازده رقم، بدون +98 و بدون فاصله. |
| parameters | object | array | خیر | مقدار جایگذاریشده در #PARAM#های قالب. هر دو شکل زیر پذیرفته میشود. |
شکل اول سادهتر است. شکل دوم همان بدنهای است که کلاینتهای sms.ir از قبل میسازند و عمداً پشتیبانی میشود تا مهاجرت به تغییر بدنه نیاز نداشته باشد. هزینه و خروجی هر دو یکسان است.
{
"template": "verify",
"mobile": "09121110001",
"parameters": { "CODE": "45678" }
}{
"template": "verify",
"mobile": "09121110001",
"parameters": [
{ "name": "CODE", "value": "45678" }
]
}CODE و code دو چیز متفاوتاند. اگر نام پارامتر با آنچه در قالب تعریف شده یکی نباشد، جای آن پر نمیشود و کاربر متن خام #CODE# را دریافت میکند. نام دقیق هر پارامتر در پنل، کنار همان قالب نوشته شده است.
{
"id": "efb20be8-c5b5-4427-ae9a-6f7e83df43b3",
"status": "queued",
"segments": 1,
"encoding": "UCS2",
"cost": 260
}| فیلد | یعنی چه |
|---|---|
| id | شناسهی پیام. آن را ذخیره کنید؛ تنها راه پیگیری بعدی همین است. |
| status | در لحظهی پاسخ همیشه queued است. |
| segments | این پیام چند پیامک حساب شده. میتواند بیشتر از ۱ باشد. |
| encoding | UCS2 برای متن فارسی، GSM7 برای متن کاملاً لاتین. |
| cost | تومانی که همین حالا از کیفپول کم شد. |
پیامک از متنِ آزاد ساخته نمیشود؛ شما یک قالبِ از پیش تعریفشده را صدا میزنید و فقط جای پارامترها را پر میکنید.
این محدودیت از سمت اپراتور میآید، نه ما: هر متنی که به مشترک میرسد باید از قبل ثبت و تأیید شده باشد. در عوض، ارسال از طریق قالب سریع است و نیاز به تأییدِ موردی ندارد.
قالبهای زیر برای همهی مشتریها در دسترساند. اگر متنِ دیگری لازم دارید، از پنل کاربری → تب «قالبها» قالبِ اختصاصیِ خودتان را بسازید؛ بعد از اینکه ما آن را به اپراتور وصل کردیم، دقیقاً مثل بقیه کار میکند.
verifyorder-placedorder-shippedفهرست نمونه — برای دیدن فهرست کامل و بهروز، وارد پنل کاربری شوید.
کدِ قالب را در فیلد template بگذارید و هر #PARAM# را با یک کلید همنام در parameters پر کنید. مثلاً برای verify با متنِ کد تأیید شما در #COMPANY#: #CODE#:
{
"template": "verify",
"mobile": "09121110001",
"parameters": {
"COMPANY": "آرین اسپرت",
"CODE": "45678"
}
}متنی که به گیرنده میرسد میشود: کد تأیید شما در آرین اسپرت: 45678
اگر کلیدی را جا بیندازید یا نامش را اشتباه بنویسید، جای آن پر نمیشود و کاربر عبارتِ #CODE# را همانطور دریافت میکند. نامِ پارامترها به بزرگی و کوچکیِ حروف حساس است.
هزینه از متنِ نهایی حساب میشود، نه از خودِ قالب. یک نامِ شرکتِ بلند یا لینکِ طولانی میتواند همان قالبِ یکپیامکی را دوپیامکی کند. با محاسبهگر بخش بعد بلندترین حالتِ ممکن را امتحان کنید.
هزینه ثابت نیست. از روی متن نهایی — قالب بهعلاوهی پارامترهای شما — در لحظهی ارسال حساب میشود.
یک پیامک ظرفیت محدودی دارد و متن بلندتر به چند پیامک تقسیم میشود. چون الفبای فارسی یونیکد است، ظرفیتش کمتر از متن لاتین است:
| متن | کدگذاری | یک پیامک | وقتی چندبخشی شد |
|---|---|---|---|
| فارسی (یا هر نویسهی غیرلاتین) | UCS2 | ۷۰ کاراکتر | هر ۶۷ کاراکتر |
| فقط لاتین | GSM7 | ۱۶۰ کاراکتر | هر ۱۵۳ کاراکتر |
نتیجهی عملی: یک نویسهی فارسی در یک قالب لاتین، هزینه را بیش از دو برابر میکند، و یک لینک بلندتر میتواند پیامک یکبخشی را دوبخشی کند. متن را با پارامترهای واقعی و بلندترین حالت ممکن امتحان کنید:
متن نهایی را — همانطور که گیرنده میبیند، با پارامترهای جایگذاریشده — اینجا بنویسید.
با ۵۲ کاراکتر دیگر، این پیام ۲ پیامک حساب میشود. · تعرفه ۲۶۰ تومان (تعرفهی پیشفرض)
cost پاسخ نگاه کنیدلازم نیست این ریاضی را در کد خودتان تکرار کنید. هر پاسخ موفق، segments و cost واقعی همان ارسال را برمیگرداند؛ اگر میخواهید مصرف را لاگ کنید، از همان استفاده کنید.
بین «پذیرفتیم» تا «رسید» فاصله هست، و این فاصله برای شما معنا دارد.
| وضعیت | یعنی چه | کیفپول |
|---|---|---|
| queued | پذیرفته شد و در صف ارسال است. پاسخ اولیه همیشه همین است. | کم شده |
| sent | به مخابرات تحویل داده شد. | کم شده |
| failed | ارسال بعد از چند تلاش برای همیشه شکست خورد. | برگشت خورد |
ارسال ناموفق تا سه بار با فاصلهی فزاینده تکرار میشود. اگر همهی تلاشها شکست بخورد، پیام failed میشود و مبلغش خودکار به کیفپول برمیگردد — لازم نیست کاری بکنید و نباید خودتان دوباره تلاش کنید.
وضعیت هر پیام، متن نهایی و هزینهاش را در پنل کاربری، تب «پیامکها» میبینید و میتوانید بر اساس شماره جستوجو کنید.
این API کلید یکتاسازی (idempotency key) ندارد. اگر بهخاطر timeout یا خطای شبکه همان درخواست را دوباره بفرستید، دو پیامک ارسال و دو بار هزینه کم میشود.
اگر پاسخی دریافت نکردید، کورکورانه retry نکنید: نتیجه را در تب «پیامکها» ببینید، یا خودتان قبل از ارسال مجدد یک قفل بگذارید.
همهی پاسخهای زیر عیناً از خود API گرفته شدهاند.
| کد | پیام | علت و راهحل |
|---|---|---|
| 401 | missing x-api-key | هدر اصلاً فرستاده نشده. |
| 401 | invalid api key | کلید اشتباه است یا باطل شده. کلید تازه بسازید. |
| 400 | invalid mobile number | شماره با الگوی 09XXXXXXXXX نمیخواند. +98، فاصله و خط تیره را حذف کنید. |
| 400 | insufficient balance | موجودی کافی نیست. هیچ پیامکی ارسال نشده و چیزی هم کم نشده. |
| 400 | parameters must be an object… | parameters نه شیء است نه آرایهی {name,value}. احتمالاً رشته فرستادهاید. |
| 404 | template "x" not found | کد قالب اشتباه است، یا قالب اختصاصیِ شخص دیگری است، یا غیرفعال شده. |
| 201 | { id, status, segments, cost } | پذیرفته و از کیفپول کم شد. تحویل هنوز انجام نشده. |
بدنهی خطاها همیشه همین شکل است:
{
"message": "insufficient balance",
"error": "Bad Request",
"statusCode": 400
}برای خطاهای اعتبارسنجی، message بهجای رشته یک آرایه از رشتههاست. اگر آن را مستقیم چاپ میکنید، هر دو حالت را در نظر بگیرید.
هر نمونه یک کار میکند: ارسال کد تأیید و خواندن هزینهی واقعی از پاسخ.
curl -X POST https://api.payampal.com/api/sms/send \
-H "x-api-key: $PAYAMPAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"template":"verify","mobile":"09121110001","parameters":{"CODE":"45678"}}'async function sendOtp(mobile, code) {
const res = await fetch('https://api.payampal.com/api/sms/send', {
method: 'POST',
headers: {
'x-api-key': process.env.PAYAMPAL_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
template: 'verify',
mobile,
parameters: { CODE: code },
}),
});
const body = await res.json();
if (!res.ok) {
// message is a string, or an array for validation errors
const msg = Array.isArray(body.message) ? body.message.join(', ') : body.message;
throw new Error(`payampal ${res.status}: ${msg}`);
}
// 201 means accepted and charged — not delivered
return body; // { id, status, segments, encoding, cost }
}function sendOtp(string $mobile, string $code): array {
$ch = curl_init('https://api.payampal.com/api/sms/send');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('PAYAMPAL_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'template' => 'verify',
'mobile' => $mobile,
'parameters' => ['CODE' => $code],
], JSON_UNESCAPED_UNICODE),
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$body = json_decode($raw, true);
if ($status >= 400) {
$m = $body['message'] ?? 'unknown';
throw new \RuntimeException('payampal: ' . (is_array($m) ? implode(', ', $m) : $m));
}
return $body;
}import os, requests
def send_otp(mobile: str, code: str) -> dict:
r = requests.post(
"https://api.payampal.com/api/sms/send",
headers={"x-api-key": os.environ["PAYAMPAL_API_KEY"]},
json={
"template": "verify",
"mobile": mobile,
"parameters": {"CODE": code},
},
timeout=15,
)
if not r.ok:
m = r.json().get("message", "unknown")
raise RuntimeError(f"payampal {r.status_code}: {m}")
# 201 — accepted and charged, delivery happens after this
return r.json()اگر پروژهتان الان مستقیم به sms.ir وصل است، سه چیز عوض میشود و بس.
| قبل — sms.ir | بعد — پیامپال | |
|---|---|---|
| آدرس | api.sms.ir/v1/send/verify | api.payampal.com/api/sms/send |
| هدر کلید | x-api-key | x-api-key همان |
| شناسهی قالب | templateId: 783969 | template: "verify" |
| شماره | mobile | mobile همان |
| پارامترها | [{name, value}] | [{name, value}] همان |
نام هدر و شکل آرایهای پارامترها عمداً یکی نگه داشته شدهاند. یعنی در بیشتر پروژهها فقط آدرس، مقدار کلید و شناسهی قالب عوض میشود و بدنهی درخواستی که از قبل میسازید دستنخورده میماند.
هزینه از روی متن قالبی که در پیامپال ثبت شده حساب میشود، ولی چیزی که مخابره میشود متن قالب واقعی سمت اپراتور است. اگر این دو یکی نباشند، محاسبهی هزینه غلط میشود — بیآنکه خطایی ببینید. پس هنگام ثبت قالب، متن دقیق را کپی کنید؛ با همان فاصلهها و نیمفاصلهها.
کلید API فقط برای ارسال است. بقیهی کارها از پنل انجام میشود.
| کار | با کلید API | از پنل کاربری |
|---|---|---|
| ارسال پیامک | بله | — |
| دیدن موجودی | خیر | تب کیفپول |
| شارژ کیفپول | خیر | تب کیفپول |
| تاریخچه و وضعیت پیامها | خیر | تب پیامکها |
| ساخت و مدیریت قالب | خیر | تب قالبها |
فعلاً بهصورت برنامهنویسی نمیشود. تا وقتی اندپوینتش اضافه شود، عملیترین کار این است که خطای insufficient balance را در کدتان جدی بگیرید: آن را لاگ کنید و به تیم خودتان هشدار بدهید تا کیفپول قبل از خالی شدن شارژ شود.
اینها همان چیزهاییاند که اگر رعایت نشوند، خرابی بیصدا است و فقط از روی شکایت کاربر معلوم میشود.
insufficient balance را جداگانه هندل کردهاماین خطا یعنی «شارژ کن»، نه «دوباره تلاش کن».id هر ارسال را ذخیره میکنمبرای وقتی که باید یک ارسال مشخص را پیگیری کنیم.آمادهاید؟ کلید API را از پنل بسازید و اولین پیامک را بفرستید.
ورود به پنل کاربری