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

پیام درون‌برنامه‌ای و صندوق پیام

بنر، مودال و نظرسنجی داخل سایت و اپ، و صندوقی که پیام‌ها در آن می‌مانند.

#پیام درون‌سایت چیست

TRIGGERS
Page viewCurrent context
Segment entryAudience match
Custom eventCustomer action
SEGMENTICOn-site decisionCheck eligibility, targeting and frequency caps
EXPERIENCES
BannerInline message
ModalFocused message
SurveyCollect feedback
تبدیل بازدید صفحه، ورود به سگمنت و رویداد به بنر، مودال و نظرسنجی مجاز

قاعده‌های نمایش روی سرور ما ارزیابی نمی‌شوند. در مرورگر بازدیدکننده ارزیابی می‌شوند.

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

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

سه نقطه‌ی پایانی روی collector هستند، با کلید نوشتن wk_seg_...:

متد و مسیرکار
GET /v1/onsiteفهرست کمپین‌های زنده
POST /v1/onsite/eventثبت نمایش، کلیک، بستن یا تبدیل
POST /v1/onsite/responseثبت پاسخ نظرسنجی

هر سه فقط وقتی ثبت می‌شوند که نصب شما یک انبار on-site داشته باشد. اگر نداشته باشد، پاسخ 405 Method Not Allowed است و نه 404، چون الگوی OPTIONS /v1/ که برای CORS ثبت شده تمام پیشوند /v1/ را برمی‌دارد.

کلید نوشتن عمومی است و داخل کد سایت شما می‌نشیند. کلید API با پیشوند sk_seg_ روی https://api.segmentic.net است و هرگز نباید به مرورگر برسد. نمایش دادن کمپین با کلید نوشتن انجام می‌شود. ساختن و منتشر کردنش با هیچ‌کدام از این دو انجام نمی‌شود: آن مسیرها روی کنترل‌پلین‌اند و در پنل و با نشست خود آدم به آن‌ها می‌رسید، همان‌طور که در ساختن و منتشر کردن کمپین آمده.

#گرفتن فهرست کمپین‌ها

GET https://in.segmentic.net/v1/onsite. هیچ پارامتری جز خود کلید ندارد.

کلید از سه جا خوانده می‌شود، به همین ترتیب: هدر Authorization: Bearer، هدر X-Segmentic-Key، یا پارامتر پرس‌وجوی write_key. SDK وب برای این GET پارامتر پرس‌وجو را به کار می‌برد، چون آن‌وقت درخواست یک درخواست ساده است، هیچ preflight ندارد و یک CDN که روی هدر Authorization تنوع نمی‌دهد می‌تواند پاسخ را کش کند.

گرفتن کمپین‌های زنده
curl -s "https://in.segmentic.net/v1/onsite?write_key=wk_seg_..."
پاسخ ۲۰۰
{
  "campaigns": [
    {
      "id": 7,
      "name": "تخفیف عید",
      "kind": "modal",
      "status": "live",
      "content": {
        "headline": "۱۵ درصد تخفیف تا پایان هفته",
        "body": "کد را در سبد خرید وارد کنید.",
        "button_text": "دیدن محصولات",
        "button_url": "/products",
        "accent": "#2563eb"
      },
      "targeting": {
        "url_contains": ["/products"],
        "devices": ["desktop"],
        "delay_seconds": 5
      },
      "max_impressions": 3,
      "cooldown_hours": 24,
      "dismissible": true,
      "starts_at": "2026-08-01T00:00:00Z",
      "ends_at": "2026-08-14T00:00:00Z",
      "impressions": 1842,
      "clicks": 96,
      "dismissals": 311,
      "created_by": "ملیکا",
      "created_at": "2026-07-28T09:12:44Z",
      "updated_at": "2026-07-30T11:02:10Z"
    }
  ],
  "cache_seconds": 60
}

پاسخ هدر Cache-Control: public, max-age=60 دارد و cache_seconds همان عدد را تکرار می‌کند تا مرورگر بدون پرسیدن، همان حساب سقف نمایش را از حافظه‌ی محلی انجام بدهد. فهرست سمت سرور با status = live و پنجره‌ی تاریخ فیلتر شده است.

اگر انبار ما از کار بیفتد پاسخ باز هم 200 است، با {"campaigns": []} و بدون cache_seconds و بدون هدر کش. این کد داخل بارگذاری صفحه‌ی مشتری می‌دود، پس خرابی ما باید به «امروز بنری نیست» تنزل کند و نه به یک خطا در کنسول سایت آن‌ها.

خطاهای احراز هویت:

وضعیتکدبدنه
کلید فرستاده نشده401{"status":"error","message":"missing write key"}
کلید ناشناس، باطل یا معلق401{"status":"error","message":"invalid write key"}
جست‌وجوی کلید خودش شکست خورد503 با Retry-After: 5{"status":"error","message":"cannot verify the write key right now; retry"}

