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

ثبت دستگاه برای اعلان موبایل

ثبت توکن دستگاه برای پوش موبایل، پوش مرورگر و پیام‌رسان‌ها.

#دو مسیر، و اینکه کدام را لازم دارید

REGISTRATION
Web subscriptionPush endpoint
FCM tokenAndroid
APNs tokeniOS
SEGMENTICDevice registryOne profile can own many reachable devices
DELIVERY ROUTES
Web pushBrowser
Android pushFCM
iOS pushAPNs
تبدیل اشتراک وب، توکن FCM و توکن APNs به مسیرهای قابل دسترس ارسال

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

دو مسیر برای رسیدن به این نقطه هست و هیچ‌کدام جای دیگری را نمی‌گیرد.

مسیربرای چه کسیچه چیزی را خودتان پر می‌کنید
SDK اندرویداپ اندرویدفقط توکن، و اگر بخواهید has_gms
POST /v1/devicesiOS، وب، دسکتاپ، اتصال سمت سرور، و هر اپی که وابستگی اضافه نمی‌کندتمام فیلدها

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

هر دو مسیر روی هاست ورودی کار می‌کنند: https://in.segmentic.net با کلید نوشتن wk_seg_.... کلید نوشتن عمومی است و قرار است داخل بسته اپ شما باشد. هاست مدیریت (https://api.segmentic.net، با کلید API sk_seg_...) هیچ مسیر دستگاهی ندارد: نه ساخت، نه ویرایش، نه حذف، و نه خواندن. تنها جایی که دستگاه‌های یک نفر دیده می‌شوند پنل است، و در خواندن دستگاه‌های یک پرونده توضیح داده شده.

روی نصب لوکال، همین آدرس‌ها روی http://localhost:8080 بالا می‌آیند.

ثبت دستگاه شمارش و سهمیه ندارد. overQuota و شمارنده مصرف فقط روی /v1/track و خانواده‌اش و /v1/batch صدا زده می‌شوند؛ POST /v1/devices هیچ‌کدام را صدا نمی‌زند. تعداد دفعاتی که دستگاه‌هایتان را دوباره ثبت می‌کنید روی صورتحساب اثری ندارد.

#مسیر SDK اندروید

توکن را خود اپ شما می‌دهد، SDK آن را نمی‌گیرد. این تصمیم عمدی است: اپی که پوش می‌فرستد از قبل فایربیس یا بازار یا مایکت را با پروژه و نسخه خودش وصل کرده. اگر SDK هم خودش توکن می‌گرفت، یعنی نسخه فایربیس را به جای شما انتخاب کرده بودیم و با نسخه خودتان تصادم می‌کردیم. نتیجه‌اش این است که کل SDK اندروید هیچ وابستگی ندارد: نه کتابخانه JSON، نه HTTP، نه AndroidX، نه فایربیس. تنها مجوزی که به اپ اضافه می‌شود INTERNET است.

یک بار راه‌اندازی، بعد ثبت از داخل همان جایی که توکن می‌رسد:

Kotlin
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        Segmentic.init(
            this,
            SegmenticOptions(
                writeKey = "wk_seg_...",
                apiHost = "https://in.segmentic.net",
            ),
        )
    }
}

class MyMessagingService : FirebaseMessagingService() {
    override fun onNewToken(token: String) {
        Segmentic.registerDevice(mapOf(PushTransport.FCM to token))
    }
}

چند مسیر روی یک دستگاه، که کدامشان تحویل می‌دهد سمت سرور تصمیم می‌شود:

Kotlin
Segmentic.registerDevice(
    mapOf(
        PushTransport.FCM to fcmToken,
        PushTransport.BAZAAR to bazaarToken,
    ),
)

PushTransport روی اندروید چهار ثابت دارد: FCM، BAZAAR، MYKET، MQTT. ثابت APNS روی اندروید وجود ندارد، چون آندروید هرگز نمی‌تواند به آن تحویل بدهد.

اگر اپ شما از قبل به play-services-base وابسته است، جواب قطعی را خودتان بدهید. SDK وقتی چیزی به آن ندهید بسته com.google.android.gms را پروب می‌کند: پیدا شد true، پیدا نشد false، هر خطای دیگر null یعنی «نمی‌دانم».

Kotlin
val gms = GoogleApiAvailability.getInstance()
    .isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
Segmentic.registerDevice(tokens, hasGms = gms)

SDK این‌ها را خودش پر می‌کند و شما لازم نیست: device_id، platform، has_gms، push_enabled، app_version، manufacturer، model، os_name، os_version، locale، timezone، sdk_name، sdk_version. هویت (user_id و anonymous_id) را هم هسته پر می‌کند نه فراخوان، تا اپ میزبان نتواند دستگاه را به نام کاربری ثبت کند که از حساب خارج شده.

device_id یک UUID تصادفی در حافظه خصوصی خود اپ است. عمدا نه شناسه تبلیغاتی است و نه Settings.Secure.ANDROID_ID؛ هر دوی آن‌ها آدم را بین اپ‌های بی‌ربط دنبال می‌کنند و این سوالی است که مشتری باید جوابش را بدهد نه ما. این شناسه تا وقتی زنده است که اپ حذف یا داده‌اش پاک نشود.

این دقیقا بایت‌هایی است که یک گوشی اندروید ۱۵ روی سیم گذاشت. فایل نمونه دست‌نویس نیست؛ با یک collector ضبط‌کننده گرفته شده و فقط از اجرای دوباره روی دستگاه واقعی بازتولید می‌شود:

بایت‌های واقعی یک نصب اندروید
{"device_id":"79a1c2c3-a61a-4816-a355-f3d5a0c7ffc2","platform":"android","anonymous_id":"711faad0-317b-40aa-81d7-253a39280348","tokens":{"fcm":"scripted-token-not-a-real-one"},"has_gms":true,"push_enabled":false,"app_version":"0.1.0","manufacturer":"Google","model":"sdk_gphone64_x86_64","os_name":"android","os_version":"15","locale":"en-US","timezone":"Asia/Tehran","sdk_name":"segmentic-android","sdk_version":"0.1.0"}

