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

مرجع: نقاط ورود داده

هر مسیر روی میزبان ورود داده، با درخواست و پاسخ کامل و قابل کپی.

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

#میزبان

https://in.segmentic.net

روی ماشین خودتان، کالکتور به http://localhost:8080 گوش می‌دهد.

کل کار کالکتور این است: اعتبارسنجی کن، تکراری‌ها را بردار، سریع تحویل بده، و هرگز به یک SDK نگو داده را دور بریزد چون مشکلی سمت ما هست. همین یک جمله بیشتر کدهای وضعیت پایین را توضیح می‌دهد: قطعی سمت ما یک 503 است که SDK دوباره امتحان می‌کند، نه یک 401 که آن را دائمی بفهمد.

https://in.segmentic.ir نام قدیمی است و عمدا به اینجا ریدایرکت نمی‌شود. SDKای که هنوز به آن اشاره می‌کند عمدا خراب می‌شود، نه اینکه ظاهرا کار کند.

#احراز هویت با کلید نوشتن

کلید نوشتن شکل wk_seg_ است به‌علاوه ۴۳ کاراکتر base64url. می‌شود آن را در سه جا گذاشت و به همین ترتیب خوانده می‌شوند؛ اولین جایی که پیدا شود برنده است:

  1. Authorization: Bearer wk_seg_...
  2. X-Segmentic-Key: wk_seg_...
  3. ?write_key=wk_seg_... در کوئری‌استرینگ

شکل عادی همان هدر است. پارامتر کوئری برای این هست که یک image beacon و یک فراخوان navigator.sendBeacon نمی‌توانند هدر بگذارند، و SDK وب همین را برای GET /v1/onsite به کار می‌برد تا آن درخواست یک GET ساده بین‌دامنه‌ای بماند و preflight نخواهد.

از هدر Authorization فقط دقیقا پیشوند Bearer (با B بزرگ و یک فاصله) برداشته می‌شود. هر طرح دیگری رد نمی‌شود بلکه به جای بعدی می‌افتد، پس Authorization: Token wk_seg_... یعنی «اینجا توکن bearer نیست» و آن هدر کلا نادیده گرفته می‌شود.

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

کلیدهای حل‌شده یک دقیقه کش می‌شوند (WRITE_KEY_CACHE)، و شکست‌ها هم همین‌طور، چون اپی که با کلید غلط منتشر شده باشد وگرنه تا ابد دیتابیس را می‌کوبد.

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

#وقتی احراز هویت شکست می‌خورد

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

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

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

#CORS

هر نقطه‌ای که کلید نوشتن می‌خواهد، این هدرها را قبل از هر کاری می‌نویسد، حتی قبل از احراز هویت، تا مرورگر کد وضعیت واقعی را ببیند نه یک خطای CORS روی 401 یا 413:

HTTP
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Segmentic-Key
Access-Control-Max-Age: 86400

OPTIONS روی هر مسیری زیر /v1/ جواب 204 می‌دهد با همان هدرها و بدون بدنه.

Access-Control-Allow-Credentials هرگز ست نمی‌شود، و همین است که origin آزاد را بی‌خطر می‌کند. کلید نوشتن تنها اعتبارنامه‌ای است که این میزبان می‌پذیرد و عمدا عمومی است، پس هیچ اعتبارنامه محیطی‌ای نیست که مرورگر خودکار بچسباند و هیچ چیزی نیست که origin آزاد لو بدهد. کوکی به اینجا نفرستید؛ خوانده نمی‌شود.

GET /v1/status و نقاط ایمیلی زیر /e/ هدر CORS ندارند. آن‌ها از داخل صفحه صدا زده نمی‌شوند.

#بدنه درخواست

JSON، تا ۵ مگابایت (5242880 بایت) در هر درخواست. بیشتر از آن 413 است با {"status":"error","message":"request body too large"}.

Content-Type بررسی نمی‌شود. هندلرها بدنه را می‌خوانند و هرچه درخواست ادعا کرده باشد آن را JSON می‌خوانند، پس text/plain با بدنه JSON امروز کار می‌کند. با این حال application/json بفرستید.

هرچه JSON معتبر نباشد 400 است با {"status":"error","message":"malformed JSON"}. توجه کنید که این شامل بدنه سالمی هم می‌شود که یک زمان بدشکل دارد: timestamp و sent_at فقط به شکل RFC 3339 دیکود می‌شوند، پس "timestamp": 1786000000 کل درخواست را به‌عنوان JSON خراب رد می‌کند، نه به‌عنوان یک فیلد غلط.

#فیلدهای بدنه رویداد

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

فیلدنوعلازمتوضیح
eventرشتهفقط روی /v1/trackبعد از یکدست‌سازی حداکثر ۱۲۸ بایت. فارسی اشکالی ندارد، فاصله‌ها می‌مانند، حروف کوچک و بزرگ عوض نمی‌شوند و قاعده snake_case وجود ندارد
user_idرشتهیکی از user_id یا anonymous_idحداکثر ۲۵۶ بایت
anonymous_idرشتهیکی از آن دوحداکثر ۲۵۶ بایت، بدون قاعده قالب، لازم نیست UUID باشد
previous_idرشتهفقط روی /v1/aliasشناسه‌ای که ادغام می‌شود. هیچ سقف طولی ندارد، برخلاف دو تای بالا
message_idرشتهنه، ولی بفرستیدحداکثر ۲۵۶ بایت. بدون آن، تلاش دوباره به‌عنوان تکراری شناخته نمی‌شود
timestampRFC 3339نهپیش‌فرض همان لحظه‌ای است که ما دریافت کردیم
sent_atRFC 3339نهاصلاح اختلاف ساعت را ممکن می‌کند
propertiesشیءنهحداکثر ۲۵۶ کلید، هر کلید حداکثر ۱۲۸ بایت، مقدار رشته‌ای حداکثر ۸۱۹۲ بایت
traitsشیءنهحداکثر ۲۵۶ کلید، با همان سقف مقدار
contextشیءنهپایین‌تر
typeرشتهاینجا نادیده گرفته می‌شودنوع را مسیر تعیین می‌کند. فقط روی آیتم‌های بسته لازم است

type در بدنه یک درخواست تک‌رویدادی با مسیر بازنویسی می‌شود، پس POST /v1/track هر چیزی هم که بدنه بگوید فقط می‌تواند رویداد track بسازد. این خطای اعتبارسنجی نیست؛ فیلد فقط جایگزین می‌شود.