سطر سوم عمدی است. یک SDK کد 401 را «این کلید هیچ‌وقت کار نمی‌کند» می‌خواند، متوقف می‌شود و رویدادها را دور می‌ریزد؛ 503 را «بعدا دوباره امتحان کن» می‌خواند و نگهشان می‌دارد.

CORS روی هر سه نقطه‌ی پایانی: Access-Control-Allow-Origin: *، متدهای POST, OPTIONS، هدرهای Content-Type, Authorization, X-Segmentic-Key، و Access-Control-Max-Age: 86400.

#شکل کمپین روی سیم

فیلدنوعتوضیح
idint64
namestringنام داخلی، در پنل دیده می‌شود
kindstringbanner، modal، slidein یا survey
statusstringdraft، live، paused یا ended
contentobjectزیر همین بخش
targetingobjectبخش ارزیابی قاعده‌ها
max_impressionsintصفر یعنی بی‌سقف
cooldown_hoursintصفر یعنی بدون فاصله
dismissiblebool
starts_atRFC3339، اختیاری
ends_atRFC3339، اختیاری
impressionsint64همیشه هست
clicksint64همیشه هست
dismissalsint64همیشه هست
created_bystring، اختیارینام نمایشی کاربری که ذخیره کرده، یا apikey:<id>
created_atزمان، اختیاری
updated_atزمان، اختیاری

فیلدهای content که همگی اختیاری‌اند: headline، body، image_url، button_text، button_url، position، background، text_color، accent، question، nps (bool)، follow_up، choices (آرایه‌ی رشته)، thank_you.

position برای بنر یعنی بالا یا پایین، و برای پیام گوشه‌ای یعنی کدام گوشه.

#آنچه این پاسخ فاش می‌کند

این پاسخ کل ساختار کمپین است، نه نسخه‌ای خلاصه‌شده. یعنی impressions، clicks، dismissals، created_by، created_at و name هم در آن هستند و هر بازدیدکننده‌ی سایت شما می‌تواند در تب Network آن‌ها را بخواند. تنها چیزی که حذف شده شناسه‌ی حساب است.

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

#ارزیابی قاعده‌ها

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

فیلدهای targeting همگی اختیاری‌اند:

فیلدمعنی
url_containsآرایه‌ی رشته. اگر هر کدام زیررشته‌ی نشانی صفحه باشد، پاس
url_not_containsآرایه‌ی رشته. اگر هر کدام در نشانی باشد، رد
devicesdesktop، mobile، tablet. خالی یعنی همه
delay_secondsint، هنگام ذخیره به بازه‌ی 0 تا 120 محدود می‌شود
scroll_percentint، هنگام ذخیره به بازه‌ی 0 تا 100 محدود می‌شود
on_exit_intentbool
new_visitors_onlybool. اگر بازدیدکننده برگشتی باشد، رد
returning_onlybool. اگر بازدیدکننده برگشتی نباشد، رد
logged_inbool اختیاری. نبودنش یعنی هر دو حالت
traitsنگاشت رشته به رشته. همه‌ی کلیدها باید دقیقا برابر باشند

تطبیق نشانی زیررشته‌ای است و هرگز عبارت باقاعده نیست. عبارت باقاعده‌ای که یک نفر بازاریاب می‌نویسد می‌تواند فاجعه‌بار کند باشد و این کد روی هر صفحه‌ی سایت شما اجرا می‌شود. مجموع url_contains و url_not_contains هنگام ذخیره حداکثر بیست قاعده است.

traits فقط با ویژگی‌هایی مقایسه می‌شود که SDK همان لحظه در حافظه‌ی محلی دارد، یعنی چیزی که خودتان با identify() به آن داده‌اید. با سگمنت‌های انبار داده مقایسه نمی‌شود. اگر می‌خواهید یک کمپین on-site را به یک سگمنت وصل کنید، این کار از اینجا شدنی نیست.

کلاس دستگاه از عرض viewport می‌آید و نه از user agent: کمتر از ۷۶۸ پیکسل mobile، کمتر از ۱۰۲۴ پیکسل tablet، بقیه desktop.

روی اندروید دو تفاوت هست که اگر ندانید کمپینتان هرگز نمایش داده نمی‌شود. اول اینکه فقط دو کلاس وجود دارد: عرض کوچک‌تر صفحه از ۶۰۰ به بالا tablet است و پایین‌تر mobile. پس کمپینی که devices: ["desktop"] دارد روی اندروید هیچ‌وقت واجد شرایط نمی‌شود. دوم اینکه url_contains و url_not_contains روی گوشی با نام صفحه مقایسه می‌شوند، یعنی همان رشته‌ای که اپ به screen() می‌دهد، نه با یک نشانی اینترنتی.

#سقف نمایش

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

ترتیب قاعده‌ها یکسان است و اهمیت دارد:

  1. اگر کمپین زنده نیست، خیر.
  2. اگر این مرورگر قبلا تبدیل شده، دیگر هرگز. این قاعده از همه بالاتر است، حتی از کمپینی که هنوز در جریان است.
  3. اگر بسته شده و کمپین dismissible است، خیر.
  4. اگر max_impressions > 0 و تعداد نمایش‌ها به آن رسیده، خیر.
  5. اگر cooldown_hours > 0 و آخرین نمایش داخل همان بازه بوده، خیر.