registerDevice سه نتیجه دارد و تفاوتشان مهم است:

  • REGISTERED: ذخیره شد.
  • REFUSED: یک 4xx گرفت. بدنه دور ریخته می‌شود، چون همان بدنه دفعه بعد هم به همان شکل رد می‌شود و نگه داشتنش یعنی هر بار بالا آمدن اپ، برای همیشه، همان درخواست ردشده. همین مقدار وقتی هم برمی‌گردد که کاربر انصراف داده باشد، و آن حالت اصلا درخواستی نمی‌فرستد.
  • PENDING: هر چیز دیگر. روی دیسک نوشته می‌شود و در هر flush و هر بالا آمدن بعدی اپ دوباره تلاش می‌شود.

این سه مقدار را خودتان تحویل نمی‌گیرید. Segmentic.registerDevice روی نخ شبکه خودش اجرا می‌شود و چیزی برنمی‌گرداند؛ نتیجه فقط در لاگ‌کت با تگ segmentic می‌آید. هسته است که مقدار را برمی‌گرداند و خودش هم آن را دوباره تلاش می‌کند.

فراخوانی دوباره registerDevice برای همان دستگاه تکراری حساب نمی‌شود؛ سرور روی device_id جایگزینی می‌کند. هر بار که ارائه‌دهنده توکن را عوض کرد دوباره صدایش بزنید.

جزئیات نصب و بقیه سطح SDK در SDK اندروید است.

#ثبت دستگاه با POST /v1/devices

یک درخواست، یک دستگاه. اندپوینت دسته‌ای وجود ندارد.

ثبت یک نصب اندروید با دو مسیر
curl -X POST https://in.segmentic.net/v1/devices \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "device_id": "dev-1",
    "user_id": "u_123",
    "platform": "android",
    "tokens": { "fcm": "fcm-tok", "bazaar": "bazaar-tok" },
    "model": "Xiaomi Redmi Note 12",
    "timezone": "Asia/Tehran"
  }'

پاسخ 200، عینا همین و نه چیز بیشتری، چون warnings وقتی خالی است اصلا در بدنه نمی‌آید:

JSON
{"status":"ok"}

آیفون همان اندپوینت است با پلتفرم و مسیر دیگر:

ثبت یک نصب iOS از کد خودتان
curl -X POST https://in.segmentic.net/v1/devices \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "device_id": "F7A1C2C3-A61A-4816-A355-F3D5A0C7FFC2",
    "user_id": "u_9137",
    "platform": "ios",
    "tokens": { "apns": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2" },
    "push_enabled": true,
    "app_version": "3.4.0",
    "model": "iPhone13,2",
    "os_name": "ios",
    "os_version": "17.4",
    "locale": "fa-IR",
    "timezone": "Asia/Tehran"
  }'
JSON
{"status":"ok"}

کلید نوشتن را سه جور می‌شود داد و هر سه معتبرند:

  • Authorization: Bearer wk_seg_...
  • X-Segmentic-Key: wk_seg_...
  • پارامتر کوئری ?write_key=wk_seg_... (برای فراخوان‌هایی مثل sendBeacon که هدر نمی‌توانند بگذارند)

نکته‌های ترابری: بدنه JSON است و بدون توجه به Content-Type پارس می‌شود؛ سقف بدنه ۵ مگابایت (5 << 20 بایت) است؛ پاسخ همیشه application/json; charset=utf-8 است. CORS باز است (Access-Control-Allow-Origin: *، متدهای POST, OPTIONS، هدرهای Content-Type, Authorization, X-Segmentic-Key، عمر پیش‌پرواز 86400) ولی credentials هرگز اجازه داده نمی‌شود.

tenant_id و app_id اگر در بدنه بفرستید نادیده گرفته می‌شوند. هر دو از کلید نوشتن می‌آیند. تستی هست که "tenant_id": 999 می‌فرستد و بررسی می‌کند دستگاه هنوز زیر مستاجر واقعی کلید ذخیره شده باشد.

#هر فیلد بدنه

فیلدنوعاجباریرفتار و پیش‌فرض
device_idرشتهبلهفاصله‌های دو سر حذف می‌شود. خالی یا بلندتر از 256 بایت کل درخواست را رد می‌کند
platformرشتهبلهبا بی‌تفاوتی به بزرگی و کوچکی حروف و با نام‌های مستعار پارس می‌شود. جدول پلتفرم‌ها
user_idرشتهیکی از این دو لازم استفاصله‌گیری، سپس برش در 256 بایت
anonymous_idرشتهیکی از این دو لازم استفاصله‌گیری، سپس برش در 256 بایت
tokensشیء، نگاشت نام مسیر به توکننه، ولی قانون «چیزی برای ذخیره نماند» را ببینیدکلیدها کوچک و فاصله‌گیری می‌شوند، مقدارها فاصله‌گیری. سقف هر توکن 4096 بایت
push_providerرشتهنهشکل قدیمی تک‌مسیره، جفت با push_token
push_tokenرشتهنهشکل قدیمی تک‌مسیره
has_gmsبولین یا nullنهسه‌حالته. نبودنش یعنی نامعلوم، که false نیست
push_enabledبولین یا nullنهسه‌حالته. نبودنش یعنی نامعلوم، که «مجاز» خوانده می‌شود
app_versionرشتهنهفاصله‌گیری، برش در 256 بایت
manufacturerرشتهنهفاصله‌گیری، برش در 256 بایت
modelرشتهنهفاصله‌گیری، برش در 256 بایت
os_nameرشتهنهفاصله‌گیری، برش در 256 بایت
os_versionرشتهنهفاصله‌گیری، برش در 256 بایت
localeرشتهنهفاصله‌گیری، برش در 256 بایت
timezoneرشتهنهفاصله‌گیری، برش در 256 بایت. همین است که «ساعت ۹ صبح بفرست» و ساعت سکوت را به ساعت خود گیرنده معنا می‌کند
sdk_nameرشتهنهفاصله‌گیری، برش در 256 بایت
sdk_versionرشتهنهفاصله‌گیری، برش در 256 بایت