سقف ۲۵۶ بایت به سه شکل متفاوت اعمال می‌شود و همین تفاوت گاز می‌گیرد. user_id و anonymous_id وقتی بلند باشند رد می‌شوند، با id_too_long. context.session_id روی ۲۵۶ بایت بریده می‌شود، بی‌صدا، پس شناسه نشست بلند تبدیل به یک شناسه نشست دیگر می‌شود. previous_id هیچ‌کدام نیست: کامل ذخیره می‌شود و تنها چیزی که محدودش می‌کند سقف ۵ مگابایتی بدنه است.

کلیدهای ویژگی و خصیصه یکدست می‌شوند: فضای اضافه بریده می‌شود، کاراکترهای کنترلی حذف می‌شوند، بعد هر دسته فاصله و هر . و - به یک _ تبدیل می‌شود و زیرخط ابتدا و انتها برداشته می‌شود. پس " spaced key " می‌شود spaced_key، و dotted.key می‌شود dotted_key و dashed-key می‌شود dashed_key. کلیدی که بعد از یکدست‌سازی خالی شود، کلا رد می‌شود.

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

#شیء context

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

فیلدذخیره می‌شود به‌عنوانسقف
context.app.versionapp_version۶۴
context.device.typedevice_type۳۲
context.device.modeldevice_model۱۲۸
context.device.manufacturerdevice_vendor۶۴
context.device.push_providerpush_provider۱۶
context.os.name و context.os.versionos_name (با حروف کوچک) و os_versionهرکدام ۳۲
context.network.carriercarrier۶۴
context.page.url و .path و .referrerpage_url و page_path و page_referrerهرکدام ۲۰۴۸
context.page.titlepage_title۵۱۲
context.campaign.source و .medium و .name و .term و .contentutm_source و utm_medium و utm_campaign و utm_term و utm_contentهرکدام ۱۲۸
context.campaign.campaign_id و .journey_idcampaign_id و journey_idعددی
context.campaign.variant_id و .message_id و .tokenvariant_id و source_message_id و sg_t۶۴ و ۲۵۶ و ۱۲۸
context.locale و .timezone و .session_idlocale و timezone و session_id۳۲ و ۶۴ و ۲۵۶
context.location.country و .region و .citycountry و region و cityهرکدام ۶۴
context.ipip، فقط وقتی از خود اتصال چیزی نگرفته باشیم۶۴
context.user_agentهیچ. هدر User-Agent برنده است
context.screen.width و .height و .densityهیچ. پذیرفته و دور ریخته می‌شود
context.location.latitude و .longitudeهیچ. پذیرفته و دور ریخته می‌شود
context.device.id و .name و .push_token و .has_gms و .ad_tracking_enabledهیچ. پذیرفته و دور ریخته می‌شود

context.device.push_token عمدا دور ریخته می‌شود. فقط مسیر نگه داشته می‌شود و هرگز خود توکن: توکن پوش داخل جریان رویدادها یعنی کپی‌شدنش در انبار داده و هر خروجی و هر بکاپ، برای مقداری که رجیستری دستگاه‌ها همین حالا مالکش است. توکن را با POST /v1/devices ثبت کنید.

کلاینت نمی‌تواند IP، user agent، نام مرورگر، پرچم ربات یا حساب را تعیین کند. این‌ها از خود اتصال و از کلید می‌آیند، چون کلاینت نباید بتواند موقعیت جغرافیایی یا دستگاه خودش را جعل کند. چیزهایی که SDK درباره دستگاه می‌فرستد بر آنچه از هدر User-Agent استخراج می‌شود مقدم است؛ هدر فقط جاهای خالی را پر می‌کند. ترافیک ربات پرچم می‌خورد و ذخیره می‌شود، هرگز حذف نمی‌شود، چون حذف بی‌صدای آن یک افت ترافیک را غیرقابل‌توضیح می‌کند؛ هر گزارشی به‌طور پیش‌فرض آن را کنار می‌گذارد.

در بیلد مستقرشده مکان‌یابی جغرافیایی از روی IP وجود ندارد. country و region و city فقط از context.location پر می‌شوند.

#شکل پاسخ

هر پاسخ JSON روی این میزبان از این فیلدها ساخته می‌شود:

فیلدنوعکی هست
status"ok" یا "error"همیشه
acceptedعددوقتی صفر نباشد
duplicatesعددوقتی صفر نباشد
rejectedعددوقتی صفر نباشد
warningsآرایه‌ای از {code, field, note}وقتی هشداری باشد
errorsآرایه‌ای از {index, reason}وقتی آیتمی از بسته رد شده باشد
messageرشتهروی خطا

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

duplicates آن بخشی از accepted است که از قبل داشتیم. از accepted کم نمی‌شود، چون accepted به تنها سوالی جواب می‌دهد که یک SDK می‌پرسد، یعنی «می‌توانم دیگر نفرستم»، و کلاینتی که هرچه پذیرفته نشده را دوباره بفرستد، یک تکراری را تا ابد می‌فرستد. پس accepted: 500 همراه duplicates: 493 یعنی هفت رویداد ذخیره شد و ۴۹۳ تا رویدادی بودند که از قبل داشتیم. نبودنش هم مثل بقیه یعنی صفر.

هشدار یعنی رویداد با یک اصلاح پذیرفته شد. کدهایی که ممکن است ببینید:

کدمعنی
generated_message_idmessage_id نیامده بود، پس ما یکی ساختیم و تلاش دوباره برای این رویداد به‌عنوان تکراری شناخته نمی‌شود
timestamp_in_futureساعت دستگاه بیش از یک ساعت جلو بود؛ روی زمان دریافت چسبانده شد
timestamp_too_oldقدیمی‌تر از پنجره ورود داده حساب شما؛ روی لبه همان پنجره چسبانده شد
too_many_propertiesبیش از ۲۵۶ ویژگی؛ متن هشدار می‌گوید چند تا آمده و چند تا نگه داشته شده
unserialisable_propertyیک ویژگی قابل کدگذاری نبود؛ field نامش را می‌گوید
too_many_traitsبیش از ۲۵۶ خصیصه
invalid_phoneشماره موبایل ایرانی معتبر نیست؛ مقدار خام همان‌طور که آمده ذخیره شد
invalid_national_idکد ملی رقم کنترلش را رد کرد؛ خصیصه حذف شد

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

#POST /v1/track

ثبت می‌کند که یک آدم چه کرد. event لازم است؛ بدون آن جواب 400 missing_event_name است.

Shell
curl -X POST https://in.segmentic.net/v1/track \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "message_id": "m-1",
    "event": "order_completed",
    "user_id": "u_123",
    "properties": { "revenue": 2500000, "currency": "IRR", "order_id": "8821", "city": "تهران" }
  }'