یک کلیک هم برای شمردن سقف تبدیل حساب می‌شود. کسی که روی لینک کلیک کرده کار را انجام داده و نشان دادن دوباره یعنی خواستن دوبار.

سابقه‌ی نمایش‌ها در مرورگر زیر کلید sg_onsite در حافظه‌ی محلی نگه داشته می‌شود. مقدار خراب یا نبودنش مثل خالی خوانده می‌شود و خطای سهمیه هنگام نوشتن بی‌صدا رد می‌شود.

#زمان نمایش

  • scroll_percent بزرگ‌تر از صفر: یک شنونده‌ی passive روی scroll وصل می‌شود و در همان درصد آتش می‌کند.
  • on_exit_intent: شنونده‌ی mouseout و آتش کردن وقتی clientY صفر یا کمتر شود. عملا فقط روی دسکتاپ کار می‌کند، چون دستگاه لمسی اشاره‌گری ندارد که به سمت نوار تب برود.
  • delay_seconds فقط وقتی خودش یک ماشه است که هیچ‌کدام از آن دو تنظیم نشده باشند. وگرنه با آن‌ها مسابقه می‌داد و پیام را روی تایمری نشان می‌داد که بازاریاب آن را حداقل در نظر گرفته بود.

#کدام نوع پیام واقعا کشیده می‌شود

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

نوعSDK وبSDK اندرویدSDK iOS
bannerکشیده می‌شودکشیده می‌شودوجود ندارد
modalکشیده می‌شود، با پرده‌ی پشتکشیده می‌شودوجود ندارد
slideinکشیده می‌شود، در گوشهمثل بنر کشیده می‌شود، بدون انیمیشنوجود ندارد
surveyکشیده می‌شود، مقیاس NPS یا فهرست گزینهکشیده نمی‌شودوجود ندارد

روی اندروید، نظرسنجی به‌جای اینکه تقریبی کشیده شود، false برمی‌گرداند. نتیجه‌اش این است که کمپین بدون سقف و بدون گزارش باقی می‌ماند، پس هر وقت این نوع پشتیبانی شد همان کاربر باز هم آن را می‌بیند و فرصت سوخته نشده است.

SDK iOS هیچ کد on-site ندارد. تنها نقطه‌های پایانی‌اش POST /v1/devices و POST /v1/batch هستند. اگر پیام درون‌اپ روی iOS می‌خواهید، باید خودتان GET /v1/onsite را صدا بزنید و ویجت را بکشید.

چند محدودیت رندر در وب که ارزش دانستن دارد:

  • ویجت هیچ‌وقت استثنا پرتاب نمی‌کند. یک ابزار تحلیلی نباید چیزی باشد که پرداخت مشتری را می‌شکند.
  • هیچ استایلی ارث نمی‌برد. هر خصوصیت مستقیم روی خود عنصر ست می‌شود، پس فقط یک !important از سمت صفحه‌ی میزبان می‌تواند بر آن غلبه کند.
  • محتوا با textContent نوشته می‌شود و هرگز با innerHTML. عنوانی که داخلش <img src=x onerror=...> باشد به شکل متن رندر می‌شود.
  • شناسه‌ی ظرف segmentic-onsite است با z-index: 2147483000، عمدا کمی پایین‌تر از بیشینه تا لایه‌ی خود مشتری هنوز بتواند بالاتر بنشیند. ظرف pointer-events: none دارد تا کلیک‌های سایت را نبلعد، و direction: rtl است.
  • همیشه حداکثر یک کمپین در آن واحد نشان داده می‌شود.
  • href دکمه فقط وقتی ست می‌شود که با http:// یا https:// شروع شود یا با یک / جلو بیاید. هر چیز دیگری، از جمله javascript:، دکمه را بدون href می‌گذارد.
  • اگر image_url بارگذاری نشود، خود تصویر حذف می‌شود تا آیکون تصویر شکسته نماند.
  • دکمه‌ی بستن aria-label="بستن" دارد و سمت چپ می‌نشیند، چون فارسی از راست خوانده می‌شود.
  • رنگ‌های پیش‌فرض وقتی محتوا آن‌ها را نیاورد: پس‌زمینه #1f2937، متن #ffffff، رنگ تأکید #2563eb.
  • ردیف NPS به زور direction: ltr می‌گیرد تا صفر سمت چپ و ده سمت راست بنشیند، حتی داخل کارتی که راست‌چین است.

#گزارش رویداد

POST https://in.segmentic.net/v1/onsite/event

فیلدنوعلازم
campaign_idint64بله، صفر رد می‌شود
user_idstringیکی از این دو
anonymous_idstringیکی از این دو
actionstringخیر، خالی یعنی impression
page_urlstringخیر
scoreintفقط نظرسنجی
answersنگاشت رشته به رشتهفقط نظرسنجی

action با حروف کوچک خوانده می‌شود و باید یکی از impression، رشته‌ی خالی، click، dismiss یا convert باشد. هر چیز دیگری 400 با پیام unknown action می‌گیرد.