برش روی مرز نویسه انجام می‌شود، پس یک فیلد فارسی هرگز به UTF-8 نامعتبر تبدیل نمی‌شود. تست این را با ۲۵۶ نسخه از «ش» می‌سنجد.

شکل قدیمی تک‌مسیره هنوز کار می‌کند و قرار نیست حذف شود، چون مشتری‌ها نسخه SDK را سال‌ها ثابت نگه می‌دارند و ارتقا نباید شرط دریافت‌کردن باشد:

شکل قدیمی، هنوز پذیرفته می‌شود
curl -X POST https://in.segmentic.net/v1/devices \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "device_id": "dev-old",
    "user_id": "u_123",
    "platform": "android",
    "push_provider": "bazaar",
    "push_token": "legacy"
  }'
JSON
{"status":"ok"}

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

#قانون «چیزی برای ذخیره نماند»

tokens خالی به‌تنهایی خطا نیست؛ بستگی به push_enabled دارد.

  • بدون توکن و push_enabled غایب یا true: خطای 400 با پیام device: registration carries no usable token. فراخوان هیچ کاری نکرده، پس ذخیره‌اش فقط جدول را بزرگ می‌کرد.
  • بدون توکن و push_enabled: false: پاسخ 200 و ردیف ذخیره می‌شود. این یک تغییر وضعیت واقعی است: کاربر اعلان را خاموش کرده و باید ثبت شود.

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

کاربر اعلان را خاموش کرده
curl -X POST https://in.segmentic.net/v1/devices \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "device_id": "dev-1",
    "user_id": "u_123",
    "platform": "android",
    "push_enabled": false
  }'
JSON
{"status":"ok"}

#پلتفرم‌ها و مسیرهایی که به هرکدام می‌رسند

platform اجباری است و پیش‌فرض ندارد. جا انداختنش خطای 400 می‌دهد.

پلتفرماملاهای پذیرفته‌شدهمسیرهای پذیرفته‌شده هنگام ثبتکمپین واقعا تحویل می‌دهد؟
androidandroidfcm، bazaar، myket، huawei، mqttبله، با fcm یا bazaar یا myket یا huawei
iosios، iphone، ipadapns، mqttبله، با apns
webweb، browser، webappwebpushبله، با webpush
windowswindows، win، win32، win64webpushنه. مسیر پیش‌فرضی برایش تعریف نشده
macosmacos، mac، mac os، osx، darwinwebpushنه. مسیر پیش‌فرضی برایش تعریف نشده
linuxlinuxwebpushنه. مسیر پیش‌فرضی برایش تعریف نشده
serverserver، backend، apiهیچ‌کدامثبت با 400 رد می‌شود

نام‌ها با بی‌تفاوتی به بزرگی و کوچکی حروف و بعد از حذف فاصله‌های دو سر پارس می‌شوند؛ " Android " می‌شود android. همین برای کلیدهای tokens هم صادق است: " FCM " می‌شود fcm.

سه پلتفرم دسکتاپ عمدا زیر web تا نشده‌اند، تا «چند تا نصب مک‌اواس داریم» جوابی داشته باشد. اصلا هم به این دلیل اضافه شدند که یک مشتری واقعی این‌ها را داشت و ایمپورتش شکست.

windows، macos و linux ثبت می‌شوند ولی امروز پوش نمی‌گیرند. جدول ترتیب مسیرها فقط برای android، ios و web مدخل دارد، و روتر برای پلتفرمی که مدخل ندارد هیچ مسیری تولید نمی‌کند. نتیجه هنگام ارسال push: no usable transport for this device است.

server رد می‌شود نه ذخیره. یک بک‌اند اصلا دستگاهی ندارد، و ردیفی که هیچ پوشی به آن نمی‌رسد در تمام عددهای «قابل دسترسی» که به مشتری نشان می‌دهیم شمرده می‌شد. یعنی platform: "api" یا platform: "backend" خطای 400 می‌گیرد.

پلتفرم ناشناس و مسیر ناشناس دو رفتار کاملا متفاوت دارند:

  • پلتفرم ناشناس (مثلا blackberry یا symbian) کل درخواست را رد می‌کند. هیچ‌چیز ذخیره نمی‌شود.
  • مسیر ناشناس (مثلا pigeon) فقط همان یک توکن را با یک هشدار transport_not_supported دور می‌ریزد و بقیه ثبت ادامه پیدا می‌کند.

مسیر mqtt پذیرفته و ذخیره می‌شود و هرگز تحویل نمی‌دهد. ثابتش تعریف شده و در ترتیب پیش‌فرض هم هست، ولی هیچ پیاده‌سازی ارائه‌دهنده MQTT در کل مخزن وجود ندارد، و روتر مسیری را که ارائه‌دهنده ندارد کنار می‌گذارد. روی mqtt حساب نکنید.

#شکل توکن هر مسیر

مسیرچه چیزی بفرستیدپاکسازی سمت سرور
fcmهمان رشته‌ای که onNewToken دادفقط فاصله‌گیری دو سر
apnsهگز حروف کوچکفاصله‌گیری، حذف < و > از دو سر، حذف تمام فاصله‌ها، کوچک‌کردن حروف
bazaarتوکنی که سرویس پوش کافه بازار دادفقط فاصله‌گیری دو سر
myketتوکنی که سرویس پوش مایکت دادفقط فاصله‌گیری دو سر
huaweiتوکنی که HMS Push Kit دادفقط فاصله‌گیری دو سر
webpushآدرس endpoint اشتراک مرورگرفقط فاصله‌گیری دو سر
mqttپذیرفته می‌شود، تحویل نمی‌دهدفقط فاصله‌گیری دو سر

پاکسازی APNs تزئینی نیست. APIهای قدیمی iOS توکن را به شکل <a1b2 c3d4> رشته می‌کنند و فرستادن همان، برای همیشه، در هر پیام، از سمت اپل رد می‌شود؛ و کمپین گزارش می‌دهد صددرصد ارسال شد. تست این را می‌سنجد: " <A1B2 C3D4 E5F6> " تبدیل می‌شود به a1b2c3d4e5f6. توجه کنید این پاکسازی فقط برای مسیر apns انجام می‌شود.

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