JSON
{"status":"ok","accepted":1}

همان فراخوان بدون message_id این جواب را می‌دهد:

JSON
{
  "status": "ok",
  "accepted": 1,
  "warnings": [
    {
      "code": "generated_message_id",
      "field": "message_id",
      "note": "no message_id sent; retries of this event cannot be de-duplicated"
    }
  ]
}

درآمد از ویژگی‌ها استخراج می‌شود، به این ترتیب: اولین مقدار غیرصفر از revenue و total و value؛ اگر نبود، price ضربدر quantity که در نبودش یک فرض می‌شود. currency پیش‌فرض IRR است و با حروف بزرگ ذخیره می‌شود، پس "irt" به شکل IRT می‌ماند. هیچ تبدیل نرخی حدس زده نمی‌شود.

#POST /v1/identify

خصیصه‌ها را روی یک پرونده می‌گذارد. event نادیده گرفته می‌شود؛ نام رویداد ذخیره‌شده همیشه identify است.

Shell
curl -X POST https://in.segmentic.net/v1/identify \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "message_id": "id-8821",
    "user_id": "u_123",
    "traits": {
      "email": "  Ali@Digikala.COM ",
      "phone": "0912 345 6789",
      "first_name": "علی",
      "city": "کرج",
      "gender": "مرد",
      "lifetime_value": 48200000,
      "is_subscriber": true,
      "referral_code": "0912345"
    }
  }'
JSON
{"status":"ok","accepted":1}

آن payload به این تبدیل می‌شود:

خصیصهذخیره‌شدهچرا
emailali@digikala.comفضای اضافه بریده و حروف کوچک می‌شود. هیچ اعتبارسنجی قالبی در کار نیست
phone+989123456789 به‌علاوه phone_operator: "mci"هرچیزی جز E.164 برای یک آدم دو پرونده می‌سازد. اپراتور از چهار رقم اول درمی‌آید و 0912 یعنی mci (همراه اول)
cityکرجفارسی یکدست می‌شود، پس کیبورد عربی هم که كرج بفرستد همین‌جا می‌نشیند
gendermaleتا می‌شود و نگاشت می‌شود. m و male و man و مرد و اقا و پسر همه male می‌شوند؛ مجموعه زنانه female می‌شود؛ هرچیز دیگر other
lifetime_valueمتن 48200000 و عدد 48200000خصیصه عددی دو بار نوشته می‌شود تا هم فیلتر «برابر» و هم «بزرگ‌تر از» کار کند
is_subscriberمتن true و عدد 1
referral_codeفقط متن 0912345رشته‌ای که شبیه عدد است هرگز تبدیل نمی‌شود، پس صفر ابتدایی زنده می‌ماند

این دوبار نوشتن تزئینی نیست. بعد از این اضافه شد که یک حساب زنده با حدود ۱۱۵ هزار پرونده، برای هر خصیصه نقشه عددی خالی داشت، پس مخاطب «موجودی کلید ۱۰۰ یا بیشتر» هیچ‌کس را برنمی‌گرداند و «کمتر از ۱۰» همه را برمی‌گرداند، از جمله کاربری که موجودی‌اش ۴۲۸ بود. نه خطایی، نه هشداری، مخاطبی که مثل یک جواب خوانده می‌شود.

national_id با رقم کنترل ایرانی اعتبارسنجی می‌شود و اگر رد شود حذف می‌شود، با هشدار invalid_national_id. مقدار معتبر با ارقام فارسی و عربی‌اش تبدیل‌شده به اسکی و فاصله‌های دو سرش بریده ذخیره می‌شود، و در باقی چیزها همان‌طور که فرستادید. خود بررسی قبل از شمردن، خط تیره و فاصله را برمی‌دارد، هشت تا ده رقم را می‌پذیرد و مقدار کوتاه را فقط برای حساب خودش با صفر به ده رقم می‌رساند، پس 12345679 هشت‌کاراکتری ذخیره می‌شود، 001-234-5679 خط تیره‌هایش را نگه می‌دارد و 001 234 5679 فاصله‌های داخلی‌اش را نگه می‌دارد. اگر سگمنت‌ها و جوین‌های شما ده رقم انتظار دارند، همان شکل ده‌رقمی را بفرستید.

#POST /v1/page و POST /v1/screen

بدنه یکی است. event اینجا اختیاری است: بدون آن نام رویداد ذخیره‌شده روی /v1/page می‌شود page_viewed و روی /v1/screen می‌شود screen_viewed.

Shell
curl -X POST https://in.segmentic.net/v1/page \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "message_id": "p-4471",
    "anonymous_id": "a_9f21c0",
    "event": "product",
    "properties": { "sku": "DKP-118820" },
    "context": {
      "page": {
        "url": "https://shop.example.ir/p/118820?utm_source=sms",
        "path": "/p/118820",
        "title": "گوشی موبایل",
        "referrer": "https://www.google.com/"
      },
      "session_id": "s_20260807_01",
      "locale": "fa-IR"
    }
  }'
JSON
{"status":"ok","accepted":1}

#POST /v1/alias

تاریخچه ناشناس را به آدم واردشده می‌چسباند. previous_id لازم است؛ بدون آن جواب 400 missing_previous_id است. نام رویداد ذخیره‌شده همیشه alias است.

Shell
curl -X POST https://in.segmentic.net/v1/alias \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{"message_id":"al-1","user_id":"u_123","previous_id":"a_9f21c0"}'
JSON
{"status":"ok","accepted":1}

SDKها این را خودکار روی اولین identify بعد از گشت‌وگذار ناشناس می‌فرستند، پیش از خود identify. اگر کلاینت خودتان را می‌نویسید، همین را کپی کنید: بدون alias کل تاریخچه پیش از ورود کاربر یتیم می‌شود و هر قیفی که از مرز ورود عبور کند عدد اشتباه گزارش می‌دهد.

#POST /v1/batch

تا ۵۰۰ رویداد در یک درخواست. هر آیتم type خودش را دارد و اینجا این فیلد نادیده گرفته نمی‌شود بلکه تعیین‌کننده است: باید یکی از track، identify، alias، page، screen باشد.

Shell
curl -X POST https://in.segmentic.net/v1/batch \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "sent_at": "2026-08-07T09:12:41Z",
    "context": { "locale": "fa-IR", "app": { "version": "5.2.1" } },
    "batch": [
      {"type":"track","message_id":"b1","event":"product_viewed","user_id":"u1"},
      {"type":"track","message_id":"b2","event":"checkout_started","user_id":"u2"},
      {"type":"identify","message_id":"b3","user_id":"u3","traits":{"phone":"09123456789"}}
    ]
  }'