ثبت یک نمایش
curl -s -X POST https://in.segmentic.net/v1/onsite/event \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{"campaign_id":7,"action":"impression","anonymous_id":"a_9f21c4","user_id":"u_9137"}'
پاسخ
{"status":"ok"}

کلید موضوع، وقتی user_id باشد "u:" + user_id است و وگرنه "a:" + anonymous_id. یک ستون و یک کلید، چون سؤال سقف این است که «این مرورگر آن را دیده یا نه» و کسی که وسط جلسه وارد حساب می‌شود همان یک مرورگر است.

اگر campaign_id نباشد یا هیچ‌کدام از دو شناسه نیامده باشد، پاسخ 400 است با campaign_id and a visitor id are required. بدنه‌ی JSON خراب 400 با پیام malformed JSON می‌گیرد.

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

اثر هر کنش روی انبار:

actionاثر
impressionseen_count یکی زیاد می‌شود، last_seen_at به‌روز می‌شود، شمارنده‌ی impressions کمپین هم زیاد می‌شود
dismissdismissed_at ست می‌شود (اولی نگه داشته می‌شود)، dismissals زیاد می‌شود
clickconverted_at ست می‌شود و clicks زیاد می‌شود
convertconverted_at ست می‌شود و هیچ شمارنده‌ای زیاد نمی‌شود

دو چیز که در گزارش‌گیری به آن برمی‌خورید:

  • این رویدادها متر و سهمیه‌بندی نمی‌شوند. برخلاف /v1/track و /v1/batch، هیچ‌کدام از دو نقطه‌ی پایانی on-site مصرف را نمی‌شمارد و سقف حساب را چک نمی‌کند.
  • این رویدادها روی گذرگاه رویداد منتشر نمی‌شوند. مستقیم داخل Postgres نوشته می‌شوند. یعنی در هیچ جریان رویدادی، هیچ رله‌ای و هیچ جدول ClickHouse دیده نمی‌شوند، پس نمی‌توانید با ابزارهای گزارش‌گیری معمول رویدادها روی آن‌ها سگمنت بسازید. شمارنده‌های جمعی روی همان فهرست عمومی کمپین‌ها که بالا آمد هستند، و به همین دلیل هر بازدیدکننده‌ای می‌تواند در تب Network ببیندشان. هرچه فراتر از یک عدد جمعی باشد، یعنی نتایج نظرسنجی و تک‌تک پاسخ‌ها، فقط روی مسیرهای کنترل‌پلین در ساختن و منتشر کردن کمپین است که در پنل به آن‌ها می‌رسید.

#پاسخ نظرسنجی

POST https://in.segmentic.net/v1/onsite/response، با همان ساختار بدنه‌ای که بالا آمد.

ترتیب کار:

  1. اگر campaign_id صفر باشد یا شناسه‌ی بازدیدکننده نباشد، 400 با campaign_id and a visitor id are required.
  2. کمپین از انبار خوانده می‌شود و از بدنه باور نمی‌شود: اینکه این یک نظرسنجی NPS هست یا نه تعیین می‌کند که score اصلا معنایی دارد یا نه، و مرورگر مرجع این موضوع نیست. اگر خواندن شکست بخورد، 400 با unknown campaign.
  3. اعتبارسنجی، پنج بررسی، در جدول پایین. خطا 400 می‌گیرد و متن خطا خود همان جمله‌ی انگلیسی است.
  4. اگر ذخیره شکست بخورد، 503 با temporarily unavailable, please retry. این تنها نقطه‌ی پایانی on-site است که می‌تواند خطای سرور برگرداند.
  5. در موفقیت، یک کنش convert هم به‌صورت best-effort ثبت می‌شود. کسی که نظرش را گفته نباید هفته‌ی بعد همان سؤال را ببیند.
  6. 200 با {"status":"ok"}.

دو تا از این پنج‌تا اصلا خطا نیستند، و دیدنشان کنار آن سه‌تای دیگر ارزش دارد:

بررسیچه می‌شود
kind کمپین survey نیست400 با onsite: this campaign is not a survey
نه user_id هست و نه anonymous_id400 با onsite: a response must name a browser or a person
content.nps درست است و score بیرون بازه‌ی صفر تا ده است، یا اصلا نیامده400 با onsite: an NPS score must be between 0 and 10
content.nps نادرست استscore با -1 بازنویسی می‌شود، بدون خطا
مقداری در answers بلندتر از دو هزار نویسه استبریده می‌شود، هرگز رد نمی‌شود
پاسخ یک نظرسنجی NPS
curl -s -X POST https://in.segmentic.net/v1/onsite/response \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{"campaign_id":12,"user_id":"u_9137","score":9,"answers":{"reason":"ارسال سریع بود"}}'
پاسخ
{"status":"ok"}

ذخیره روی کلید (حساب، کمپین، user_id، anonymous_id) idempotent است و score و answers را با مقدار تازه جایگزین می‌کند. کسی که پاسخ می‌دهد، صفحه را نو می‌کند و دوباره پاسخ می‌دهد، یک نظر دارد. responded_at از ساعت سرور می‌آید و نه از بدنه.