اگر SDK اپل به شما Data می‌دهد، خودتان به هگز تبدیلش کنید. رشته‌کردن با description همان <a1b2 c3d4> را می‌سازد:

تبدیل درست توکن APNs به رشته
func application(
    _ application: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
    let hex = deviceToken.map { String(format: "%02x", $0) }.joined()
    register(apnsToken: hex)   // your own POST /v1/devices call
}

#اجازه اعلان و پلی سرویسز: هر دو سه‌حالته‌اند

نبودن یک فیلد با false بودنش یکی نیست، و این دو فیلد جایی هستند که این تفاوت پول و مخاطب برایتان خرج می‌کند.

push_enabled اجازه اعلان در سطح سیستم عامل است:

مقدار در JSONمعنیاثر روی هدف‌گیری
فیلد غایب، یا nullنامعلوم؛ SDK آنقدر قدیمی است که گزارش نمی‌دهد«مجاز» خوانده می‌شود
trueکاربر اجازه دادهمجاز
falseکاربر اعلان را در تنظیمات خاموش کردهاز هر کمپینی حذف می‌شود

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

نکته ذخیره‌سازی که به آن برمی‌خورید: ستون push_enabled BOOLEAN NOT NULL DEFAULT TRUE است. یعنی سه‌حالته موقع نوشتن جمع می‌شود. روی درج COALESCE($8, TRUE) و روی به‌روزرسانی COALESCE($8, devices.push_enabled). نامعلوم برای ردیف تازه true می‌شود و ردیف موجود را دست‌نخورده می‌گذارد، چون سکوت SDK نباید چیزی را که نسخه جدیدتر قبلا گفته پاک کند.

has_gms گزارش SDK از سالم بودن گوگل پلی سرویسز است:

مقدار در JSONمعنیاثر
غایب یا nullSDK نگاه نکردهروتر فرض می‌کند احتمالا هست و FCM را امتحان می‌کند
trueپلی سرویسز سالم به نظر رسیدFCM انتخاب اول
falseپلی سرویسز نیستFCM هنگام ارسال کاملا کنار گذاشته می‌شود، و ثبت هشدار fcm_without_gms می‌دهد

توکن FCM حتی وقتی has_gms: false است ذخیره می‌شود، چون پلی سرویسز بعدا می‌تواند نصب شود و دور ریختن توکن آن بازیابی را غیرممکن می‌کرد.

وقتی منظورتان «بررسی نکردم» است، false نفرستید. فیلد را اصلا نگذارید. push_enabled: false دستگاه را از هر کمپینی بیرون می‌برد و has_gms: false تا ثبت بعدی که خلافش را بگوید FCM را برای آن دستگاه خاموش می‌کند.

روی iOS اصلا has_gms نفرستید. آنجا بی‌معنی است و SDK آیفون هم نمی‌فرستد.

#هشدارها

هشدار یعنی چیزی را پذیرفتیم ولی تغییرش دادیم. آرایه warnings در پاسخ 200 هم می‌آید و در پاسخ 400 هم. تنها چهار کد وجود دارد.

codefieldچه وقتتوکن چه می‌شود
empty_tokenنام مسیرتوکن خالی یا فقط فاصله بوددور ریخته می‌شود
token_too_longنام مسیرتوکن از 4096 بایت بلندتر بوددور ریخته می‌شود
transport_not_supportedنام مسیرمسیر از نظر فیزیکی نمی‌تواند به آن پلتفرم برسد، یا اصلا نام مسیر ناشناس استدور ریخته می‌شود
fcm_without_gmsfcmتوکن FCM روی دستگاهی که has_gms: false گزارش کردهنگه داشته می‌شود

متن message هرکدام به ترتیب: token was empty and has been ignored، token exceeds the maximum length and has been ignored، transport <t> cannot deliver to <platform>، device reports no Play Services; FCM will not be used for it.

این نمونه واقعی است. یک نصب اندروید که هم توکن APNs فرستاده و هم FCM:

یک توکن درست و یک توکن اشتباه
curl -X POST https://in.segmentic.net/v1/devices \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "device_id": "dev-1",
    "user_id": "u",
    "platform": "android",
    "tokens": { "apns": "wrong", "fcm": "right" }
  }'

پاسخ 200. توکن قابل استفاده ذخیره شده و آن یکی نه:

JSON
{"status":"ok","warnings":[{"code":"transport_not_supported","message":"transport apns cannot deliver to android","field":"apns"}]}

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

#ردها و کدهای وضعیت

پیام 400 عینا متن خطای اعتبارسنجی است، و هشدارها روی شکست هم همراه می‌آیند. بدون آن‌ها، کسی که از یک بیلد اندروید توکن APNs فرستاده فقط «توکن قابل استفاده‌ای نیست» را می‌بیند و هیچ سرنخی ندارد.

وضعیتmessageچه وقت
400device: device_id is requireddevice_id نیامده، بعد از فاصله‌گیری خالی است، یا از 256 بایت بلندتر است
400device: platform must be one of android, ios, web, windows, macos, linuxplatform نیامده، ناشناس است، یا به server می‌رسد
400device: user_id or anonymous_id is requiredهر دو خالی‌اند
400device: registration carries no usable tokenهیچ توکن قابل استفاده‌ای نماند و اعلان صریحا خاموش اعلام نشده
400malformed JSONبدنه JSON نیست
401missing write keyهیچ کلیدی در هدر یا کوئری نبود
401invalid write keyکلید ناشناس، باطل‌شده، یا معلق
413request body too largeبدنه از ۵ مگابایت گذشت
503cannot verify the write key right now; retryخود جستجوی کلید شکست خورد. با هدر Retry-After: 5
503temporarily unavailable, please retryنوشتن دستگاه در پایگاه داده شکست خورد
405(بدون بدنه JSON)این نصب اصلا انبار دستگاه ندارد، پس مسیر ثبت نشده

نمونه یک 400 کامل، از تستی که پلتفرم ios را با توکن fcm می‌فرستد. ترتیب کلیدها همین است: status، بعد warnings، بعد message:

JSON
{"status":"error","warnings":[{"code":"transport_not_supported","message":"transport fcm cannot deliver to ios","field":"fcm"}],"message":"device: registration carries no usable token"}

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

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

  • device_id بلندتر از 256 بایت پیام device_id is required می‌گیرد، با اینکه یکی فرستاده‌اید. پیام گمراه‌کننده است.
  • POST /v1/devices/unregister با بدنه خراب هم دقیقا همان device_id is required را می‌دهد. JSON خرابتان به‌عنوان «شناسه نفرستادی» گزارش می‌شود.
  • 405 روی /v1/devices در محیط استیجینگ یک واقعیت پیکربندی است نه اشکال بدنه شما. وقتی انبار دستگاه وصل نباشد، مسیر اصلا ثبت نمی‌شود؛ الگوی OPTIONS /v1/ کل مسیرهای زیر /v1/ را ادعا می‌کند، پس سرور مسیر را می‌شناسد و متد را نه.

هر پاسخ این اندپوینت‌ها، چه موفق و چه ناموفق، هدر X-Segmentic-Trace دارد: شانزده رقم هگز. در هیچ بدنه‌ای تکرار نمی‌شود، پس اگر آن را لاگ نکنید از دست می‌رود، و برای پیگیری یک درخواست تنها چیزی است که به کار ما می‌آید.

فهرست کامل قرارداد خطاها در خطاها است.

#همان دستگاه دوباره، همان توکن جای دیگر

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

ثبت دوباره همان دستگاه. ثبت یک upsert روی (tenant_id, device_id) است، و دو قانون در انبار اعمال می‌شود نه در فراخوان:

  • هرگز پاک نکن. فیلدی که SDK نفرستاده مقدار ذخیره‌شده‌اش را نگه می‌دارد. هر ستون رشته‌ای از COALESCE(NULLIF(EXCLUDED.x, ''), devices.x) رد می‌شود. SDKها حقایق دستگاه را از چند جا و در زمان‌های مختلف گزارش می‌کنند، پس اگر سکوت را «پاکش کن» می‌خواندیم، توکن FCM در هر بار باز شدن اپ توکن بازار را پاک می‌کرد.
  • توکن را بگیر. اگر نصب دیگری همان توکن را دارد، از دست داده است.

همچنین last_seen_at همیشه جلو می‌رود، و revoked_at به NULL برمی‌گردد چون نصب دوباره یک دستگاه باطل‌شده را زنده می‌کند. توکنی که بازنشسته شده بود retired_at صفر می‌شود، چون برگشتن یک توکن بازنشسته یعنی اپ دوباره نصب شده و مسیر زنده است.

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

SQL
DELETE FROM device_tokens
WHERE tenant_id = $1 AND transport = $2 AND token = $3 AND device_id <> $4

این حذف داخل همان تراکنش upsert اجرا می‌شود، و یک ایندکس یکتا آن را از «تا حد امکان» به «غیرقابل مذاکره» تبدیل می‌کند:

SQL
CREATE UNIQUE INDEX idx_device_tokens_unique
    ON device_tokens (tenant_id, transport, token)
    WHERE retired_at IS NULL

دو کاربر روی یک گوشی. ردیف دستگاه با نصب کلید می‌خورد، نه با آدم. یک گوشی در عمرش چند حساب را می‌بیند و یک آدم چند گوشی دارد؛ اگر با کاربر کلید می‌خورد، پیام‌های حساب قبلی به هرکسی که بعد وارد می‌شود می‌رسید. با POST /v1/devices بعدی که user_id تازه دارد، upsert همان user_id را جایگزین می‌کند: ورود جدیدتر برنده است. جدا کردن حساب قبلی کار خروج از حساب است و اگر آن را صدا نزنید، تا ثبت بعدی حساب قبلی همچنان چسبیده است.

یک کاربر با چند دستگاه. دو جا محدود می‌شود.

  • سقف تعداد نصب‌هایی که یک نفر روی آن‌ها دریافت می‌کند: پیش‌فرض ۵، از DELIVERY_DEVICES_PER_USER. کسی که پنج بار گوشی عوض کرده هنوز پنج ردیف دارد و بدون سقف همان اعلان را پنج بار می‌گیرد، که اسپم خوانده می‌شود و سریع‌ترین راه از دست دادن اجازه پوش است.
  • نصب‌های کهنه کنار گذاشته می‌شوند: پیش‌فرض ۱۸۰ روز، از DELIVERY_STALE_DEVICE.

سقف دستگاه می‌شمارد نه ردیف توکن. گوشی با سه مسیر یک گیرنده است. ترتیب last_seen_at DESC است، تازه‌ترین نصب اول.

نصب ناشناس. ثبتی که فقط anonymous_id دارد پذیرفته و ذخیره می‌شود، ولی امروز هدف هیچ کمپینی نمی‌شود. کوئری تحویل فقط روی d.user_id = ANY($2) انتخاب می‌کند و هیچ مسیر کدی در انبار وجود ندارد که دستگاه را با anonymous_id بخواند. پس پوش به یک نصب هنوز شناسایی‌نشده وجود ندارد. اگر کمپین خوش‌آمد برای کاربر ثبت‌نام‌نکرده می‌خواهید، امروز جوابی ندارد؛ باید اول شناسایی اتفاق بیفتد.

#خروج از حساب و حذف نصب

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

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

فیلدنوعاجباریمعنی
device_idرشتهبلهنصبی که روی آن عمل می‌شود
user_idرشتهنهوقتی بیاید، فقط اگر همان کاربر چسبیده باشد جدا می‌کند
revokedبولیننه، پیش‌فرض falsefalse خروج از حساب، true نصب را رفته علامت می‌زند
خروج از حساب
curl -X POST https://in.segmentic.net/v1/devices/unregister \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{"device_id": "dev-1", "user_id": "u_123"}'
JSON
{"status":"ok"}
نصب رفته است
curl -X POST https://in.segmentic.net/v1/devices/unregister \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{"device_id": "dev-1", "revoked": true}'
JSON
{"status":"ok"}

در پایگاه داده دقیقا این اتفاق می‌افتد. خروج از حساب:

SQL
UPDATE devices SET user_id = '', last_seen_at = now()
WHERE tenant_id = $1 AND device_id = $2
  AND ($3 = '' OR user_id = $3)

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

و حذف نصب:

SQL
UPDATE devices SET
    revoked_at = now(),
    revoked_reason = 'unregistered',
    first_uninstalled_at = COALESCE(first_uninstalled_at, now())
WHERE tenant_id = $1 AND device_id = $2 AND revoked_at IS NULL

دلیل unregistered است نه uninstalled، چون این SDK است که unregister را صدا می‌زند و در عمل خیلی بیشتر خروج از حساب است تا حذف اپ. یکی شمردن این دو، هر خروج از حساب را در گزارش حذف نصب شبیه ریزش نشان می‌داد.

هیچ‌کدام از این دو، توکن‌ها را بازنشسته نمی‌کند. دستگاه باطل‌شده با شرط d.revoked_at IS NULL از ارسال کنار می‌رود.

پاسخ‌ها: 200 با {"status":"ok"}؛ 400 با {"status":"error","message":"device_id is required"} وقتی بدنه JSON نیست یا device_id خالی است؛ 503 با پیام موقت وقتی نوشتن شکست بخورد؛ و همان 401 و 503 احراز هویت بخش قبل.

#معادل مرورگر: اشتراک وب‌پوش

مرورگر توکن ندارد، اشتراک دارد. معادل POST /v1/devices روی وب دو اندپوینت جدا است.

ثبت اشتراک مرورگر
curl -X POST https://in.segmentic.net/v1/webpush/subscribe \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "u_9137",
    "subscription": {
      "endpoint": "https://fcm.googleapis.com/fcm/send/abc123",
      "p256dh": "BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U",
      "auth": "tBHItJI5svbpez7KI4CCXg"
    }
  }'
JSON
{"status":"ok"}

شکل تخت هم پذیرفته می‌شود، یعنی endpoint و p256dh و auth در سطح بالا. شکل تودرتو وجود دارد چون همان شکل خود Push API است: صفحه می‌تواند عینا چیزی را که مرورگر داده پست کند، بدون اینکه بازش کند و مهم‌تر، بدون اینکه کلیدها را دوباره کدگذاری کند. رشته base64 که یک تابع کمکی خوش‌نیت رمزگشایی و دوباره کدگذاری کرده، کلاسیک‌ترین راهی است که یک اشتراک بی‌صدا روی ماشین گیرنده از رمزگشایی می‌افتد.

فیلدنوعاجباریمعنی
user_idرشتهبلهاشتراک به نام یک آدم ذخیره می‌شود
endpointرشتهبلهآدرس سرویس پوش این نصب مرورگر. داشتنش برای ارسال کافی است، پس مثل یک راز با آن رفتار می‌شود و هرگز به کلاینت برنمی‌گردد
p256dhرشتهبلهکلید عمومی مرورگر، base64url، نقطه فشرده‌نشده P-256
authرشتهبلهراز مشترک ۱۶ بایتی که مرورگر ساخته

هر چهار تا لازم‌اند. نبودن هرکدام 400 با {"status":"error","message":"user_id and a complete subscription are required"} می‌دهد و هیچ‌چیز ذخیره نمی‌شود: endpoint بدون کلید غیرقابل استفاده است، بدنه رمز نمی‌شود، و ذخیره‌اش به‌جای یک اتصال خراب، یک گیرنده همیشه‌ناموفق نشان می‌داد. هدر User-Agent درخواست هم کنارش ذخیره می‌شود.

لغو اشتراک فقط endpoint می‌خواهد:

لغو اشتراک
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/abc123"}'
JSON
{"status":"ok"}

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

کانال وب‌پوش امروز علاوه بر اشتراک، به یک ردیف دستگاه هم نیاز دارد. توزیع‌کننده برای کانال‌های push و webpush سراغ رجیستری دستگاه می‌رود و اگر چیزی پیدا نکند، قبل از اینکه فرستنده وب‌پوش اصلا صدا زده شود، پیام را با دلیل not_reachable کنار می‌گذارد. اشتراک‌های مرورگر در جدول جداگانه‌ای (webpush_subscriptions) ذخیره می‌شوند، پس بازدیدکننده‌ای که فقط POST /v1/webpush/subscribe را صدا زده امروز غیرقابل دسترس شمرده می‌شود. این بررسی شرط ندارد: روی نصبی که اصلا انبار دستگاه وصل نشده هم اجرا می‌شود، و آنجا هر پوش و هر وب‌پوش، بدون استثنا، همین‌جا کنار گذاشته می‌شود.

راه‌حل امروزی این است که همان مرورگر را به‌عنوان دستگاه هم ثبت کنید، با platform: "web" و endpoint به‌عنوان توکن مسیر webpush:

همان مرورگر، به‌عنوان یک ردیف دستگاه
curl -X POST https://in.segmentic.net/v1/devices \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "device_id": "browser-9f31",
    "user_id": "u_9137",
    "platform": "web",
    "tokens": { "webpush": "https://fcm.googleapis.com/fcm/send/abc123" }
  }'
JSON
{"status":"ok"}

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

بقیه سطح وب، شامل service worker و اینکه چرا باید از ریشه دامنه سرو شود و کلید VAPID را از کجا می‌گیرید، در SDK وب است.

#پیام‌رسان‌ها: بله، ایتا، روبیکا

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

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

وصل کردن یک شناسه چت
curl -X POST https://in.segmentic.net/v1/messenger/link \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "u_9137",
    "platform": "bale",
    "chat_id": "44120099",
    "source": "bot_start"
  }'
JSON
{"status":"ok"}
فیلدنوعاجباریپیش‌فرض
user_idرشتهبله
platformرشتهبله، یکی از bale، eitaa، rubika
chat_idرشتهبله
usernameرشتهنهذخیره می‌شود و هرگز با ثبت بعدی پاک نمی‌شود
sourceرشتهنهbot_start

source ثبت می‌کند شناسه از کجا آمده. پیش‌فرضش bot_start است، تنها مسیری که رضایت واقعی دارد؛ هر چیز دیگر ارزش دارد که بعدا بشود پیدایش کرد. کسی که در بات /start زده داستان رضایتش با کسی که شناسه‌اش از یک فایل CSV آمده یکی نیست.

