پرش به محتوا
مستندات
EN
ورود به پنل
شروع
  • نقشهٔ مستندات
  • راه‌اندازی سریع
  • مفهوم‌ها
جمع‌آوری داده
  • تعریف رویداد
  • فرهنگ‌نامهٔ رویدادها
  • گذاشتن رویداد
  • ادغام هویت
  • SDK وب
  • SDK اندروید
  • ثبت دستگاه
  • سرور به سرور
  • کاتالوگ محصولات
  • وب‌هوک
مخاطب و پیام‌رسانی
  • سگمنت
  • سناریو
  • پیام تراکنشی
  • رضایت و سقف
  • پیام درون‌برنامه‌ای
تحلیل و خروجی
  • گزارش و خروجی
مرجع توسعه‌دهنده
  • مرجع API
    • نقاط ورود داده
    • API مدیریتی
  • کدهای خطا
  • سقف‌ها
  • OpenAPI
ابزارهای توسعه
  • سرور MCP
  • کار با عامل
حریم خصوصی و تغییرات
  • داده‌های شخصی
  • نسخه و تغییرها

ارسال پیام تراکنشی

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

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

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

#نقطهٔ ورود

MESSAGE SOURCES
Transactional APIOne request
CampaignAudience send
JourneyAutomated action
SEGMENTICDelivery routerApply idempotency, policy and provider routing
CHANNELS
SMSCustomer line
PushDevice route
Email and in-appRendered content
مسیر درخواست تراکنشی، کمپین و سناریو تا کانال درست ارسال
HTTP
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 کیلوبایت).

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

  1. user_id خالی نباشد
  2. template_id صفر نباشد
  3. idempotency_key خالی نباشد
  4. idempotency_key با الگویش بخواند
  5. vars حداکثر ۴۰ عضو داشته باشد
  6. category یکی از transactional و critical یا خالی باشد
  7. 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-shippedshort (کمتر از هشت نویسه)
otp:2026-08-01:u_9137has space
a1b2c3d4quote'inside
x.y_z-1:2semi;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 | مشتری عزیز }}.

سه منبع با هم ترکیب می‌شوند، از ضعیف‌ترین، و هرکدام قبلی را کنار می‌زند:

  1. متغیرهای پیام‌ها، که یک بار برای کل حساب تعریف می‌شوند (GET/PUT /v1/settings/content-vars روی API پنل)
  2. پروندهٔ کاربر و اولین دستگاهش: 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
  3. همین 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 حرف درگاه را دارد.

یک ارسال موفق:

JSON
{
  "message_id": "t7.order-8821-shipped",
  "status": "sent",
  "sent_at": "2026-08-01T12:00:00Z"
}

تکرار کلیدی که ارسال اولش رد شده بود:

JSON
{
  "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.

#همهٔ شکست‌ها

کدبدنهعلتچه کار کنید
400malformed JSON: ...بدنهٔ ناخوانا، فیلد ناشناخته، یا بدنهٔ بزرگ‌تر از ۲۵۶ کیلوبایتبدنه را درست کنید. تلاش مجدد کمکی نمی‌کند.
400transactional: user_id is requireduser_id نداردبدنه را درست کنید
400transactional: template_id is requiredtemplate_id نیست یا صفر استبدنه را درست کنید
400transactional: idempotency_key is requiredنه در بدنه کلید هست نه در هدربدنه را درست کنید
400متن مربوط به شکل کلیدکلید با ^[A-Za-z0-9._:-]{8,200}$ نمی‌خواندکلید را درست کنید. کلید تصادفی تازه نسازید.
400transactional: unknown channelجزو آن هشت مقدار نیستبخش کانال‌ها
400transactional: this endpoint does not send marketing; use a campaign"category": "marketing"کمپین بسازید
400transactional: 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 را بخوانید
409transactional: a message with this idempotency key is already in flight و Retry-After: 1تلاش قبلی خودتان هنوز در جریان استیک ثانیه صبر کنید و همان کلید را دوباره بفرستید
429rate limit exceeded: N requests per minute و Retry-After: 60سقف دقیقه‌ای حسابعقب بکشید
429{"error":{"code":"budget_exhausted"}} و Retry-After: 60بودجهٔ وزنی میزبان مدیریتیعقب بکشید
503budget_unavailableبودجه خوانده نشد و این بررسی بسته می‌شکنددوباره تلاش کنید
503message 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 قالب هیچ‌وقت پر نشده بود، «پس هر کد سفارش و هر کد ورود شکست می‌خورد».

#ثبت الگو

ثبت در دو جا انجام می‌شود و فقط یکی از آن دو سگمنتیک است.

  1. نزد اپراتور یا درگاه شما. متن را می‌فرستید و آن‌ها تایید می‌کنند. سگمنتیک هیچ اتصالی ندارد که این کار را بکند و هیچ اتصالی ندارد که از کاوه‌نگار یا SMS.ir بپرسد یک کد تایید شده یا نه.
  2. داخل سگمنتیک، تا مسیر ارسال جواب را بدون پرسیدن از کسی بداند:
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

Shell
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" }
  }'
200
{
  "message_id": "t7.order-8821-shipped",
  "status": "sent",
  "sent_at": "2026-08-01T12:00:00Z"
}

#Node

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

send.js
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 مثل هر رویداد دیگری هستند، پس یک رله می‌تواند بیرون بفرستدشان. بخش وب‌هوک را ببینید.
قبلیسناریوبعدیرضایت و سقف

در این صفحه

  • نقطهٔ ورود
  • بدنهٔ درخواست
  • کلید یکتایی
  • دسته‌بندی پیام
  • کانال‌ها
  • قالب‌ها
  • شخصی‌سازی
  • پاسخ
  • همهٔ شکست‌ها
  • سقف نرخ و دو پوشش خطا
  • پیامک ایران: شرط الگو
  • نمونه‌های کامل
  • کاری که این نقطهٔ ورود نمی‌کند

سگمنتیک

این صفحه از روی کد نوشته شده است