#یک نقص، و دو تا که رفع شد

page_url را هیچ SDK منتشرشده‌ای پر نمی‌کند. فیلد وجود دارد و ذخیره می‌شود، ولی نه SDK وب و نه SDK اندروید آن را نمی‌فرستند. اگر لازمش دارید، خودتان باید بفرستیدش.

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

پاسخ چندگزینه‌ای و متن آزاد SDK وب بی‌صدا دور ریخته می‌شد. رندرکننده پاسخ را به شکل { score?, choice?, text? } می‌فرستاد و کلاینت همان را مستقیم داخل بدنه POST باز می‌کرد. ساختار سمت سرور نه فیلد choice دارد و نه text، پس encoding/json هر دو را حذف می‌کرد و جواب 200 می‌داد. یعنی برای نظرسنجی چندگزینه‌ای، ردیف ذخیره‌شده می‌گفت این نفر پاسخ داده و نمی‌گفت چه پاسخی داده. حالا رندرکننده نگاشت answers می‌فرستد، همان شکلی که SDK اندروید از اول می‌فرستاد.

پاسخ متنی تکمیلی، نمره واقعی NPS را با صفر بازنویسی می‌کرد. روی نظرسنجی NPS که follow_up دارد، SDK وب دو بار POST می‌کرد: یک بار با {"score": 9} و یک بار با {"text": "..."}. POST دوم score نداشت، پس مقدار صفر Go به کار می‌رفت، اعتبارسنجی هم می‌پذیرفت چون صفر یک نمره منتقد معتبر است، و ذخیره روی همان کلید نمره واقعی را با آن جایگزین می‌کرد. هر پاسخ تکمیلی یک منتقد به حساب شما اضافه می‌کرد. دو چیز عوض شد: هر POST حالا کل پاسخ را می‌برد نه فقط تکه تازه را، و بدنه‌ای که روی نظرسنجی NPS اصلا score ندارد با 400 رد می‌شود به‌جای اینکه صفر خوانده شود.

[!warning] پاسخ‌هایی که قبل از این رفع ثبت شده‌اند برگشتنی نیستند. گزینه‌ای که دور ریخته شده هیچ‌وقت نوشته نشد، و نمره‌ای که بازنویسی شده جای نمره واقعی را گرفت. اگر عدد NPS نظرسنجی‌ای که سؤال تکمیلی داشته جایی نقل شده، آن عدد پایین‌تر از واقعیت بوده، به اندازه یک منتقد به ازای هر نفری که جمله‌ای نوشته. نمره‌هایی که بعد از این رفع جمع می‌شوند سالم‌اند.

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

#SDK وب

on-site به‌طور پیش‌فرض روشن است. یعنی نصب SDK بدون هیچ تنظیم دیگری، کمپین‌های منتشرشده‌ی شما را روی سایتتان می‌کشد.

نصب معمول
import segmentic from "@segmentic/web";

const client = segmentic.init({
  writeKey: "wk_seg_...",
  apiHost: "https://in.segmentic.net",
});

segmentic.identify("u_9137", { city: "تهران", plan: "gold" });

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

گزارش‌ها از صف دسته‌ای رد نمی‌شوند: هر دو POST مستقیم و با keepalive: true می‌روند. یک نمایش نباید پشت دسته‌ای بماند که منتظر نوزده پیام دیگر است، روی صفحه‌ای که بازدیدکننده دارد ترکش می‌کند.

بعد از پیمایش سمت کلاینت (مثلا در یک اپ تک‌صفحه‌ای) خودتان باید ارزیابی را دوباره راه بیندازید:

بعد از تغییر مسیر
client.refreshOnsite();

#ویجت خودتان

اگر سیستم طراحی خودتان را دارید، رندر ما را خاموش کنید و فقط منطق واجد شرایط بودن و سقف نمایش را نگه دارید:

رندر اختصاصی
import segmentic, { eligible, deviceOf, readSeen } from "@segmentic/web";

const client = segmentic.init({
  writeKey: "wk_seg_...",
  apiHost: "https://in.segmentic.net",
  onsite: false,
});

// بعد از اینکه فهرست یک بار گرفته شد.
const visitor = {
  url: location.href,
  device: deviceOf(window.innerWidth),
  loggedIn: segmentic.getUserId() !== null,
  returning: true,
  traits: { plan: "gold" },
  now: Date.now(),
};

const [campaign] = eligible(client.onsiteCampaigns(), visitor, readSeen(localStorage));
if (campaign) {
  drawYourOwnWidget(campaign);
}

onsiteCampaigns() و refreshOnsite() متدهای نمونه‌ی کلاینت‌اند و روی شیء پیش‌فرض ماژول نیستند. یعنی segmentic.onsiteCampaigns() وجود ندارد؛ باید نمونه‌ای را که init() برمی‌گرداند نگه دارید. توابع eligible، matches، maySee، isLive، deviceOf، readSeen، writeSeen، recordSeen و recordAction از خود ماژول export شده‌اند.