JSON
{"status":"ok","accepted":3}

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

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

JSON
{
  "status": "ok",
  "accepted": 2,
  "rejected": 1,
  "errors": [ { "index": 1, "reason": "missing_identity" } ]
}

دو شکست کل درخواست را رد می‌کنند، هر دو با 400: آرایه batch خالی (batch_empty) و بیش از ۵۰۰ آیتم (batch_too_large: 501 items, limit 500). رد شدن به دلیل سهمیه هم کل بسته را می‌گیرد و هرگز بخشی از آن را نمی‌پذیرد، چون پذیرش نصفه SDK را از تشخیص اینکه کدام آیتم‌ها را باید دوباره بفرستد ناتوان می‌کند.

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

دیباگر زنده رویداد در پنل، رویدادهایی را که از /v1/batch می‌آیند نمی‌بیند. ضبط فقط روی مسیر تک‌رویدادی و مسیر وبهوک انجام می‌شود. هر SDK موبایلی بسته می‌فرستد و SDK وب هم همین‌طور، پس اگر به دیباگر نگاه می‌کنید و چیزی نمی‌بینید در حالی که accepted بالا می‌رود، دلیلش همین است.

#message_id و حذف تکراری

message_id همان چیزی است که تلاش دوباره را بی‌خطر می‌کند. SDKها روی شبکه موبایل بی‌ثبات با شدت دوباره می‌فرستند، پس بدون آن شمارش خرید بی‌صدا دو برابر می‌شود.

  • دامنه‌اش حساب شماست. دو حساب می‌توانند یک message_id مشترک داشته باشند بدون اینکه به هم بخورند.
  • پنجره ۴۸ ساعت است. باید با فاصله از طولانی‌ترین تلاش دوباره SDK بیشتر باشد: کلاینت اندرویدی که یک روز رویدادها را آفلاین بافر کرده و بعد فرستاده، باید هنوز شناخته شود.
  • تکراری 200 جواب می‌گیرد با accepted: 1 و duplicates: 1، دقیقا مثل تحویل اول، چون SDKای که خطا بگیرد تا ابد دوباره می‌فرستد. داخل بسته هم تکراری‌ها به همان دلیل در accepted شمرده می‌شوند و کنارش در duplicates گزارش می‌شوند.
  • شناسه پیام قبل از انتشار رویداد رزرو می‌شود و اگر انتشار شکست بخورد همان رزرو پس داده می‌شود، پس رویدادی که 503 گرفته، وقتی SDK دوباره می‌فرستدش، تکراری حساب نمی‌شود.
  • تکراری صورتحساب نمی‌شود. SDKای که دوباره می‌فرستد برای ما یک جست‌وجوی کش هزینه دارد، نه یک خط فاکتور که شما سرش بحث کنید.
  • اگر انبار حذف تکراری در دسترس نباشد، رویداد به هر حال پذیرفته و منتشر می‌شود. پذیرفتن یک تکراری احتمالی قطعا بهتر از گم‌کردن رویداد است: تکراری پایین‌دست قابل تعمیر است و داده نبوده نیست.

مکانیزم یک «بنویس اگر نبود» با زمان انقضاست، نه فیلتر بلوم، پس مثبت کاذب ندارد.

#زمان و اختلاف ساعت

timestamp یعنی آن اتفاق کی افتاد، روی دستگاه. sent_at یعنی دستگاه کی درخواست را فرستاد. دومی همان چیزی است که اجازه می‌دهد اولی را اصلاح کنیم.

  1. نبودن timestamp یعنی زمانی که ما دریافت کردیم. بدون هشدار.
  2. timestamp بیش از یک ساعت جلوتر از ساعت ما، روی زمان دریافت چسبانده می‌شود با هشدار timestamp_in_future. زمان جلوتر از الان فقط از ساعت غلط دستگاه می‌آید، و رد نکردنش رویداد را در بازه‌هایی می‌گذارد که گزارش‌ها آن‌ها را نهایی کرده‌اند.
  3. timestamp قدیمی‌تر از پنجره ورود داده حساب شما، روی لبه همان پنجره چسبانده می‌شود با هشدار timestamp_too_old. پنجره پیش‌فرض ۳۰ روز است؛ حسابی که رویدادها را بیشتر نگه می‌دارد پنجره بلندتری می‌گیرد. این عدد به‌ازای حساب است نه یک ثابت سراسری.
  4. در غیر این صورت، اگر sent_at آمده باشد و با ساعت ما بیش از یک دقیقه فرق داشته باشد، کل اختلاف به timestamp اضافه می‌شود. مقدار اصلاح‌شده فقط وقتی استفاده می‌شود که هنوز داخل پنجره بیفتد. برای اصلاح هیچ هشداری داده نمی‌شود.

یک مثال کامل: ساعت دستگاه دو ساعت عقب است. می‌گوید رویداد ساعت ۰۸:۰۰ رخ داده و ساعت ۱۰:۰۰ فرستاده شده. ما ساعت ۱۲:۰۰ دریافت می‌کنیم. اختلاف دو ساعت است، پس زمان ذخیره‌شده ۱۰:۰۰ است نه ۰۸:۰۰.

ورود داده زنده همیشه می‌چسباند و هرگز زمان خارج از پنجره را رد نمی‌کند. یعنی مهاجرت تاریخچه از این میزبان، بی‌صدا هرچه قدیمی‌تر از پنجره است را روی یک لحظه روی هم می‌ریزد، 200 جواب می‌دهد و درست به نظر می‌رسد تا ماه‌ها بعد که یک قیف بی‌معنا شود. این سر یک حساب واقعی که دو سال تاریخچه منتقل می‌کرد اتفاق افتاده است. تاریخچه را از /v1/track یا /v1/batch نریزید.

#کدهای وضعیت روی نقاط رویداد

