ارسال پیام تراکنشی
یک پیام به یک نفر، همین حالا: کد ورود، وضعیت سفارش، یادآوری پرداخت. با قرارداد یکتایی که ارسال دوباره را بیخطر میکند.
پیام تراکنشی یعنی یک پیام به یک نفر، همین حالا: کد ورود، تایید سفارش، اطلاع از ارسال مرسوله، یادآوری پرداخت. بکاند شما تصمیم میگیرد که باید فرستاده شود، سگمنتیک میفرستد و همانجا جواب میدهد که چه شد.
این نقطه، خلاف جهت بقیهٔ محصول است. کمپین پیامی است که ما تصمیم گرفتهایم بفرستیم و سناریو پیامی است که رفتار خود کاربر آن را راه انداخته. این یکی جایی است که هندلر پرداخت شما میگوید «این را همین الان بفرست»، و همهٔ تفاوتها از همین یک جمله درمیآید: فراخوانی همگام است، روی کلیدی که خودتان انتخاب میکنید یکتا میماند، و از سقف تعداد و ساعت سکوت رد میشود.
نقطهٔ ورود
POST https://api.segmentic.net/v1/messages
Authorization: Bearer sk_seg_...
Content-Type: application/json
کلید، کلید API است (sk_seg_...)، محرمانه است و جایش روی سرور شماست. کلید نوشتن (wk_seg_...) داخل اپ شما منتشر میشود و اینجا با کد خطای مخصوص خودش رد میشود، write_key_rejected، تا کسی که مقدار اشتباه را از یک صفحه کپی کرده بفهمد کدام مقدار را لازم دارد، نه اینکه دنبال یک غلط تایپی بگردد.
مجوز لازم campaign.send است، نه campaign.write. نقشهای owner و admin و marketer آن را دارند. approver و analyst و viewer و finance ندارند. نوشتن پیشنویس یک کمپین و بیرون فرستادن پیام دو کار جدا هستند و این مسیر کار دوم را انجام میدهد.
این مسیر فقط وقتی وجود دارد که مسیر ارسال روی آن نصب پیکربندی شده باشد. اگر نشده باشد اصلا ثبت نمیشود و مسیر پیشفرض mux جواب 404 میدهد با بدنهٔ {"error":{"code":"unknown_endpoint","message":"no such endpoint: POST /v1/messages, see GET /v1/capabilities"}}. اول GET /v1/capabilities را بگیرید: روی نصبی که نمیتواند بفرستد "transactional": false گزارش میکند. روی ماشین محلی، API مدیریتی فقط وقتی سرو میشود که PUBLIC_API_ADDR مقدار داشته باشد، و پیشفرض خالی است، پس تا مقدار ندهید روی آن میزبان چیزی بالا نیست.
پاسخ همیشه فارسی است و Accept-Language روی این میزبان هیچ اثری ندارد. میانافزار زبان روی شنوندهٔ خود پنل سوار است و روی این یکی نه، پس تشخیص زبان به پیشفرضش میافتد که فارسی است. اسم reason_fa هم از همینجا میآید. هدر را بفرستید اگر دوست دارید؛ کسی نمیخواندش.
بدنهٔ درخواست
| فیلد | نوع | اجباری | پیشفرض |
|---|---|---|---|
user_id | رشته | بله | ندارد |
channel | رشته | بله | ندارد |
template_id | عدد | بله | ندارد |
idempotency_key | رشته | بله، در بدنه یا در هدر Idempotency-Key | ندارد |
category | رشته | خیر | transactional |
vars | شیء رشته به رشته | خیر | ندارد |
فیلدی برای متن پیام وجود ندارد. template_id اجباری است و محتوا داخل قالب میماند. دلیلش در خود کد نوشته شده: گرفتن متن بهصورت خطی «متن پیام را روی مسیر درخواست سامانهای میگذارد که اغلب از داخل هندلر پرداخت صدا زده میشود، و هر ارسال را بعد از وقوع غیرقابل بازبینی میکند». بخش قالبها را ببینید.
فیلد ناشناخته با 400 رد میشود. کسی که بهجای idempotency_key نوشته idempotencyKey در غیر این صورت روی هر تلاش مجدد یک کلید تازه میگرفت و به ازای هر تلاش یک پیام میفرستاد، یعنی همان خرابیای که این نقطهٔ ورود برای جلوگیری از آن ساخته شده، از راه غلط تایپیای که هیچکس در هیچ لاگی نمیبیندش.
سقف بدنه 262144 بایت است (256 کیلوبایت).
اعتبارسنجی به این ترتیب اجرا میشود و اولین خطا همان چیزی است که به شما گفته میشود:
user_idخالی نباشدtemplate_idصفر نباشدidempotency_keyخالی نباشدidempotency_keyبا الگویش بخواندvarsحداکثر ۴۰ عضو داشته باشدcategoryیکی ازtransactionalوcriticalیا خالی باشدchannelیکی از مقدارهای پذیرفتهشده باشد
کانال آخر از همه بررسی میشود. درخواستی که هم کانال بدی دارد و هم کلید ششحرفی، دربارهٔ کلیدش جواب میگیرد.
کلید یکتایی
این کلید شناسهٔ درخواست نیست. اسم اتفاقی است که در سامانهٔ خودتان افتاده است. مثل order-8821-shipped یا otp:2026-08-01:u_9137 یا invoice-5512-reminder-1.
اهمیتش از کاری میآید که یک کلید تولیدشده میکند. هندلر پرداختی که روی هر تلاش uuid() صدا میزند، بعد از چهار ثانیه تایماوت میخورد و دوباره تلاش میکند، برای یک اتفاق دو کلید متفاوت ساخته است. هر دو تازهاند. هر دو میفرستند. مشتری برای یک مرسوله دو پیامک میگیرد و به پشتیبانی زنگ میزند. کلیدی که از خود سفارش و مرحلهاش ساخته شده، روی هر دو تلاش یک رشتهٔ واحد است، پس تلاش دوم جواب تلاش اول را برمیگرداند و چیزی نمیفرستد.
شکل کلید: ^[A-Za-z0-9._:-]{8,200}$. حرف، رقم، نقطه، خط تیره، زیرخط و دونقطه، حداقل 8 و حداکثر 200 نویسه.
| پذیرفته میشود | رد میشود |
|---|---|
order-8821-shipped | short (کمتر از هشت نویسه) |
otp:2026-08-01:u_9137 | has space |
a1b2c3d4 | quote'inside |
x.y_z-1:2 | semi;colon |
کلید فقط از فاصلههای دو سرش پاک میشود و بس. بزرگی و کوچکی حرفها حفظ میشود، پس Order-1 و order-1 دو کلید جدا و دو پیام جدا هستند. یکی کردنشان، دو کلیدی را که یک فراخوان سختگیر شاید برای دو کار جدا به کار میبرد در هم میریخت.
کلید میتواند در بدنه به اسم idempotency_key بیاید یا در هدر Idempotency-Key. بدنه برنده است و هدر فقط وقتی خوانده میشود که فیلد بدنه خالی باشد. شکل هدری برای این وجود دارد که بیشتر کلاینتهای HTTP از قبل یک لایهٔ تلاش مجدد دارند که همین هدر را میگذارد.
کلید چه کار میکند
شناسهٔ پیام از کلید مشتق میشود، تولید نمیشود: t{tenant_id}.{key}. برای حساب شمارهٔ 7 و کلید order-8821 شناسه میشود t7.order-8821. پیشوند t یک ارسال تراکنشی را در گزارش پیامها و در انتساب، از کمپین (c) و سناریو (j) جدا میکند.
چون شناسه مشتق است، حذف تکراریها در لایههای پایینتر هم یک تکرار را میشناسد: حذف تکراری خود مسیر ارسال، شمارندهٔ سقف تعداد و گزارش پیامها همگی همان شناسه را میبینند، حتی اگر سطر دفتر یکتایی از دست رفته باشد.
دامنهٔ کلید (tenant_id, idempotency_key) است. یک کلید در دو حساب، دو پیام است.
رزرو کلید یک رفتوبرگشت است: یک INSERT با ON CONFLICT که برمیگرداند این فراخوان برنده شده یا نه. خواندن و بعد نوشتن نیست، چون رسیدن همزمان دو تلاش با یک کلید حالت عادی است و «اول بخوان بعد بنویس» بینشان پنجرهای میگذارد که هر دو از آن رد میشوند.
| وضعیت | چه میگیرید |
|---|---|
| اولین فراخوان | ارسال اجرا میشود و نتیجه روی کلید ذخیره میشود |
| تکرار کلیدی که تمام شده | همان نتیجهٔ ذخیرهشده، با "replayed": true. هیچ ارائهدهندهای صدا زده نمیشود. |
| تکرار کلیدی که ارسالش عمدا متوقف شده بود | همان امتناع، تکرار میشود. دوباره تلاش نمیشود. |
| تکرار وقتی تلاش اول هنوز در جریان است | 409 بههمراه Retry-After: 1 |
| تکرار بعد از اینکه تلاش اول خطا داده | رزرو آزاد شده، پس این تلاش اجرا میشود |
پاسخ تکراری، متن همان درخواست اول را برمیگرداند. reason_fa همان موقع ساخته و ذخیره شده است، پس تغییر بعدی آن جمله به کلیدی که یک بار جواب گرفته نمیرسد.
کلید تا کی به یاد میماند
سطرهای دفتر یکتایی بعد از API_IDEMPOTENCY_RETENTION جارو میشوند، که پیشفرضش هفت روز است. بعد از آن بازه، همان کلید دوباره میفرستد. کلید یک محافظ برای تلاش مجدد است، نه سابقهٔ همیشگی چیزهایی که فرستادهاید.
رزروهای رهاشده، یعنی آنهایی که پروسه وسط ارسال مرده و جا گذاشته، بعد از API_STALE_RESERVATION جارو میشوند که پیشفرضش یک دقیقه است. بدون این جاروب، یک ورکر کرشکرده تا ابد به هر تلاش مجدد آن کلید 409 میداد، و چیزی که متوقف میشد رسیدهای سفارش مشتری بود.
دستهبندی پیام
| دسته | از چه چیزی رد میشود |
|---|---|
marketing | از هیچچیز. همهٔ قاعدهها اعمال میشوند. |
transactional | همیشه از ساعت سکوت، و از سقف تعداد مگر آن سقف transactional را نام برده باشد |
critical | از هرچه تراکنشی رد میشود، بهعلاوهٔ خاموشبودن کانال و لغو یک موضوع |
نه transactional و نه critical از توقف اپراتوری رد نمیشوند. کسی که زیر بررسی تقلب یا توقیف حقوقی است هیچ پیامی نمیگیرد، حتی هشدار امنیتی. بخش رضایت و سقف را ببینید.
دستهٔ marketing اینجا با 400 رد میشود. متن خطا این است: transactional: this endpoint does not send marketing; use a campaign. این نقطهٔ ورود از سقف تعداد و ساعت سکوت رد میشود، پس پذیرفتن پیام تبلیغاتی روی آن یعنی به هر مشتری یک راه مستند برای دور زدن قاعدههای ارسال خودش دادهایم، و اولین باری که به چشم میآمد، یک پیامک تبلیغاتی ساعت سه بامداد به یک فهرست کامل بود.
مقدار ناشناخته در category جواب 503 message not sent میگیرد، نه 400. این یک ناهماهنگی واقعی در نسخهٔ امروز است و دانستنش میارزد: یک غلط تایپی در category شبیه قطعی سمت ما دیده میشود، و فراخوانی که دوباره تلاش کند باز هم 503 میگیرد. اول این فیلد را نگاه کنید، بعد ما را.
نبودن category به transactional تفسیر میشود، هیچوقت به critical. دستهٔ حیاتی از خاموشبودن کانال رد میشود، پس پیشفرض گرفتنش یعنی فراخوانی که این فیلد را جا انداخته، به کسی میرسد که آن کانال را با دست خودش خاموش کرده است.
دستهٔ خود قالب، دستهٔ داخل درخواست شما را کنار میزند. قالبی که با marketing ذخیره شده و از همین نقطهٔ ورود با "category": "transactional" فرستاده میشود، تبلیغاتی تحویل داده میشود، یعنی ساعت سکوت و سقف تعداد رویش اعمال میشوند. امتناعی که بالاتر گفته شد دربارهٔ فیلد درخواست است. حرف آخر با قالب است، چون همین فیلد است که نمیگذارد تایید سفارش تا ساعت نه صبح نگه داشته شود و به همان اندازه نمیگذارد یک تبلیغ با برچسب تراکنشی از سقف رد شود.
کد ورود critical است نه transactional. هر دو به خط خدماتی میرسند، پس تحویلشان یکسان به نظر میآید، و تفاوت فقط زیر سقف نرخ حساب خودش را نشان میدهد: سقفی که transactional را نام برده باشد کدهای یکبارمصرف شما را هم میشمارد، پس کمپین بزرگی که ساعت حساب را پر کند میتواند یکی را نگه دارد. کسی که با تبلیغات خودتان از حساب خودش بیرون بماند، سقف نرخی نیست که درست کار میکند. critical تنها دستهای است که هیچ سیاست ذخیرهشدهای به آن نمیرسد، هر چیزی که گفته باشد.
نبودن category یعنی transactional، پس کد ورودی که این فیلد را نگذارد دقیقا در معرض همین است. صریح بگذاریدش.
کانالها
مقدار channel باید دقیق یکی از اینها باشد:
push sms email webpush inapp bale eitaa rubika
سه مقدار که منطقی به نظر میرسند و رد میشوند:
webرد میشود. مقدار درستwebpushاست. جاهای دیگر پلتفرمwebرا بهعنوان نام قدیمی میپذیرند و شرح ابزار MCP هنوزwebرا تبلیغ میکند، ولی این نقطهٔ ورود مقایسهٔ حرفبهحرف میکند و شرح MCP اشتباه است.webpushبفرستید.messengerرد میشود. آن یک چتر برای نوشتن کمپین است که به ازای هر گیرنده باز میشود. ارسال تراکنشی باید مستقیمbaleیاeitaaیاrubikaرا نام ببرد.webhookاینجا رد میشود، و در کل پلتفرم هیچ فرستندهای برای تحویلش وجود ندارد.
اینکه یک نصب واقعا روی کدامیک از اینها میتواند تحویل بدهد به پیکربندی بستگی دارد. ارسال روی کانالی که ترابریاش پیکربندی نشده، نتیجهٔ failed میدهد نه خطای اعتبارسنجی.
قالبها
قالب باید قبل از ارسال وجود داشته باشد. عنوان، متن، تصویر، لینک عمیق، دکمهها، عمر پیام، اولویت، اتصال به الگوی پیامک و همان دستهای که دستهٔ شما را کنار میزند همگی روی قالب هستند.
قالبها روی کنترلپلین ساخته و ویرایش میشوند، یعنی همان APIای که پنل با آن حرف میزند:
GET /v1/templates template.read
POST /v1/templates template.write
POST /v1/templates/preview template.read
روی میزبان مدیریتی هیچ مسیری برای قالب وجود ندارد، و کنترلپلین هم از اینترنت مسیردهی نشده است. در استقرار مرجع، api.segmentic.net فقط شنوندهٔ مدیریتی را منتشر میکند. پس یکپارچهسازیای که کلید sk_seg_ دارد نمیتواند قالب بسازد، فهرست بگیرد، بخواند یا حذف کند. فقط میتواند با شناسه به یکی ارجاع بدهد، و آن شناسه باید از آدمی بیاید که پنل را باز کرده است. GET /v1/templates/{id} هم روی هیچ سطحی وجود ندارد و مسیر حذف هم ندارد.
مقدار template_id ناشناخته، بایگانیشده یا متعلق به حساب دیگر، جواب 503 message not sent میگیرد. نه 400 است و نه 404، چون خطا را انبار برمیگرداند نه اعتبارسنج درخواست. رزرو آزاد میشود، پس تلاش مجدد شما اجرا میشود، و تلاش مجددتان دقیق به همان شکل شکست میخورد. وقتی یک ارسال بلافاصله و پشت سر هم 503 میدهد، اول شناسهٔ قالب را نگاه کنید.
قالب روی مسیر ارسال سی ثانیه کش میشود. یک ویرایش در پنل تا نیم دقیقه طول میکشد تا به ارسال برسد.
شخصیسازی
فیلد vars یک نگاشت تخت رشته به رشته است با حداکثر ۴۰ عضو. مقدارها هرجا که قالب {{ key }} نوشته باشد جایگزین میشوند، با یک مقدار جایگزین اختیاری بعد از خط عمودی: {{ first_name | مشتری عزیز }}.
سه منبع با هم ترکیب میشوند، از ضعیفترین، و هرکدام قبلی را کنار میزند:
- متغیرهای پیامها، که یک بار برای کل حساب تعریف میشوند (
GET/PUT /v1/settings/content-varsروی API پنل) - پروندهٔ کاربر و اولین دستگاهش:
user_id،first_name،last_name،email،phone،city،region،country،language،full_name،order_count،total_revenue،last_order_date،birthday، هر ویژگی ذخیرهشده، بهعلاوهٔdevice_platformوapp_version - همین
varsروی این درخواست
مقدارهای شما برندهاند و این تنها ترتیب امن است: متغیری که برای کل حساب تعریف شده نباید بر خود آدمی که پیام برایش میرود بچربد، و هیچچیز نباید بر مقداری بچربد که سامانهٔ شما یک میلیثانیه پیش ساخته است.
متغیری که نه مقدار دارد و نه جایگزین، ارسال را متوقف میکند. نتیجه suppressed است با دلیل missing_personalisation، و فهرست کلیدهای گمشده در لاگ میآید نه در پاسخ. رشتهٔ خالی هم گمشده حساب میشود. اگر رندر نه عنوان بدهد و نه متن، دلیل empty_content میشود. برای اینکه ببینید یک مجموعه مقدار به چه چیزی تبدیل میشود و کدام کلیدها گماند، از POST /v1/templates/preview روی API پنل استفاده کنید؛ همان رندرکنندهای را صدا میزند که مسیر ارسال صدا میزند.
رقمهای لاتین داخل مقدارهای جایگزینشده، در عنوان و متن به رقم فارسی تبدیل میشوند و جداکنندهٔ هزارگان میگیرند. در deep_link و icon و image و هرچه داخل نگاشت data قالب باشد تبدیل نمیشوند، چون myapp://order/۱۲۳۴۵ لینکی است که اپ نمیتواند بخواندش. مقداری که حتی یک حرف لاتین داشته باشد رقمهای اسکیاش را نگه میدارد، چون گیرنده کد سفارشی مثل AB-1234567 را قرار است در یک جعبهٔ جستجو تایپ کند.
پاسخ
جواب 200 با این بدنه:
| فیلد | نوع | چه وقت هست |
|---|---|---|
message_id | رشته | همیشه |
status | رشته | همیشه |
sent_at | زمان RFC3339 | همیشه |
reason | رشته | فقط وقتی پیام عمدا فرستاده نشده |
reason_fa | رشته | فقط وقتی reason هست. جملهٔ آماده، روی این میزبان همیشه فارسی. |
error | رشته | فقط وقتی چیزی شکست خورده |
replayed | بولین | فقط وقتی درست است |
مقدار status یکی از اینهاست:
| وضعیت | معنی |
|---|---|
sent | دستکم یک ترابری آن را پذیرفت |
suppressed | عمدا نفرستادیم. reason میگوید چرا. |
deferred | نگه داشته شد تا بعد دوباره تلاش شود |
failed | همهٔ ترابریها ردش کردند. error حرف درگاه را دارد. |
یک ارسال موفق:
{
"message_id": "t7.order-8821-shipped",
"status": "sent",
"sent_at": "2026-08-01T12:00:00Z"
}
تکرار کلیدی که ارسال اولش رد شده بود:
{
"message_id": "t7.order-8821-shipped",
"status": "suppressed",
"reason": "channel_opt_out",
"reason_fa": "کاربر این کانال را خاموش کرده است",
"replayed": true,
"sent_at": "2026-08-01T12:00:00Z"
}
کد 200 نمیگوید پیام رفته است. هر سه حالت suppressed و deferred و failed هم با 200 میآیند، چون به درخواست درست جواب داده شده و ارسال انجام نشده. دلیل اینکه یک پیامک تراکنشی بدون الگوی تاییدشده با 200 و "status": "failed" برمیگردد این است که درخواست سالم بوده و حساب آمادهٔ ارسال نبوده. روی status شرط بگذارید، نه فقط روی کد HTTP.
همهٔ شکستها
| کد | بدنه | علت | چه کار کنید |
|---|---|---|---|
400 | malformed JSON: ... | بدنهٔ ناخوانا، فیلد ناشناخته، یا بدنهٔ بزرگتر از ۲۵۶ کیلوبایت | بدنه را درست کنید. تلاش مجدد کمکی نمیکند. |
400 | transactional: user_id is required | user_id ندارد | بدنه را درست کنید |
400 | transactional: template_id is required | template_id نیست یا صفر است | بدنه را درست کنید |
400 | transactional: idempotency_key is required | نه در بدنه کلید هست نه در هدر | بدنه را درست کنید |
400 | متن مربوط به شکل کلید | کلید با ^[A-Za-z0-9._:-]{8,200}$ نمیخواند | کلید را درست کنید. کلید تصادفی تازه نسازید. |
400 | transactional: unknown channel | جزو آن هشت مقدار نیست | بخش کانالها |
400 | transactional: this endpoint does not send marketing; use a campaign | "category": "marketing" | کمپین بسازید |
400 | transactional: too many variables | بیش از ۴۰ عضو در vars | کمتر بفرستید |
401 | {"error":{"code":"unauthenticated"}} | کلید ندارید یا کلید بد است | کلید را بررسی کنید |
401 | {"error":{"code":"write_key_rejected"}} | کلید wk_seg_ فرستادهاید | کلید sk_seg_ را بگذارید |
401 | {"error":{"code":"key_expired"}} | کلید منقضی شده | کلید تازه بسازید |
403 | {"error":{"code":"forbidden","need":"campaign.send"}} | کلید این مجوز را ندارد | مجوز campaign.send بدهید |
404 | {"error":{"code":"unknown_endpoint"}} | این نصب مسیر ارسال ندارد، پس این مسیر اصلا ثبت نشده | از اپراتورتان بپرسید و GET /v1/capabilities را بخوانید |
409 | transactional: a message with this idempotency key is already in flight و Retry-After: 1 | تلاش قبلی خودتان هنوز در جریان است | یک ثانیه صبر کنید و همان کلید را دوباره بفرستید |
429 | rate limit exceeded: N requests per minute و Retry-After: 60 | سقف دقیقهای حساب | عقب بکشید |
429 | {"error":{"code":"budget_exhausted"}} و Retry-After: 60 | بودجهٔ وزنی میزبان مدیریتی | عقب بکشید |
503 | budget_unavailable | بودجه خوانده نشد و این بررسی بسته میشکند | دوباره تلاش کنید |
503 | message not sent | قالب بارگذاری نشد، یا category مقدار ناشناخته داشت، یا مسیر ارسال خطا داد | template_id و category را نگاه کنید و بعد دوباره تلاش کنید |
200 | نتیجهٔ کامل | پیام رفت و نوشتن دفتر یکتایی شکست خورد | آن را رفته حساب کنید. دوباره نفرستید. |
سطر آخر عمدی است. وقتی پیام واقعا رفته و فقط ثبتش شکست خورده، فراخوانی که خلافش را بشنود دوباره تلاش میکند و یک پیام دوم میفرستد، پس نتیجه برگردانده میشود هرچند سمت ما چیزی خراب شده است.
سقف نرخ و دو پوشش خطا
دو سازوکار جدا اعمال میشوند و عمدا در دو جهت مخالف میشکنند.
سقف نرخ هر حساب. روی هر درخواست از خود حساب خوانده میشود و اگر نبود از پیشفرض نصب یعنی API_RATE_PER_MINUTE که مقدارش صفر است، و صفر یعنی اندازهگیری کلا خاموش. وقتی سقفی برقرار باشد، هدرهای X-RateLimit-Limit و X-RateLimit-Remaining روی هر پاسخ میآیند نه فقط روی امتناعها، چون فراخوانی که نمیبیند چقدر به سقف نزدیک است هیچ راهی ندارد قبل از رد شدن سرعتش را کم کند. عبور از سقف 429 است با Retry-After: 60.
پنجره یک سطل ثابت یکدقیقهای است، نه پنجرهٔ لغزان. یک فراخوان میتواند روی مرز دو پنجره چیزی نزدیک به دو برابر سقف بفرستد. این معامله عمدی است و کنار خود کد نوشته شده.
این سقف باز میشکند. اگر سقف خوانده نشود، ارسال اجازه میگیرد. دلیلش روی خود نقطهٔ ورود نوشته شده: این مسیر رسید سفارش و کد ورود را حمل میکند و قطعی یک شمارنده نباید دلیل این باشد که کسی نمیتواند وارد حسابش شود.
بودجهٔ وزنی میزبان مدیریتی. مقدار PUBLIC_API_BUDGET_PER_MINUTE پیشفرض ۶۰۰ واحد در دقیقه به ازای هر کلید است. هزینهٔ POST /v1/messages یک واحد است. عبور از بودجه 429 budget_exhausted است با Retry-After: 60. این یکی بسته میشکند: بودجهای که قابل بررسی نباشد 503 budget_unavailable میدهد.
شکل خطا روی این مسیر یکدست نیست. روی میزبان مدیریتی، امتناعهای احراز هویت و مجوز از پوشش عمومی استفاده میکنند، یعنی {"error":{"code":"...","message":"...","need":"..."}}. هرچه خود هندلر میسازد، یعنی همهٔ 400ها و آن 409 و 429 سقف نرخ و 503، شکل تخت {"error":"متن"} را دارد. کدی که خطاهای این یک مسیر را میخواند باید هر دو شکل را بخواند.
پیامک ایران: شرط الگو
پیامک تراکنشی بدون الگوی تاییدشده، روی درگاه شکست نمیخورد. اصلا به درگاه نمیرسد. داخل سگمنتیک رد میشود و با "status": "failed" و "error": "sms: a service line requires an approved pattern" برمیگردد.
در ایران دو نوع خط پیامک هست و اشتباه انتخاب کردن، به دو شکل متفاوت و گران شکست میخورد.
| خط | نامش | رفتار |
|---|---|---|
| تبلیغاتی | خط تبلیغاتی | ارزان، و برای هر مشترکی که پیامک تبلیغاتی را نزد اپراتورش بسته باشد نامرئی است، که سهم بسیار بزرگی از شمارههای ایران است. درگاه باز هم موفقیت گزارش میکند. |
| خدماتی | خط خدماتی | به همه میرسد، از جمله همان مشترکان. و دقیق به همین دلیل فقط میتواند محتوای تراکنشی حمل کند، آن هم فقط متنی که اپراتور از پیش تایید کرده باشد. |
خط از روی دسته انتخاب میشود، نه توسط کسی که پیام را مینویسد: marketing روی خط تبلیغاتی میرود و بقیه روی خط خدماتی. این عمدا در سطح کمپین قابل تنظیم نیست، چون تنها جایی که یک بازاریاب سراغ دور زدنش میرود، دقیق همان جایی است که خط خدماتی مشتری را از دستش درمیآورد.
پس هر پیامی که از POST /v1/messages میرود به خط خدماتی میرسد، و خط خدماتی الگو میخواهد. الگو یعنی متن پیام همانطور که اپراتور تاییدش کرده، با جایخالیهای نامدار، که زیر یک کد ثبت شده است. درگاه هر متنی روی خط خدماتی را که با یک الگوی ثبتشده نخواند رد میکند.
این یک بار در پروداکشن اتفاق افتاده است. یک مهاجرت در همین مخزن ثبت کرده که ستون pattern_code قالب هیچوقت پر نشده بود، «پس هر کد سفارش و هر کد ورود شکست میخورد».
ثبت الگو
ثبت در دو جا انجام میشود و فقط یکی از آن دو سگمنتیک است.
- نزد اپراتور یا درگاه شما. متن را میفرستید و آنها تایید میکنند. سگمنتیک هیچ اتصالی ندارد که این کار را بکند و هیچ اتصالی ندارد که از کاوهنگار یا
SMS.irبپرسد یک کد تایید شده یا نه. - داخل سگمنتیک، تا مسیر ارسال جواب را بدون پرسیدن از کسی بداند:
GET /v1/sms-patterns template.read
POST /v1/sms-patterns template.write
POST /v1/sms-patterns/{id}/status template.write
اینها روی API پنل هستند. روی میزبان مدیریتی هیچ مسیری برای الگوی پیامک وجود ندارد، پس ثبت الگو و وضعیت تاییدش از یک یکپارچهسازی خودکار نمیشود.
مسیر POST /v1/sms-patterns اینها را میگیرد: provider_id (اجباری و ناصفر)، pattern_code (اجباری)، body (اجباری) و آرایهٔ اختیاری variables. الگوی ذخیرهشده را برمیگرداند: id، provider_id، pattern_code، body، variables، status، status_note، created_at، updated_at.
مسیر POST /v1/sms-patterns/{id}/status بدنهٔ {"status": "...", "note": "..."} میگیرد. حرکتهای مجاز اینهاست:
pending -> approved | rejected اپراتور جواب داد
approved -> revoked اپراتور پسش گرفت
any -> pending متن ثبتشده ویرایش شد
این گذارها در خود SQL و داخل شرط WHERE اعمال میشوند، پس دو نفر که همزمان جواب بدهند نمیتوانند هر دو برنده شوند. ویرایش body یک الگوی تاییدشده آن را به pending برمیگرداند و یادداشتش را پاک میکند؛ ویرایش فقط متغیرها، یا ذخیرهٔ دوبارهٔ متنی که عوض نشده، وضعیت را دست نمیزند. الگوی پسگرفتهشده با فراخوان وضعیت دوباره تایید نمیشود، دوباره ثبت میشود و روی pending مینشیند. یکتایی روی (حساب، ارائهدهنده، کد الگو) است.
وصل کردن الگو به قالب
دو فیلد روی قالب این کار را میکنند:
pattern_code: کد اپراتورpattern_tokens: نگاشتی از نام توکنهای درگاه به نام متغیرهای خودتان
این دو فضای نام واقعا با هم فرق دارند. مال شما first_name و order_id است؛ مال آنها اغلب token و token2 و token3 است.
توکن الگو از نگاشت data قالب پر میشود، نه مستقیم از vars. مسیر ارسال مقدار را از data رندرشده برمیدارد، پس متغیری که در vars میفرستید فقط وقتی به درگاه میرسد که نگاشت data قالب کلیدی داشته باشد که آن را رندر کند.
شکل درست، برای الگویی که متن تاییدشدهاش «کد ورود شما: %token%» است و نام توکنش token:
{
"name": "کد ورود",
"channel": "sms",
"category": "transactional",
"body": "کد ورود شما: {{code}}",
"data": { "code": "{{code}}" },
"pattern_code": "verify-login",
"pattern_tokens": { "token": "code" }
}
{
"user_id": "u_9137",
"channel": "sms",
"template_id": 42,
"idempotency_key": "otp:2026-08-01:u_9137",
"vars": { "code": "8391" }
}
مقدارهایی که از data رد میشوند رقمهای لاتینشان را نگه میدارند، چون data را ماشین میخواند.
تایید الگو همراه خود قالب و در یک کوئری خوانده و برای همان سی ثانیه کش میشود، با EXISTS (... status = 'approved') روی کد الگو. ارائهدهنده عمدا جزو این شرط نیست، چون اینکه کدام درگاه پیام را حمل میکند موقع ارسال با مسیریابی تصمیم گرفته میشود.
بدون الگو چه میشود
| شرط | نتیجه |
|---|---|
خط خدماتی، قالب pattern_code ندارد | قبل از درگاه رد میشود: failed و sms: a service line requires an approved pattern |
| خط خدماتی، کد هست ولی الان تایید نیست | قبل از درگاه رد میشود: failed و sms: this pattern is not approved by the operator |
| شماره بهعنوان موبایل ایران خوانده نمیشود | failed و sms: not a valid Iranian mobile number |
| اصلا خط خدماتی پیکربندی نشده | failed و بدون ارائهدهنده. بیسروصدا روی خط تبلیغاتی برنمیگردد. |
رد کردن اینجا بهجای واگذاشتنش به تنبیه درگاه، عمدی است: فرستادن زیر الگوی پسگرفتهشده همان تخلفی است که خط را از مشتری میگیرد، و با خط، هر کد سفارش و هر کد ورودی که میفرستد.
دو قاعدهٔ دیگر از همین یک واقعیت درمیآید:
- کوتاهکردن لینک روی خط خدماتی هرگز اعمال نمیشود. ملاک، وجود کد الگوست. بازنویسی هر تکه از یک الگوی تاییدشده باعث میشود دیگر با آن نخواند.
- پانویس لغو هیچوقت به متن تراکنشی اضافه نمیشود. به متن یک پیامک تبلیغاتی
لغو۱۱اضافه میشود؛ متن تراکنشی دقیق همانطور که تایید شده رها میشود، به همان دلیل.
نمونههای کامل
curl
curl -sS -X POST https://api.segmentic.net/v1/messages \
-H "Authorization: Bearer sk_seg_..." \
-H "Content-Type: application/json" \
-d '{
"user_id": "u_9137",
"channel": "sms",
"template_id": 42,
"category": "transactional",
"idempotency_key": "order-8821-shipped",
"vars": { "code": "8391" }
}'
{
"message_id": "t7.order-8821-shipped",
"status": "sent",
"sent_at": "2026-08-01T12:00:00Z"
}
Node
حلقهٔ تلاش مجدد همان تکهای است که ارزش کپی کردن دارد. همان کلید را تکرار میکند، پس تکراری که روی یک ارسال تمامشده بنشیند جواب ذخیرهشده را میگیرد نه یک ارسال دوباره.
const KEY = process.env.SEGMENTIC_API_KEY; // sk_seg_...
async function sendTransactional(payload) {
for (let attempt = 0; attempt < 4; attempt++) {
const res = await fetch("https://api.segmentic.net/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (res.status === 409 || res.status === 429 || res.status === 503) {
const wait = Number(res.headers.get("Retry-After") || 1);
await new Promise((r) => setTimeout(r, wait * 1000));
continue; // همان کلید، هیچوقت کلید تازه
}
const out = await res.json();
if (!res.ok) throw new Error(`segmentic ${res.status}: ${JSON.stringify(out)}`);
return out;
}
throw new Error("segmentic: gave up after 4 attempts");
}
// یک مرسوله، یک کلید، ساختهشده از خود مرسوله.
const result = await sendTransactional({
user_id: "u_9137",
channel: "sms",
template_id: 42,
category: "transactional",
idempotency_key: "order-8821-shipped",
vars: { code: "8391" },
});
if (result.status !== "sent") {
// suppressed یا deferred یا failed. همهٔ اینها با HTTP 200 میآیند.
console.warn("not delivered:", result.status, result.reason, result.error);
}
کاری که این نقطهٔ ورود نمیکند
هرکدام از اینها در کد نیست، نه اینکه فقط مستند نشده باشد.
- جستجو با کلید یکتایی وجود ندارد. وقتی پاسخ را از دست دادید، روی میزبان مدیریتی مسیری نیست که جواب بدهد «کلید فلان چه ساخت». اگر شناسه پیام را دارید، گزارش پیامها روی API پنل با
GET /v1/messages?message_id=...همان ردیف را پیدا میکند. جستوجوی کاربر، گیرنده و بازه زمانی هم در همان مسیر وجود دارد. - مسیر گزارش پیامها روی میزبان مدیریتی نیست.
POST /v1/messagesهست؛GET /v1/messagesنیست. - مسیرهای قالب روی میزبان مدیریتی نیستند. بخش قالبها را ببینید.
- مسیرهای الگوی پیامک روی میزبان مدیریتی نیستند. بخش ثبت الگو را ببینید.
- زمانبندی ندارد. فیلدی برای «فلان ساعت بفرست» وجود ندارد. این فراخوان یا همین حالا میفرستد یا نمیفرستد.
- لغو و فراخوانی برگشت ندارد. فراخوانی برگشت روی پیامی اعمال میشود که در صف منتظر مانده باشد. این یکی منتظر نمیماند.
- پیوست و محتوای خطی از هیچ نوعی ندارد.
- برای یک پیام، فراخوان برگشتی رسید تحویل ندارد. رسید اپراتور پیامک جمع میشود ولی فقط بهصورت تجمعی و در گزارش یک کمپین دیده میشود. چیزی که میتوانید مشترکش شوید جریان معمولی رویدادهاست:
message_sentوmessage_failedمثل هر رویداد دیگری هستند، پس یک رله میتواند بیرون بفرستدشان. بخش وبهوک را ببینید.