با onsite: false هیچ گزارشی هم خودکار فرستاده نمی‌شود. خودتان باید POST /v1/onsite/event را برای نمایش، کلیک و بستن صدا بزنید، وگرنه سقف نمایش سمت سرور هیچ‌وقت پر نمی‌شود و آمار کمپین صفر می‌ماند.

#ساختن و منتشر کردن کمپین

این شش مسیر روی کنترل‌پلین‌اند، یعنی همان APIای که پنل با آن حرف می‌زند. روی میزبان مدیریتی هیچ مسیری برای مدیریت کمپین on-site وجود ندارد.

کنترل‌پلین از اینترنت مسیردهی نشده است. در استقرار مرجع، api.segmentic.net فقط API مدیریتی را روی شنونده‌ی خودش سرو می‌کند و شنونده‌ی کنترل‌پلین عمدا منتشر نشده است. همین مسیرها روی https://api.segmentic.net به هندلر پیش‌فرض می‌افتند و 404 unknown_endpoint می‌گیرند. تنها مسیر عمومی به جدول زیر، پروکسی سمت سرور خود پنل است روی https://app.segmentic.net/api/proxy/v1/... که با کوکی نشست کاربر واردشده احراز هویت می‌کند و بدون آن 401 می‌دهد. کلید sk_seg_ به آن نمی‌رسد. پس ساختن و منتشر کردن کمپین on-site کاری است که یک آدم در پنل انجام می‌دهد.

متد و مسیردسترسی لازم
GET /v1/onsite/campaignscampaign.read
GET /v1/onsite/campaigns/{id}campaign.read
PUT /v1/onsite/campaignscampaign.write
POST /v1/onsite/campaigns/{id}/statuscampaign.send
GET /v1/onsite/campaigns/{id}/resultscampaign.read
GET /v1/onsite/campaigns/{id}/responsesprofile.read

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

ذخیره کردن هرگز منتشر نمی‌کند. اگر در بدنه‌ی PUT مقدار status را live بگذارید، به draft بازنویسی می‌شود. status خالی هم draft می‌شود.

ساختن یک بنر
PUT /api/proxy/v1/onsite/campaigns
Content-Type: application/json

{
  "campaign": {
    "name": "ارسال رایگان مرداد",
    "kind": "banner",
    "content": {
      "headline": "ارسال رایگان تا پایان مرداد",
      "button_text": "خرید",
      "button_url": "/products",
      "position": "top"
    },
    "targeting": { "url_not_contains": ["/checkout"], "delay_seconds": 3 },
    "max_impressions": 3,
    "cooldown_hours": 24,
    "dismissible": true
  }
}
زنده کردنش
POST /api/proxy/v1/onsite/campaigns/7/status
Content-Type: application/json

{"status":"live"}
پاسخ
{"id":7,"status":"live"}

status فقط draft، live، paused یا ended را می‌پذیرد و هر چیز دیگری 400 می‌گیرد.

خطاهای اعتبارسنجی PUT، همگی با کد 400 و متن فارسی:

شرطچه وقت
نام لازم استname بعد از trim خالی باشد
نوع ناشناسkind یکی از آن چهارتا نباشد
محتوا لازم استکمپین غیرنظرسنجی که هم headline و هم body خالی دارد
سؤال لازم استنظرسنجی بدون question
پیوند نامعتبرbutton_url یا image_url که با http://، https:// یا / شروع نشود
هر دو مخاطب با همnew_visitors_only و returning_only هر دو درست
قاعده‌های زیادیبیش از بیست قاعده‌ی نشانی
گزینه‌های زیادینظرسنجی با بیش از هشت گزینه

مقادیری که هنگام ذخیره خودکار اصلاح می‌شوند:

  • max_impressions کوچک‌تر یا مساوی صفر به 3 تبدیل می‌شود.
  • cooldown_hours منفی به 24 تبدیل می‌شود. توجه کنید که صفر صریح دست‌نخورده می‌ماند و یعنی بدون فاصله.
  • kind برابر modal مقدار dismissible را به زور درست می‌کند. مودالی که بسته نمی‌شود پیام نیست، گروگان‌گیری است.
  • نظرسنجی NPS فهرست choices را از دست می‌دهد.
  • دکمه‌ای که متن دارد و نشانی ندارد، یا برعکس، نیمه‌ی باقی‌مانده‌اش حذف می‌شود.
  • javascript: و data: رد می‌شوند. آن پیوند داخل صفحه‌ی خود مشتری رندر می‌شود و در همان مبدأ و با همان کوکی‌ها اجرا می‌شد.

GET /v1/onsite/campaigns حداکثر صد کمپین برمی‌گرداند و پاسخش {"campaigns": [...], "kinds": [...]} است. هر کمپین علاوه بر فیلدهای معمول، kind_label، status_label و ctr دارد. ctr برابر clicks / impressions * 100 است و با صفر نمایش، صفر می‌شود و نه خطای تقسیم.

#نتیجه‌ی NPS