قطع اتصال فقط user_id و platform می‌خواهد:

قطع اتصال
curl -X POST https://in.segmentic.net/v1/messenger/unlink \
  -H "Authorization: Bearer wk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{"user_id": "u_9137", "platform": "eitaa"}'
JSON
{"status":"ok"}

ردها: 400 با {"status":"error","message":"user_id, chat_id and a known platform are required"} روی اتصال، و {"status":"error","message":"user_id and a known platform are required"} روی قطع اتصال. پلتفرمی مثل telegram همینجا رد می‌شود؛ ستون یک CHECK دارد و مقدار ناشناخته در پستگرس با خطایی می‌شکست که هیچ‌کس نمی‌تواند رویش کاری بکند.

وصل کردن دوباره تنها راه پاک‌کردن یک بلاک است: blocked_at و blocked_reason صفر می‌شوند و هیچ‌چیز دیگری آن‌ها را پاک نمی‌کند. تلاش خودکاری که تصمیم بگیرد بلاک منقضی شده، پیام دادن به کسی است که رفته.

هیچ وب‌هوکی برای پیام‌های بات وجود ندارد. مسیر ورودی وب‌هوک فقط منابع یکپارچه‌سازی (دیجی‌کالا، باسلام، ترب، زرین‌پال، ووکامرس، شاپیفای، سگمنت) را می‌پذیرد و هیچ‌کدام پیام‌رسان نیستند. یعنی بات را خودتان اجرا می‌کنید: بات شما /start می‌گیرد، بک‌اند شما شناسه چت را به شناسه کاربر خودتان نگاشت می‌کند، و بک‌اند شما POST /v1/messenger/link را صدا می‌زند.

#وقتی کمپین پوش می‌فرستد، دقیقا چه می‌شود

ترتیب مراحل خودش یک تصمیم است.

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

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

کوئری تحویل این است:

SQL
SELECT d.user_id, d.device_id, d.platform, d.has_gms,
       d.app_version, d.locale, d.timezone, d.last_seen_at,
       t.transport, t.token
FROM devices d
JOIN device_tokens t
  ON t.tenant_id = d.tenant_id AND t.device_id = d.device_id AND t.retired_at IS NULL
WHERE d.tenant_id = $1
  AND d.user_id = ANY($2)
  AND d.revoked_at IS NULL
  AND d.push_enabled
  AND d.last_seen_at >= $3
ORDER BY d.user_id, d.last_seen_at DESC, d.device_id

پس یک دستگاه برای کمپین نامرئی است اگر: توکن زنده نداشته باشد، باطل شده باشد، push_enabled آن false باشد، بیش از ۱۸۰ روز دیده نشده باشد، بعد از پنجمین نصب تازه بیاید، یا اصلا user_id نداشته باشد.

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

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

برای هر دستگاه، روتر مسیر را انتخاب می‌کند. ترتیب اولویت:

پلتفرمترتیب امتحان
androidfcm، بعد bazaar، بعد myket، بعد huawei، بعد mqtt
iosapns، بعد mqtt
webwebpush

FCM اول است چون وقتی پلی سرویسز کار می‌کند سریع‌ترین و ارزان‌ترین مسیر است. بازار و مایکت بعدش، چون دقیقا همان دستگاه‌هایی را می‌پوشانند که FCM نمی‌تواند. mqtt آخر است و امروز هیچ ارائه‌دهنده‌ای ندارد، پس همیشه کنار می‌رود.

این فهرست با سه چیز فیلتر می‌شود: دستگاه واقعا برای آن مسیر توکن داشته باشد، ارائه‌دهنده‌ای برایش پیکربندی شده باشد، و برای FCM، دستگاه has_gms: false گزارش نکرده باشد. صفر مسیر یعنی نتیجه rejected با متن push: no usable transport for this device.

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

نتیجه ارائه‌دهندهچه می‌شود
sentبرمی‌گردد، موفقیت روی مدار ثبت می‌شود
invalid_tokenمسیر بعدی امتحان می‌شود، و آن توکن بازنشسته می‌شود
unavailable یا rate_limitedمسیر بعدی امتحان می‌شود، شکست روی مدار شمرده می‌شود
rejectedهمانجا برمی‌گردد. بدنه ردشده همه‌جا رد می‌شود و امتحان مسیر دیگر فقط سهمیه می‌سوزاند

توکن مرده روی یک مسیر هیچ چیزی درباره مسیرهای دیگر نمی‌گوید، و همین رد شدن به مسیر بعدی است که مخاطبی را که FCM نمی‌تواند برساند بازمی‌گرداند.

توکن مرده بازنشسته می‌شود و آخرینش نصب را باطل می‌کند. وقتی ارائه‌دهنده بگوید توکن نامعتبر است، آن توکن retired_at می‌خورد. اگر آن دستگاه دیگر هیچ توکن زنده‌ای نداشته باشد، خود دستگاه با revoked_reason = 'uninstall_detected' باطل می‌شود. این تنها سیگنال واقع‌بینانه حذف اپ است: اپ نمی‌تواند در حین حذف شدن unregister را صدا بزند، پس بدون این، پایگاه نصب هر مشتری فقط رشد می‌کرد. بازنشسته می‌شود نه حذف، تا اگر همان توکن با ثبت بعدی برگشت، نصب دوباره تشخیص داده شود.

مدار محافظ. ۱۰ شکست پشت سر هم یک مسیر را به مدت ۳۰ ثانیه باز می‌کند. اگر همه مسیرها به‌عنوان در دسترس نبودن کنار رفتند، نتیجه unavailable با متن every transport was unavailable است.

هر پوش شناسه‌های انتساب را با خودش می‌برد، و جای آن‌ها در هر مسیر فرق می‌کند:

مسیرشناسه‌ها کجا سوارند
fcmداخل message.data: sg_mid، sg_cid، sg_jid، sg_link، sg_t
bazaar و myket و huaweiداخل data: sg_mid، sg_link، sg_t. بدون شناسه کمپین و سناریو. هواوی این نگاشت را به‌صورت رشته JSON می‌گیرد نه شیء
apnsسطح بالای بدنه، کنار aps: sg_mid، sg_link، sg_t
webpushداخل JSON رمزشده: mid، tkn، url

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