وضعیتکدبدنه
پذیرفته شد200{"status":"ok","accepted":1} و در صورت وجود warnings
پذیرفته شد و تکراری بود200یکسان
کلید نوشتن نیست401{"status":"error","message":"missing write key"}
کلید بد، باطل یا معلق401{"status":"error","message":"invalid write key"}
جست‌وجوی کلید شکست خورد، قطعی ماست503{"status":"error","message":"cannot verify the write key right now; retry"}
بدنه بیشتر از ۵ مگابایت413{"status":"error","message":"request body too large"}
بدنه JSON نیست400{"status":"error","message":"malformed JSON"}
حساب از سهمیه گذشته402{"status":"error","message":"<یک جمله فارسی>"}
اعتبارسنجی رویداد تکی شکست خورد400{"status":"error","message":"<متن کامل دلیل>"}
بسته خالی یا بیش از ۵۰۰400{"status":"error","message":"batch_empty"} یا "batch_too_large: 501 items, limit 500"
بعضی آیتم‌های بسته بد بودند200{"status":"ok","accepted":N,"rejected":M,"errors":[...]}
هم گذرگاه و هم بافر دیسک شکست خوردند503{"status":"error","message":"temporarily unavailable, please retry"}

دلایل رد کدهای پایداری‌اند، چون پنل آن‌ها را به فارسی نگاشت می‌کند و مشتری‌ها رویشان هشدار می‌گذارند: unknown_type، missing_identity، missing_event_name، event_name_too_long، event_name_invalid_chars، id_too_long، missing_previous_id، batch_too_large، batch_empty. دو تای آن‌ها مقدار خود شما را داخل پیام می‌آورند، مثل unknown_type: "trak"، پس روی پیشوند تطبیق بدهید نه روی تساوی.

ردیف آخر تنها موردی است که SDK باید به دلیل داده دوباره تلاش کند. قطعی گذرگاه به‌تنهایی آن را نمی‌سازد: کالکتور روی یک لاگ پیش‌نوشت محلی می‌افتد، پس پایین‌بودن صف اصلا برای شما دیده نمی‌شود. باید هر دو شکست بخورند. توجه کنید که این 503 هدر Retry-After ندارد؛ فقط 503 مربوط به جست‌وجوی کلید دارد.

پاسخ 402 صرف‌نظر از Accept-Language شما فارسی است. کالکتور مذاکره زبان ندارد: هیچ‌چیز درخواست را با یک زبان برچسب نمی‌زند، پس آن جمله هر بار به فارسی برمی‌گردد. روی کد وضعیت شرط بگذارید، نه روی متن.

#POST /v1/devices

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

Shell
curl -X POST https://in.segmentic.net/v1/devices \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "device_id": "d_5f2a91c4",
    "user_id": "u_123",
    "platform": "android",
    "tokens": { "fcm": "cZ1x...:APA91b..." },
    "push_enabled": true,
    "has_gms": true,
    "app_version": "5.2.1",
    "manufacturer": "Samsung",
    "model": "SM-A546E",
    "os_name": "android",
    "os_version": "14",
    "locale": "fa-IR",
    "timezone": "Asia/Tehran",
    "sdk_name": "segmentic-android",
    "sdk_version": "1.4.0"
  }'
JSON
{"status":"ok"}
فیلدنوعلازمتوضیح
device_idرشتهبلهحداکثر ۲۵۶ بایت
user_idرشتهیکی از آن دو
anonymous_idرشتهیکی از آن دو
platformرشتهبلهandroid، ios، web، windows، macos، linux و نام‌های جایگزینی مثل iphone، ipad، osx، darwin، win، browser. مقدار server رد می‌شود
tokensشیءیک توکن قابل استفادهنام حامل به توکن
push_provider و push_tokenرشتهنهشکل قدیمی تک‌مسیره. اگر هر دو بیایند tokens برنده است
push_enabledبولیننهنبودنش یعنی روشن، تا SDK قدیمی کاربران خودش را ساکت نکند
has_gmsبولیننهنبودنش یعنی «نگفت»، که با false یکی نیست
app_version، manufacturer، model، os_name، os_version، locale، timezone، sdk_name، sdk_versionرشتهنههرکدام حداکثر ۲۵۶ بایت

کدام حامل به کدام پلتفرم می‌رسد:

پلتفرمحامل‌ها
androidfcm، bazaar، myket، mqtt
iosapns، mqtt
web، windows، macos، linuxwebpush

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

آن جدول می‌گوید ثبت چه چیزی را می‌پذیرد، نه اینکه به چه چیزی می‌شود تحویل داد، و دو سطرش امروز اصلا چیزی تحویل نمی‌دهند. هیچ ارائه‌دهنده mqttای پیاده‌سازی نشده: نام حامل یک ثابت است و در ترتیب اولویت روتر هم نشسته، و پشتش هیچ کدی چیزی نمی‌فرستد، پس دستگاه اندروید یا iOS که فقط توکن mqtt دارد بی‌مشکل ثبت می‌شود و هرگز در دسترس نیست. windows و macos و linux هم در روتر پوش مسیری ندارند: جدول اولویت به‌ازای پلتفرم روتر فقط android و ios و web را دارد، پس ثبت دسکتاپ ذخیره و شمرده می‌شود و هرگز چیزی برایش نمی‌رود. fcm یا apns یا webpush روی web ثبت کنید، و ثبت دسکتاپ یا mqtt را دفترداری بخوانید نه دسترس‌پذیری.

توکن‌های APNs در راه ورود تعمیر می‌شوند. APIهای قدیمی iOS توکن را به شکل <a1b2 c3d4> رشته می‌کنند، و فرستادن همان عینا برای همیشه از طرف اپل رد می‌شود، پس کروشه‌ها و فاصله‌ها حذف و مقدار با حروف کوچک ذخیره می‌شود.

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

JSON
{
  "status": "error",
  "message": "device: registration carries no usable token",
  "warnings": [
    { "code": "transport_not_supported", "message": "...", "field": "apns" }
  ]
}

بدون آن‌ها، توسعه‌دهنده SDKای که از بیلد اندروید توکن APNs می‌فرستد فقط «توکن قابل استفاده‌ای نیست» را می‌بیند و هیچ سرنخی ندارد. کدهای هشدار اینجا empty_token و token_too_long (بیش از ۴۰۹۶) و transport_not_supported و fcm_without_gms هستند. دلایل رد، عینا، این‌ها هستند: device: device_id is required و device: platform must be one of android, ios, web, windows, macos, linux و device: user_id or anonymous_id is required و device: registration carries no usable token. ثبتی که push_enabled: false دارد و توکن ندارد پذیرفته می‌شود، چون آن یک تغییر وضعیت واقعی است.

شکست ذخیره‌سازی 503 است با {"status":"error","message":"temporarily unavailable, please retry"}. برخلاف رویداد، ثبت ناموفق هیچ بافری پشتش ندارد، پس SDK باید دوباره تلاش کند.

#POST /v1/devices/unregister

Shell
curl -X POST https://in.segmentic.net/v1/devices/unregister \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{"device_id":"d_5f2a91c4","user_id":"u_123","revoked":false}'
JSON
{"status":"ok"}