خواندن نتیجه
GET /api/proxy/v1/onsite/campaigns/12/results
پاسخ
{
  "nps": {
    "responses": 128,
    "promoters": 61,
    "passives": 40,
    "detractors": 27,
    "score": 26.5625,
    "reliable": true
  },
  "min_reliable": 50
}

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

سطل‌ها ثابت‌اند و قابل تنظیم نیستند: نه و ده ترویج‌کننده، هفت و هشت بی‌تفاوت، صفر تا شش منتقد. NPS فقط به این دلیل ارزش نقل کردن دارد که همه‌جا یک معنی می‌دهد.

reliable زیر پنجاه پاسخ نادرست است. NPS با یازده پاسخ، با یک پاسخ بیشتر بیست واحد جابه‌جا می‌شود، و عددی که در جلسه‌ی هیئت‌مدیره نقل می‌شود نباید این‌طور باشد.

پاسخ‌های خام:

خواندن پاسخ‌های خام
GET /api/proxy/v1/onsite/campaigns/12/responses?limit=200

limit بین 1 و 500 پذیرفته می‌شود و هر مقدار بیرون این بازه، از جمله مقدار نامعتبر یا نبودنش، به 100 تبدیل می‌شود. پاسخ‌ها از تازه به قدیم مرتب‌اند. صفحه‌بندی وجود ندارد: نه cursor و نه offset.

#صندوق پیام

صندوق پیام جای دیگری است که پیام‌های کمپین در آن می‌مانند تا اپ دفعه‌ی بعد باز شود. دو نقطه‌ی پایانی روی collector دارد و هر دو POST‌اند:

POST /v1/inbox
POST /v1/inbox/ack

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

#احراز هویت دومرحله‌ای

اینجا تنها جایی است که کلید نوشتن به تنهایی کافی نیست. صندوق نخستین نقطه‌ی پایانی خواندنی این پلتفرم است، ردیف‌هایش متن پیام و کد تخفیف شخصی‌سازی‌شده دارند، و «صندوق کاربر ۹۱۳۷۲ را بده» پشت کلیدی که هر کسی می‌تواند از سورس صفحه بخواند، نقطه‌ی پایانی‌ای نیست که بتواند وجود داشته باشد.

پس دو بررسی جدا انجام می‌شود:

  1. کلید نوشتن حساب را مشخص می‌کند. عمومی است و چیزی درباره‌ی اینکه چه کسی می‌پرسد نمی‌گوید.
  2. user_hash ثابت می‌کند که سرور خود شما این آدم را احراز هویت کرده است.

فرمول، که سرور شما هنگام ورود کاربر آن را حساب می‌کند:

فرمول user_hash
user_hash = hex(hmac_sha256(identity_secret, user_id))

مقایسه در زمان ثابت انجام می‌شود و مقدار ورودی trim و به حروف کوچک تبدیل می‌شود. اگر حساب identity_secret نداشته باشد، یا user_id خالی باشد، یا اثبات خالی باشد، نتیجه نادرست است.

راهی برای ساختن identity_secret از پنل یا از API وجود ندارد. تنها نویسنده‌ی این مقدار در کل درخت کد، ابزار خط فرمان adminctl است که سی‌ودو بایت تصادفی می‌سازد، آن را base64url می‌کند و فرمول بالا را چاپ می‌کند. یعنی مشتری نمی‌تواند خودش صندوق پیام را راه بیندازد و باید از ما بخواهد. چرخاندن این کلید هم هر هش صادرشده را باطل می‌کند و تا وقتی مشتری دوباره deploy نکند، کل اپش از صندوقش بیرون می‌افتد.

پاسخ‌های خطا:

حالتکدبدنه
user_id نیامده400{"status":"error","message":"user_id is required"}
هش غلط، هش نیامده، یا حسابی که اصلا identity_secret ندارد403{"status":"error","message":"user identity is not verified"}

کد 403 و نه 404، و پاسخ در هر دو حالت یکسان، تا نشود از این نقطه‌ی پایانی به‌عنوان اوراکلی برای فهمیدن اینکه چه شناسه‌های کاربری وجود دارند استفاده کرد.

گرفتن صندوق
curl -s -X POST https://in.segmentic.net/v1/inbox \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{"user_id":"u_9137","user_hash":"3f2a1c...","limit":25}'
پاسخ
{
  "status": "ok",
  "messages": [
    {
      "message_id": "c104.u_9137",
      "title": "سفارش شما ارسال شد",
      "body": "کد رهگیری در اپلیکیشن قابل مشاهده است.",
      "image": "https://cdn.example.ir/box.png",
      "deeplink": "myapp://orders/104",
      "surface": "inbox",
      "token": "1.42.k1.oi.mfx2b1.QkNERUZHSElK",
      "created_at": "2026-08-06T08:11:00Z",
      "expires_at": "2026-09-05T08:11:00Z",
      "seen": false
    }
  ]
}

messages همیشه آرایه است و هرگز null نیست. اگر انبار از کار بیفتد، پاسخ 503 است با {"status":"error","messages":null,"message":"temporarily unavailable, please retry"}.

