پیام درونبرنامهای و صندوق پیام
بنر، مودال و نظرسنجی داخل سایت و اپ، و صندوقی که پیامها در آن میمانند.
پیام درونسایت چیست
قاعدههای نمایش روی سرور ما ارزیابی نمیشوند. در مرورگر بازدیدکننده ارزیابی میشوند.
دلیلش حساب سادهای است. اگر هر بازدید صفحه یک درخواست به ما بزند، یک فروشگاه متوسط ایرانی روزی یک میلیون درخواست به ما میفرستد، آن هم روی مسیر رندر صفحهی خودش: تأخیر ما جلوی محتوای آنها مینشیند و در دسترس بودن ما جلوی کسبوکارشان. بهجای آن، 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.
شکل کمپین روی سیم
| فیلد | نوع | توضیح |
|---|---|---|
id | int64 | |
name | string | نام داخلی، در پنل دیده میشود |
kind | string | banner، modal، slidein یا survey |
status | string | draft، live، paused یا ended |
content | object | زیر همین بخش |
targeting | object | بخش ارزیابی قاعدهها |
max_impressions | int | صفر یعنی بیسقف |
cooldown_hours | int | صفر یعنی بدون فاصله |
dismissible | bool | |
starts_at | RFC3339، اختیاری | |
ends_at | RFC3339، اختیاری | |
impressions | int64 | همیشه هست |
clicks | int64 | همیشه هست |
dismissals | int64 | همیشه هست |
created_by | string، اختیاری | نام نمایشی کاربری که ذخیره کرده، یا 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 | آرایهی رشته. اگر هر کدام در نشانی باشد، رد |
devices | desktop، mobile، tablet. خالی یعنی همه |
delay_seconds | int، هنگام ذخیره به بازهی 0 تا 120 محدود میشود |
scroll_percent | int، هنگام ذخیره به بازهی 0 تا 100 محدود میشود |
on_exit_intent | bool |
new_visitors_only | bool. اگر بازدیدکننده برگشتی باشد، رد |
returning_only | bool. اگر بازدیدکننده برگشتی نباشد، رد |
logged_in | bool اختیاری. نبودنش یعنی هر دو حالت |
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() میدهد، نه با یک نشانی اینترنتی.
سقف نمایش
سقف هم در مرورگر اعمال میشود و هم روی سرور، و هیچکدام به تنهایی کافی نیست: فقط حافظهی محلی یعنی هر کسی که آن را پاک کند یک مودال بیسقف میبیند، و فقط سرور یعنی یک درخواست به ازای هر بازدید صفحه.
ترتیب قاعدهها یکسان است و اهمیت دارد:
- اگر کمپین زنده نیست، خیر.
- اگر این مرورگر قبلا تبدیل شده، دیگر هرگز. این قاعده از همه بالاتر است، حتی از کمپینی که هنوز در جریان است.
- اگر بسته شده و کمپین
dismissibleاست، خیر. - اگر
max_impressions > 0و تعداد نمایشها به آن رسیده، خیر. - اگر
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_id | int64 | بله، صفر رد میشود |
user_id | string | یکی از این دو |
anonymous_id | string | یکی از این دو |
action | string | خیر، خالی یعنی impression |
page_url | string | خیر |
score | int | فقط نظرسنجی |
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 | اثر |
|---|---|
impression | seen_count یکی زیاد میشود، last_seen_at بهروز میشود، شمارندهی impressions کمپین هم زیاد میشود |
dismiss | dismissed_at ست میشود (اولی نگه داشته میشود)، dismissals زیاد میشود |
click | converted_at ست میشود و clicks زیاد میشود |
convert | converted_at ست میشود و هیچ شمارندهای زیاد نمیشود |
دو چیز که در گزارشگیری به آن برمیخورید:
- این رویدادها متر و سهمیهبندی نمیشوند. برخلاف
/v1/trackو/v1/batch، هیچکدام از دو نقطهی پایانی on-site مصرف را نمیشمارد و سقف حساب را چک نمیکند. - این رویدادها روی گذرگاه رویداد منتشر نمیشوند. مستقیم داخل Postgres نوشته میشوند. یعنی در هیچ جریان رویدادی، هیچ رلهای و هیچ جدول ClickHouse دیده نمیشوند، پس نمیتوانید با ابزارهای گزارشگیری معمول رویدادها روی آنها سگمنت بسازید. شمارندههای جمعی روی همان فهرست عمومی کمپینها که بالا آمد هستند، و به همین دلیل هر بازدیدکنندهای میتواند در تب Network ببیندشان. هرچه فراتر از یک عدد جمعی باشد، یعنی نتایج نظرسنجی و تکتک پاسخها، فقط روی مسیرهای کنترلپلین در ساختن و منتشر کردن کمپین است که در پنل به آنها میرسید.
پاسخ نظرسنجی
POST https://in.segmentic.net/v1/onsite/response، با همان ساختار بدنهای که بالا آمد.
ترتیب کار:
- اگر
campaign_idصفر باشد یا شناسهی بازدیدکننده نباشد،400باcampaign_id and a visitor id are required. - کمپین از انبار خوانده میشود و از بدنه باور نمیشود: اینکه این یک نظرسنجی NPS هست یا نه تعیین میکند که
scoreاصلا معنایی دارد یا نه، و مرورگر مرجع این موضوع نیست. اگر خواندن شکست بخورد،400باunknown campaign. - اعتبارسنجی، پنج بررسی، در جدول پایین. خطا
400میگیرد و متن خطا خود همان جملهی انگلیسی است. - اگر ذخیره شکست بخورد،
503باtemporarily unavailable, please retry. این تنها نقطهی پایانی on-site است که میتواند خطای سرور برگرداند. - در موفقیت، یک کنش
convertهم بهصورت best-effort ثبت میشود. کسی که نظرش را گفته نباید هفتهی بعد همان سؤال را ببیند. 200با{"status":"ok"}.
دو تا از این پنجتا اصلا خطا نیستند، و دیدنشان کنار آن سهتای دیگر ارزش دارد:
| بررسی | چه میشود |
|---|---|
kind کمپین survey نیست | 400 با onsite: this campaign is not a survey |
نه user_id هست و نه anonymous_id | 400 با 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 بلندتر از دو هزار نویسه است | بریده میشود، هرگز رد نمیشود |
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/campaigns | campaign.read |
GET /v1/onsite/campaigns/{id} | campaign.read |
PUT /v1/onsite/campaigns | campaign.write |
POST /v1/onsite/campaigns/{id}/status | campaign.send |
GET /v1/onsite/campaigns/{id}/results | campaign.read |
GET /v1/onsite/campaigns/{id}/responses | profile.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 اجازه داشته باشد بکند.
احراز هویت دومرحلهای
اینجا تنها جایی است که کلید نوشتن به تنهایی کافی نیست. صندوق نخستین نقطهی پایانی خواندنی این پلتفرم است، ردیفهایش متن پیام و کد تخفیف شخصیسازیشده دارند، و «صندوق کاربر ۹۱۳۷۲ را بده» پشت کلیدی که هر کسی میتواند از سورس صفحه بخواند، نقطهی پایانیای نیست که بتواند وجود داشته باشد.
پس دو بررسی جدا انجام میشود:
- کلید نوشتن حساب را مشخص میکند. عمومی است و چیزی دربارهی اینکه چه کسی میپرسد نمیگوید.
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. آنچه شما میسازید:
- حساب کردن
user_hashروی سرور خودتان هنگام ورود کاربر.identity_secretنباید هرگز به مرورگر یا به باینری اپ برسد. - صدا زدن
POST /v1/inboxو نگه داشتن نتیجه. - کل رابط کاربری: فهرست، حالت خوانده و نخوانده، ژست رد کردن، و باز کردن
deeplink. - فرستادن
message_openedباmessage_idوtokenتا بازکردنها قابل شمردن باشند. - صفحهبندی. وجود ندارد. بیستوپنج ردیف در هر تماس، بدون 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. ستونش هست و هیچکس رویش نمینویسد. بخش یک نقص، و دو تا که رفع شد.
اگر یکی از اینها مسیر شما را میبندد، بگویید. اینجا نوشته شده تا بعد از یک بعدازظهر کشف نشود.