device_id لازم است. بدون آن جواب 400 {"status":"error","message":"device_id is required"} است، و بدنه‌ای که JSON معتبر نباشد هم دقیقا همین جواب را می‌گیرد نه malformed JSON را.

revoked: true یعنی اپ حذف نصب شده و آن نصب رفته است. revoked: false، که پیش‌فرض است، یعنی خروج از حساب: کاربر جدا می‌شود و توکن می‌ماند. این را روی خروج از حساب صدا بزنید. روی یک گوشی مشترک، ماندن حساب قبلی یعنی نفر بعدی به‌روزرسانی سفارش یک نفر دیگر را می‌گیرد.

#وب‌پوش

فقط وقتی سرو می‌شود که وب‌پوش پیکربندی شده باشد. دو مسیر.

POST /v1/webpush/subscribe همان چیزی را می‌گیرد که مرورگر به شما داده، چه تودرتو چه تخت:

Shell
curl -X POST https://in.segmentic.net/v1/webpush/subscribe \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "u_123",
    "subscription": {
      "endpoint": "https://fcm.googleapis.com/fcm/send/dK9...",
      "p256dh": "BJ7s...",
      "auth": "k1Qb..."
    }
  }'
JSON
{"status":"ok"}

شکل تخت هم کار می‌کند:

JSON
{"user_id":"u_123","endpoint":"https://...","p256dh":"BJ7s...","auth":"k1Qb..."}

شکل تودرتو برای این هست که صفحه بتواند هرچه مرورگر داده را بدون باز کردنش بفرستد، و مهم‌تر، بدون کدگذاری دوباره کلیدها. رشته base64 که یک کمک‌کننده خوش‌نیت آن را دیکود و دوباره کد کرده باشد، کلاسیک‌ترین راهی است که یک اشتراک بی‌صدا از رمزگشایی می‌افتد.

هر چهار فیلد user_id و endpoint و p256dh و auth لازم‌اند. نبودن هرکدام 400 {"status":"error","message":"user_id and a complete subscription are required"} است، چون endpoint بدون کلید بی‌فایده است: payload را نمی‌شود رمز کرد.

POST /v1/webpush/unsubscribe فقط endpoint می‌خواهد و هیچ شناسه کاربری بررسی نمی‌شود:

Shell
curl -X POST https://in.segmentic.net/v1/webpush/unsubscribe \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{"endpoint":"https://fcm.googleapis.com/fcm/send/dK9..."}'
JSON
{"status":"ok"}

خود endpoint راز آن اشتراک است. داشتنش همین حالا برای فرستادن به آن مرورگر کافی است، پس خواستن چیز بیشتری برای اینکه کسی بتواند دریافت را قطع کند، محافظت از جهت اشتباه است. endpoint خالی 400 {"status":"error","message":"endpoint is required"} است.

شکست ذخیره‌سازی روی هر دو مسیر 503 است نه یک 200 بلعیده‌شده، چون کاربر همین حالا اجازه‌ای داده که صفحه نمی‌تواند دو بار بخواهدش.

اشتراک وب‌پوش به‌تنهایی هیچ‌کس را در دسترس نمی‌کند. پیش از رسیدن به فرستنده وب‌پوش، هر ارسال روی کانال webpush ردیف‌های دستگاه همان کاربر را می‌خواند و اگر ردیفی نباشد پیام را با دلیل not_reachable سرکوب می‌کند. این بررسی چه رجیستری دستگاه پیکربندی شده باشد چه نشده باشد اجرا می‌شود، پس روی نصبی که انبار دستگاه ندارد هر وب‌پوشی سرکوب می‌شود، و کمپین همان سرکوب را گزارش می‌دهد نه خطا را. اگر فقط پوش مرورگر را یکپارچه می‌کنید، همان کاربر را علاوه بر اشتراک با POST /v1/devices هم ثبت کنید (platform: "web")، و قبل از اینکه کمپینی روی آن بسازید با یک ارسال آزمایشی مطمئن شوید.

#پیام‌رسان‌ها

فقط وقتی سرو می‌شود که پیام‌رسان‌ها پیکربندی شده باشند. چت بله، ایتا یا روبیکا را به یک پرونده وصل می‌کند.

Shell
curl -X POST https://in.segmentic.net/v1/messenger/link \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "u_123",
    "platform": "bale",
    "chat_id": "44120099",
    "username": "ali_gh",
    "source": "bot_start"
  }'
JSON
{"status":"ok"}

platform باید دقیقا bale یا eitaa یا rubika باشد. هرچیز دیگر، از جمله telegram، جواب 400 {"status":"error","message":"user_id, chat_id and a known platform are required"} می‌گیرد. ستون پشت آن در دیتابیس یک قید بررسی دارد، پس مقدار ناشناخته وگرنه پایین‌تر با خطایی شکست می‌خورد که هیچ‌کس نمی‌تواند رویش کاری بکند.

user_id و chat_id و یک platform معتبر لازم‌اند. username و source اختیاری‌اند و همان‌طور که آمده‌اند رد می‌شوند. مقداری که رضایت واقعی را حمل می‌کند bot_start است، یعنی خود آدم بات را شروع کرده؛ هرچیز دیگری ارزش این را دارد که بعدا بشود پیدایش کرد.

POST /v1/messenger/unlink فقط user_id و platform می‌خواهد:

Shell
curl -X POST https://in.segmentic.net/v1/messenger/unlink \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{"user_id":"u_123","platform":"bale"}'
JSON
{"status":"ok"}

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

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

هر درخواست یک اعتبارنامه دوم دارد، user_hash، که بک‌اند خودتان هنگام ورود کاربر حساب می‌کند:

user_hash = lowercase_hex( HMAC-SHA256( identity_secret, user_id ) )
Shell
printf '%s' "u_123" \
  | openssl dgst -sha256 -hmac "$SEGMENTIC_IDENTITY_SECRET" -r \
  | cut -d' ' -f1

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

Shell
curl -X POST https://in.segmentic.net/v1/inbox \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "u_123",
    "user_hash": "9f1c0b7d3e5a...",
    "limit": 20
  }'
JSON
{
  "status": "ok",
  "messages": [
    {
      "message_id": "c104.u_123",
      "title": "سفارش شما ارسال شد",
      "body": "بسته شما تحویل پست شد.",
      "image": "https://cdn.example.ir/parcel.png",
      "deeplink": "myapp://orders/8821",
      "surface": "inbox",
      "token": "1.7.k2.ce.mfz1t8.9c4a...",
      "created_at": "2026-08-07T09:00:00Z",
      "expires_at": "2026-08-21T09:00:00Z",
      "seen": false
    }
  ]
}