#خواندن دستگاه‌های یک پرونده

این کارت پرونده در پنل است، نه یک اندپوینت که کلید API شما به آن برسد. مسیرش GET /v1/profiles/{user_id}/devices است و فقط روی کنترل‌پلین داشبورد ثبت شده؛ آن لیسنر عمدا از بیرون شبکه داخلی قابل دسترس نیست. همان مسیر روی https://api.segmentic.net به هندلر پیش‌فرض می‌افتد و 404 unknown_endpoint می‌گیرد.

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

HTTP
GET /api/proxy/v1/profiles/u_9137/devices
JSON
{
  "devices": [
    {
      "platform": "android",
      "transports": "bazaar,fcm",
      "app_version": "3.4.0",
      "model": "Xiaomi Redmi Note 12",
      "timezone": "Asia/Tehran",
      "push_enabled": true,
      "revoked": false,
      "last_seen": "2026-08-05T09:14:22Z"
    }
  ]
}

مجوز لازم profile.read است. کسی که اجازه دارد ویژگی‌ها و تاریخچه رویداد یک آدم را ببیند، از قبل نیمه حساس‌تر ماجرا را می‌خواند.

فیلدنوعتوضیح
platformرشته
transportsرشتهبا ویرگول جدا شده. یک نصب با دو توکن یک گوشی است نه دو تا. توکن‌های بازنشسته هم در همین رشته می‌آیند، پس مسیری که اینجا می‌بینید لزوما زنده نیست
app_versionرشتهوقتی خالی باشد نمی‌آید
modelرشتهوقتی خالی باشد نمی‌آید
timezoneرشتهوقتی خالی باشد نمی‌آید
push_enabledبولین
revokedبولین
last_seenرشتهRFC3339 به وقت UTC

ترتیب last_seen_at DESC است و سقف 50 ردیف. نصب‌های باطل‌شده عمدا در فهرست هستند: «شما اپ را سوم ماه حذف کردید» جواب سوال «چرا پوش نمی‌گیرم؟» است و پنهان کردن ردیف، سوال را بی‌جواب می‌گذارد.

خطاها: 400 با user_id is required، و 503 وقتی کوئری شکست بخورد.

درخواست حق فراموش‌شدن اول device_tokens و بعد ردیف devices را حذف می‌کند، چون توکن‌ها به device_id آویزان‌اند و در غیر این صورت تا ابد به آن‌ها پوش می‌رفت. جزئیات در حریم خصوصی.

#چیزهایی که وجود ندارند

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

  • هیچ مسیر دستگاهی روی هاست مدیریت (sk_seg_...)، حتی خواندن. GET /v1/profiles/{user_id}/devices فقط روی کنترل‌پلین داشبورد ثبت شده و عمومی مسیریابی نمی‌شود، پس با کلید API نمی‌شود فهرست دستگاه‌های یک نفر را گرفت.
  • هیچ ابزار MCP مربوط به دستگاه یا پوش.
  • ارائه‌دهنده MQTT. ثابتش هست، ثبت می‌شود، ارسال نمی‌شود.
  • اندپوینت ثبت دسته‌ای دستگاه. یک درخواست، یک دستگاه.
  • هیچ شکل GET یا DELETE از این اندپوینت‌ها. همه POST با بدنه JSON‌اند.
  • ترتیب مسیر پیش‌فرض برای windows، macos و linux. ثبت می‌شوند، تحویل نمی‌گیرند.
  • هر جستجوی دستگاه بر اساس anonymous_id. نصب ناشناس ذخیره می‌شود و هدف هیچ کمپینی نمی‌شود.
  • وب‌هوک پیام بات که شناسه چت بله، ایتا یا روبیکا را خودکار وصل کند.
  • هر اعتبارسنجی روی شکل توکن FCM، بازار یا مایکت، فراتر از «خالی نباشد» و «از 4096 بایت بلندتر نباشد».
  • هدر apns-topic روی درخواست‌های APNs، و با آن apns-push-type، apns-expiration، apns-priority و apns-collapse-id. اپل برای احراز هویت مبتنی بر توکن apns-topic را لازم دارد، پس این یک شکاف واقعی است نه ظرافت مستندات. عملا یعنی ttl، collapse_key و priority روی FCM اعمال می‌شوند و روی APNs نادیده گرفته می‌شوند.
  • اعتبارنامه APNs در سطح مشتری. کاتالوگ کانال‌ها برای push فقط fcm، bazaar، myket و huawei دارد، پس پوش iOS برای همه مشتری‌های یک نصب روی کلید APNs خود آن نصب می‌رود.
  • ارسال تاییدشده بازار یا مایکت با توکن واقعی فروشگاه. نگاشت توکن پذیرفته می‌شود ولی هرگز با توکن واقعی امتحان نشده، چون هر دو به حساب توسعه‌دهنده و اپ منتشرشده نیاز دارند.
  • تست سطح توزیع‌کننده برای کانال وب‌پوش.
قبلیSDK اندرویدبعدیسرور به سرور

در این صفحه

  • دو مسیر، و اینکه کدام را لازم دارید
  • مسیر SDK اندروید
  • ثبت دستگاه با POST /v1/devices
  • هر فیلد بدنه
  • پلتفرم‌ها و مسیرهایی که به هرکدام می‌رسند
  • شکل توکن هر مسیر
  • اجازه اعلان و پلی سرویسز: هر دو سه‌حالته‌اند
  • هشدارها
  • ردها و کدهای وضعیت
  • همان دستگاه دوباره، همان توکن جای دیگر
  • خروج از حساب و حذف نصب
  • معادل مرورگر: اشتراک وب‌پوش
  • پیام‌رسان‌ها: بله، ایتا، روبیکا
  • وقتی کمپین پوش می‌فرستد، دقیقا چه می‌شود
  • خواندن دستگاه‌های یک پرونده
  • چیزهایی که وجود ندارند

سگمنتیک

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