limit کوچک‌تر یا مساوی صفر یا بزرگ‌تر از 25 به 25 تبدیل می‌شود. فقط ردیف‌هایی برمی‌گردند که رد نشده‌اند و منقضی نشده‌اند، و از تازه به قدیم مرتب‌اند. همان دستور، delivered_at را هم ست می‌کند، چون این همان لحظه‌ای است که می‌شود اثباتش کرد: بایت‌ها از سرور خارج شدند. اینکه آن آدم بعدش نگاه کرده یا نه seen_at است و ادعای دیگری و ضعیف‌تری است.

token همان چیزی است که ثابت می‌کند بازکردنی که اپ گزارش می‌کند به پیامی مربوط است که ما واقعا فرستاده‌ایم. بدون آن، هر بازکردن درون‌اپ یک ادعای بی‌امضاست که دفتر ثبتش می‌کند و نمی‌شمارد.

تأیید دیده شدن یا رد کردن:

تأیید
curl -s -X POST https://in.segmentic.net/v1/inbox/ack \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{"user_id":"u_9137","user_hash":"3f2a1c...","seen":["c104.u_9137"],"dismissed":["c99.u_9137"]}'
پاسخ
{"status":"ok"}

seen نخستین لحظه‌ی دیده شدن را نگه می‌دارد و بازنویسی نمی‌شود، تا پیامی که در هر بار باز شدن اپ دوباره رندر می‌شود، برای همیشه زمان بازکردنش را «همین الان» گزارش نکند. dismissed ردیف را از فهرست‌های بعدی حذف می‌کند. آرایه‌ی خالی خطا نیست و هیچ کاری نمی‌کند. شکست هر کدام از این دو 503 می‌گیرد.

seen سیگنال تعامل نیست. برای اینکه یک بازکردن درون‌اپ شمرده شود، اپ باید یک رویداد message_opened از مسیر عادی رویداد بفرستد، با context.campaign.message_id و context.campaign.token که خود صندوق به آن داده است. راه دوم و «مورد اعتماد» برای همان سیگنال، چیزی می‌شد که فقط به حرف تماس‌گیرنده‌ای اعتماد می‌کرد که چیزی جز یک کلید نوشتن عمومی ندارد.

#آنچه اپ شما باید بسازد

هیچ SDK منتشرشده‌ای کلاینت صندوق ندارد. نه وب، نه اندروید، نه iOS. آنچه شما می‌سازید:

  1. حساب کردن user_hash روی سرور خودتان هنگام ورود کاربر. identity_secret نباید هرگز به مرورگر یا به باینری اپ برسد.
  2. صدا زدن POST /v1/inbox و نگه داشتن نتیجه.
  3. کل رابط کاربری: فهرست، حالت خوانده و نخوانده، ژست رد کردن، و باز کردن deeplink.
  4. فرستادن message_opened با message_id و token تا بازکردن‌ها قابل شمردن باشند.
  5. صفحه‌بندی. وجود ندارد. بیست‌وپنج ردیف در هر تماس، بدون cursor و بدون offset. اگر کاربری صد پیام دارد، فقط بیست‌وپنج تای تازه‌تر را می‌بینید تا وقتی بقیه را رد کنید یا منقضی شوند.

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

#چه چیزی امروز نیست

فهرست صادقانه‌ی کارهایی که یک مشتری انتظارشان را دارد و امروز وجود ندارند:

  • on-site روی iOS. هیچ کدی نیست.
  • نظرسنجی روی اندروید. رندرکننده false برمی‌گرداند.
  • انیمیشن slide-in روی اندروید. مثل بنر کشیده می‌شود.
  • وصل کردن هدف‌گیری on-site به سگمنت. traits فقط با ویژگی‌های محلی SDK مقایسه می‌شود.
  • صفحه‌بندی صندوق پیام و کلاینت صندوق در هر SDK.
  • ساختن identity_secret بدون ما. فقط از خط فرمان.
  • دیدن رویدادهای on-site در گزارش‌های رویدادمحور. روی گذرگاه منتشر نمی‌شوند.
  • خاموش کردن شمارنده‌ها و created_by در پاسخ عمومی GET /v1/onsite.
  • پر کردن page_url از سمت SDK. ستونش هست و هیچ‌کس رویش نمی‌نویسد. بخش یک نقص، و دو تا که رفع شد.

اگر یکی از این‌ها مسیر شما را می‌بندد، بگویید. اینجا نوشته شده تا بعد از یک بعدازظهر کشف نشود.

قبلیرضایت و سقفبعدیگزارش و خروجی

در این صفحه

  • پیام درون‌سایت چیست
  • گرفتن فهرست کمپین‌ها
  • ارزیابی قاعده‌ها
  • کدام نوع پیام واقعا کشیده می‌شود
  • گزارش رویداد
  • پاسخ نظرسنجی
  • SDK وب
  • ساختن و منتشر کردن کمپین
  • صندوق پیام
  • چه چیزی امروز نیست

سگمنتیک

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