messages همیشه آرایه است و هرگز null نیست، تا SDKای که بدون بررسی nil رویش حلقه می‌زند یک حلقه خالی بگیرد نه یک کرش.

token امضای انتساب همان پیام است. آن را در context.campaign.token روی رویداد message_opened که به /v1/track می‌فرستید برگردانید، تا بشود ثابت کرد آن باز شدن مال پیامی است که واقعا ما فرستاده‌ایم.

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

limit بدون تغییر به انبار داده می‌رود. صفر یعنی سقف خود انبار اعمال می‌شود، و آن سقف اینجا منتشر نشده است.

هر شکست هویتی همان 403 است:

JSON
{"status":"error","message":"user identity is not verified"}

هش غلط، هش نیامده و حسابی که اصلا راز هویت ندارد از هم قابل تشخیص نیستند، چون تشخیص‌دادنشان این نقطه را به ابزاری برای فهمیدن اینکه چه شناسه‌های کاربری وجود دارند تبدیل می‌کند. حالت «تاییدنشده» وجود ندارد.

POST /v1/inbox/ack همان اعتبارنامه را می‌گیرد به‌علاوه دو آرایه:

Shell
curl -X POST https://in.segmentic.net/v1/inbox/ack \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "u_123",
    "user_hash": "9f1c0b7d3e5a...",
    "seen": ["c104.u_123"],
    "dismissed": ["c99.u_123"]
  }'
JSON
{"status":"ok"}

این ack هیچ تعاملی ثبت نمی‌کند. باز شدن درون‌برنامه‌ای از مسیر عادی رویداد گزارش می‌شود، با همان توکنی که صندوق به شما داده، تا دقیقا با همان بررسی امضایی تایید شود که هر کانال دیگری با آن تایید می‌شود. مسیر دوم و مورد اعتماد برای همان سیگنال، یک چیز دیگر برای خراب شدن بود، و این یکی به حرف فراخوانی اعتماد می‌کرد که چیزی جز یک کلید نوشتن عمومی ندارد.

#پیام‌های روی سایت

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

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

Shell
curl "https://in.segmentic.net/v1/onsite?write_key=wk_seg_..."
HTTP
HTTP/1.1 200 OK
Cache-Control: public, max-age=60
Content-Type: application/json; charset=utf-8
JSON
{
  "campaigns": [
    {
      "id": 12,
      "name": "بنر تخفیف نوروز",
      "kind": "banner",
      "status": "live",
      "content": { },
      "targeting": { },
      "max_impressions": 3,
      "cooldown_hours": 24,
      "dismissible": true,
      "starts_at": "2026-03-15T00:00:00Z",
      "ends_at": "2026-03-25T00:00:00Z",
      "impressions": 41822,
      "clicks": 1104,
      "dismissals": 380
    }
  ],
  "cache_seconds": 60
}

kind یکی از banner و modal و slidein و survey است. content و targeting در نمونه بالا جمع شده‌اند؛ شکلشان در پیام‌های روی سایت هست. شصت ثانیه آن‌قدر بلند هست که بیشتر بازدیدهای یک فروشگاه شلوغ اصلا به ما نرسد، و آن‌قدر کوتاه هست که مکث‌کردن یک کمپین وقتی هنوز همان آدمی که دکمه را زده نگاه می‌کند اثر بگذارد. به‌خاطر همین پنجره، مرورگر تاریخ شروع و پایان را دوباره محلی چک می‌کند، تا کمپینی که پایانش وسط کش رد شود بدون منتظرماندن متوقف شود.

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

شکست ذخیره‌سازی اینجا 200 با فهرست خالی جواب می‌دهد، هرگز 5xx. این داخل بارگذاری صفحه شما می‌دود: خرابی ما باید به «امروز بنری نیست» تنزل کند، نه به یک خطای کنسول روی سایت شما.

POST /v1/onsite/event ثبت می‌کند چه اتفاقی افتاد:

Shell
curl -X POST https://in.segmentic.net/v1/onsite/event \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "campaign_id": 12,
    "anonymous_id": "a_9f21c0",
    "action": "click",
    "page_url": "https://shop.example.ir/p/118820"
  }'
JSON
{"status":"ok"}

campaign_id و یکی از user_id یا anonymous_id لازم‌اند؛ بدونشان جواب 400 {"status":"error","message":"campaign_id and a visitor id are required"} است. action یکی از impression (رشته خالی هم همین معنی را دارد) و click و dismiss و convert است و به حروف کوچک و بزرگ حساس نیست؛ هرچیز دیگر 400 {"status":"error","message":"unknown action"} است.

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

POST /v1/onsite/response جواب یک نظرسنجی را ثبت می‌کند و score و answers را هم می‌گیرد:

Shell
curl -X POST https://in.segmentic.net/v1/onsite/response \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "campaign_id": 31,
    "user_id": "u_123",
    "score": 9,
    "answers": { "why": "ارسال سریع بود" },
    "page_url": "https://shop.example.ir/thanks"
  }'
JSON
{"status":"ok"}

کمپین به‌جای اینکه به آن اعتماد شود، خوانده و بررسی می‌شود: اینکه این یک نظرسنجی NPS هست یا نه تعیین می‌کند که امتیاز اصلا معنایی دارد یا نه، و مرورگر مرجع این موضوع نیست. شناسه‌ای که پیدا نشود 400 {"status":"error","message":"unknown campaign"} است. کمپینی که نظرسنجی نیست 400 {"status":"error","message":"onsite: this campaign is not a survey"} است. وقتی کمپین NPS تنظیم شده باشد امتیاز باید آمده باشد و بین ۰ تا ۱۰ باشد وگرنه onsite: an NPS score must be between 0 and 10؛ وقتی NPS نباشد امتیاز به -1 مجبور می‌شود، یعنی «بدون امتیاز». نیامدن این فیلد رد می‌شود، نه اینکه صفر خوانده شود. قبلا صفر خوانده می‌شد و صفر یک نمره منتقد معتبر است، پس بدنه‌ای که اصلا امتیاز نداشت به‌عنوان بدترین جواب مقیاس ذخیره می‌شد و امتیازی را که همان نفر قبلا داده بود جایگزین می‌کرد. پاسخ متنی بلندتر از ۲۰۰۰ کاراکتر به‌جای رد شدن بریده می‌شود، چون کسی که سه پاراگراف درباره تحویل سفارشش نوشته حرفی زده که ارزش نگه‌داشتن دارد. ثبت یک پاسخ یک convert هم ثبت می‌کند، تا کسی که نظرش را گفته هفته بعد همان سوال را نگیرد.

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

#GET /v1/status

بدون احراز هویت، بدون خواندن دیتابیس، بدون محدودیت نرخ.

Shell
curl -i https://in.segmentic.net/v1/status
HTTP
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: no-store, no-cache, must-revalidate
X-Server-Time: 2026-08-07T09:12:41Z

{"status":"ok","service":"collector","version":"1.42.0"}

version مهر بیلد کالکتوری است که دارد می‌دود، تا «آیا استقرار رفته بالا» از بیرون قابل جواب‌دادن باشد. /readyz و نقطه متریک‌ها روی یک listener مدیریتی جداگانه‌اند و جزو این سطح نیستند.

#مسیرهایی که فقط با پیکربندی وجود دارند

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

مسیرهاکی سرو می‌شود
/v1/track، /v1/identify، /v1/page، /v1/screen، /v1/alias، /v1/batch، /v1/statusهمیشه
/v1/devices، /v1/devices/unregisterوقتی انبار دستگاه پیکربندی شده باشد
/v1/webpush/subscribe، /v1/webpush/unsubscribeوقتی وب‌پوش پیکربندی شده باشد
/v1/messenger/link، /v1/messenger/unlinkوقتی پیام‌رسان‌ها پیکربندی شده باشند
/v1/inbox، /v1/inbox/ackوقتی صندوق پیکربندی شده باشد
/v1/onsite، /v1/onsite/event، /v1/onsite/responseوقتی پیام‌های روی سایت پیکربندی شده باشند

مسیر ثبت‌نشده زیر /v1/ جواب 405 Method Not Allowed می‌دهد نه 404. الگوی preflight مربوط به CORS هر مسیری زیر آن پیشوند را برای OPTIONS برداشته، پس روتر مسیر را می‌شناسد و متد را نه. 405 اینجا را «این قابلیت روی این استقرار روشن نیست» بخوانید و با همان چشمی نگاهش کنید که به 404 نگاه می‌کنید. بیرون از /v1/، مسیر ثبت‌نشده یک 404 ساده است.

#بقیه مسیرهای این میزبان

این‌ها روی همین میزبان‌اند و جزو سطح SDK نیستند. هرکدام جای خودشان توضیح داده شده‌اند.

مسیرچیست
GET /e/oپیکسل باز شدن ایمیل. همیشه یک GIF شفاف جواب می‌دهد، حتی برای توکن جعلی، چون تصویر شکسته وسط یک ایمیل تبلیغاتی آشکارترین نقصی است که گیرنده می‌بیند
GET /e/u و POST /e/uلغو اشتراک یک‌کلیکی. آن GET عمدا لغو اشتراک نمی‌کند؛ رضایت و لغو اشتراک را ببینید
GET /e/p و POST /e/pمرکز ترجیحات گیرنده
POST /v1/hooks/{source}/{token}وبهوک پلتفرم‌ها از دیجی‌کالا، باسلام، ترب، زرین‌پال، ووکامرس، شاپیفای و سگمنت؛ وبهوک‌ها را ببینید
POST /v1/bounce/{local}ورودی برگشت ایمیل، که با نشانی بازگشت خودش آدرس‌دهی می‌شود
GET /sdk/*باندل SDK مرورگر، که از همین origin سرو می‌شود تا یک ورودی در سیاست امنیت محتوای شما هم اسکریپت و هم درخواست‌هایش را پوشش بدهد
GET /s/*اینجا یک 404 قطعی. لینک‌های کوتاه روی دامنه کوتاه خودشان هستند، چون دامنه کوتاه‌تر یعنی کاراکتر کمتر در هر پیامک

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

#چیزهایی که این میزبان ندارد

  • هیچ نوع محدودیت نرخی نیست. نه در ثانیه، نه به‌ازای کلید، نه به‌ازای IP، نه روی لبه و نه داخل برنامه. تنها کنترل حجم سهمیه ماهانه است که 402 جواب می‌دهد.
  • 503 مربوط به شکست انتشار هدر Retry-After ندارد. فقط 503 جست‌وجوی کلید دارد. از عقب‌نشینی خودتان استفاده کنید.
  • Content-Type اعمال نمی‌شود. بدنه JSON با هر برچسبی پذیرفته می‌شود.
  • مذاکره زبان نیست. Accept-Language نادیده گرفته می‌شود؛ تنها پیام آدم‌خوان روی این میزبان، یعنی رد شدن به دلیل سهمیه، همیشه فارسی است.
  • مکان‌یابی جغرافیایی از روی IP نیست. مکان فقط از context.location می‌آید.
  • نسخه GET و PUT و DELETE نقاط رویداد وجود ندارد. GET /v1/track جواب 405 می‌دهد.
  • روی مسیر بسته ضبط دیباگ نیست، پس دیباگر زنده رویداد در پنل نسبت به ترافیک بسته‌ای کور است.
  • کوکی سمت سرور و شناسه ناشناس ساخته‌شده توسط سرور وجود ندارد. ماندگارکردن anonymous_id کاملا کار SDK است، و اگر کلاینت خودتان را می‌نویسید، کار شماست.
  • هیچ راهی برای خواندن دوباره یک رویداد نیست. هیچ‌چیز روی این میزبان آنچه فرستاده‌اید را برنمی‌گرداند. آن را در پنل یا از API مدیریتی کوئری کنید.
قبلیمرجع APIبعدیAPI مدیریتی

در این صفحه

  • میزبان
  • احراز هویت با کلید نوشتن
  • وقتی احراز هویت شکست می‌خورد
  • CORS
  • بدنه درخواست
  • فیلدهای بدنه رویداد
  • شیء context
  • شکل پاسخ
  • POST /v1/track
  • POST /v1/identify
  • POST /v1/page و POST /v1/screen
  • POST /v1/alias
  • POST /v1/batch
  • message_id و حذف تکراری
  • زمان و اختلاف ساعت
  • کدهای وضعیت روی نقاط رویداد
  • POST /v1/devices
  • POST /v1/devices/unregister
  • وب‌پوش
  • پیام‌رسان‌ها
  • صندوق درون‌برنامه‌ای
  • پیام‌های روی سایت
  • GET /v1/status
  • مسیرهایی که فقط با پیکربندی وجود دارند
  • بقیه مسیرهای این میزبان
  • چیزهایی که این میزبان ندارد

سگمنتیک

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