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

مرجع: API مدیریتی

مسیرهای پایدار میزبان مدیریتی برای مخاطب، کمپین، سناریو، گزارش و پیام، همراه مجوز هرکدام.

#میزبان و کلید

این API روی https://api.segmentic.net جواب می‌دهد و همه مسیرهایش با /v1/ شروع می‌شود. هیچ بخش /api در مسیر نیست. اگر جایی نوشته شده /api/v1/events، غلط است و جواب آن 404 با کد unknown_endpoint است.

کلیدی که اینجا پذیرفته می‌شود فقط sk_seg_... است. کلید نوشتن (wk_seg_...) که داخل اپ و سایت شما می‌نشیند، اینجا رد می‌شود و پیام خطا هم دقیقا همین را می‌گوید، چون کسی که با کلید نوشتن به اینجا رسیده معمولا مقدار اشتباه را از یک صفحه کپی کرده و کلمه «unauthorized» او را دنبال یک غلط املایی می‌فرستد.

این یک شنونده جداگانه است، نه یک پیشوند مسیر. دو مسیری که کلید می‌سازند (POST /v1/team/keys و POST /v1/team/invites) متن خام یک اعتبارنامه را در بدنه پاسخ برمی‌گردانند؛ روی یک mux مشترک، فاصله آن‌ها تا عمومی‌شدن یک نگهبان فراموش‌شده بود. اینجا اصلا آدرس‌پذیر نیستند. نبودن از بررسی‌کردن بهتر است.

سه چیز که مرورگر را از این در بیرون نگه می‌دارد:

  • نشست (کوکی) پذیرفته نمی‌شود. یعنی این سطح هیچ اعتبارنامه محیطی ندارد و پرسش CSRF از اساس منتفی است.
  • هیچ پاسخ‌دهنده‌ای برای preflight روی این mux ثبت نشده است. یک OPTIONS از مرورگر به catch-all می‌خورد و 404 می‌گیرد.
  • این API برای تماس سرور به سرور است. کلید sk_seg_ را در کد سمت مرورگر نگذارید.

روی یک نصب محلی، این API تا وقتی PUBLIC_API_ADDR مقدار نگیرد اصلا سرو نمی‌شود؛ پیش‌فرضش رشته خالی است. اولین کاری که یک یکپارچه‌سازی تازه باید بکند خواندن GET /v1/status است.

متد اشتباه روی یک مسیر واقعی، 405 نمی‌گیرد. چون مسیرها با الگوی متددار ثبت شده‌اند، PUT /v1/campaigns/5 به catch-all می‌افتد و همان 404 با کد unknown_endpoint را می‌گیرد.

#همه مسیرها، یک‌جا

بیست‌ودو مسیر با احراز هویت، به‌علاوه یک probe وضعیت و یک catch-all. غیر از این‌ها چیزی روی این میزبان وجود ندارد.

متد و مسیرمجوزهزینهقفل نرموقتی ثبت می‌شود که
GET /v1/statusنداردنداردخیرهمیشه
GET /v1/whoamiندارد۱خیرهمیشه
GET /v1/capabilitiesندارد۱خیرهمیشه
GET /v1/schema/eventsevent.read۵خیرهمیشه
GET /v1/schema/traitsevent.read۵خیرهمیشه
GET /v1/ingest/qualityevent.read۵خیرهمیشه
POST /v1/audiences/validatesegment.read۱خیرهمیشه
POST /v1/audiences/countsegment.read۲۵خیرهمیشه
GET /v1/segmentssegment.read۱خیرsegments
GET /v1/segments/{id}segment.read۱خیرsegments
POST /v1/segmentssegment.write۱خیرsegments
PUT /v1/segments/{id}segment.write۱خیرsegments
DELETE /v1/segments/{id}segment.delete۱خیرsegments
GET /v1/campaignscampaign.read۱خیرcampaigns
GET /v1/campaigns/{id}campaign.read۵خیرcampaigns
POST /v1/campaignscampaign.write۱خیرcampaigns
PUT /v1/campaigns/{id}/recurrencecampaign.send۱خیرcampaigns و campaign_recurrence
DELETE /v1/campaigns/{id}/recurrencecampaign.write۱خیرcampaigns و campaign_recurrence
POST /v1/campaigns/{id}/sendcampaign.send۱خیرcampaigns
GET /v1/templatestemplate.read۱خیرtemplates
GET /v1/templates/{id}template.read۱خیرtemplates
POST /v1/templatestemplate.write۱خیرtemplates
POST /v1/templates/rendertemplate.read۱خیرtemplates
GET /v1/journeysjourney.read۱خیرjourneys
GET /v1/journeys/{id}journey.read۵خیرjourneys
POST /v1/journeysjourney.write۱خیرjourneys
GET /v1/journeys/{id}/draftjourney.read۱خیرjourneys
POST /v1/journeys/validatejourney.read۱خیرjourneys
POST /v1/journeys/{id}/publishjourney.publish۱خیرjourneys
POST /v1/journeys/{id}/{action}journey.write۱خیرjourneys
POST /v1/campaigns/{id}/submitcampaign.write۱خیرcampaigns و campaign_approval
POST /v1/eventsprofile.write۵خیرingest
GET /v1/exportsdata.export۱بلهasync_exports
POST /v1/exportsdata.export۲۵بلهasync_exports
POST /v1/reports/funnelanalytics.read۲۵بلهanalytics
POST /v1/reports/retentionanalytics.read۲۵بلهanalytics
POST /v1/messagescampaign.send۱خیرtransactional

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


#احراز هویت

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

  1. هدر Authorization: Bearer <token> (نام scheme به بزرگی و کوچکی حرف حساس نیست)
  2. هدر X-Segmentic-Key: <token>
  3. کوکی‌های __Host-segmentic_session و بعد segmentic_session

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

Shell
curl -s https://api.segmentic.net/v1/whoami \
  -H "Authorization: Bearer sk_seg_..."

چهار جواب 401 وجود دارد و هرکدام کد خودش را دارد، چون هرکدام شما را به یک جای متفاوت می‌فرستند:

کدچه اتفاقی افتادهپیام
unauthenticatedهیچ اعتبارنامه‌ای نیامده، یا آمده و resolve نشده (کلید باطل‌شده، کلید ناشناس، حساب معلق)a valid API key is required
api_key_requiredیک نشست معتبر آمده، کوکی یا bearerthis API accepts sk_seg_ keys only; session credentials are not valid here
write_key_rejectedتوکنی که با wk_ شروع می‌شود، پیش از هر جست‌وجویی در دیتابیسمتن پایین
key_expiredستون expires_at کلید گذشته استthis API key has expired
JSON
{
  "error": {
    "code": "write_key_rejected",
    "message": "that is an SDK write key (wk_…); this API needs a management key (sk_seg_…)"
  }
}

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

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

JSON
{
  "error": {
    "code": "forbidden",
    "message": "this key does not carry campaign.send, see GET /v1/whoami for what it does carry",
    "need": "campaign.send"
  }
}

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

  • اگر حساب فهرست IP مجاز تنظیم کرده باشد و آن را به کلیدهای API هم اعمال کرده باشد، تماس از نشانی خارج از فهرست 403 با کد ip_not_allowed می‌گیرد.
  • اعتبارنامه کارکنان سگمنتیک روی این میزبان 401 با کد wrong_surface می‌گیرد.

هر دو در پاکت مسطح پنل جواب می‌دهند، نه پاکت این API. جزئیات در پاکت خطا.

کلیدها منقضی می‌شوند. کلیدی که در پنل ساخته می‌شود، اگر عددی ندهید، ۳۶۵ روز عمر دارد. کلید با نقش owner ساخته نمی‌شود، پس هیچ کلید API هرگز tenant.transfer یا tenant.delete ندارد.


#GET /v1/whoami

مجوز نمی‌خواهد، هزینه ۱. هر کلید معتبری جواب می‌گیرد. این و توانمندی‌ها دو تماسی هستند که یک کلاینت باید موقع راه‌اندازی بزند: یکی می‌گوید این کلید چه می‌تواند، دیگری می‌گوید این نصب چه دارد.

Shell
curl -s https://api.segmentic.net/v1/whoami \
  -H "Authorization: Bearer sk_seg_..."
JSON
{
  "tenant_id": 7,
  "api_key_id": 3,
  "role": "analyst",
  "permissions": [
    "analytics.read",
    "audit.read",
    "campaign.read",
    "data.export",
    "event.read",
    "journey.read",
    "member.read",
    "profile.read",
    "segment.read",
    "settings.read",
    "template.read"
  ],
  "scoped": false
}
فیلدنوعهمیشه هستمعنی
tenant_idعددبلهشناسه حساب
api_key_idعددبلهشناسه همین کلید. نام روی سیم api_key_id است، نه key_id
roleرشتهبلهیکی از هفت نقش
permissionsآرایه رشتهبله، خالی هم [] است نه nullمجموعه موثر، مرتب‌شده الفبایی
scopedبولینبلهاینکه کلید زیر نقش خودش باریک شده یا نه

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

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

بودجه باقی‌مانده در این پاسخ نیست. هیچ راهی برای پرسیدن «چقدر بودجه مانده» وجود ندارد. رجوع کنید به بودجه درخواست.

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


#GET /v1/capabilities

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

Shell
curl -s https://api.segmentic.net/v1/capabilities \
  -H "Authorization: Bearer sk_seg_..."
JSON
{
  "version": "v1",
  "features": {
    "segments": true,
    "campaigns": true,
    "analytics": true,
    "transactional": true,
    "export": false,
    "import": true,
    "journeys": true,
    "ingest": true,
    "async_exports": true,
    "campaign_approval": true
  },
  "limits": {
    "max_page_size": 100,
    "max_preview_rows": 100,
    "max_batch_size": 500,
    "estimate_sample": 100,
    "query_timeout_sec": 30
  }
}

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

#features، کلید به کلید

کلیدچه مسیرهایی را روشن می‌کند
segmentsپنج مسیر /v1/segments
campaignsهفت مسیر /v1/campaigns
campaign_recurrenceدو مسیر PUT و DELETE /v1/campaigns/{id}/recurrence
analyticsPOST /v1/reports/funnel و POST /v1/reports/retention
transactionalPOST /v1/messages
exportهیچ‌چیز روی این میزبان. خروجی‌گیر CSV پنل است. اطلاعاتی است و بس
importهیچ‌چیز روی این میزبان. بارگذاری CSV پنل است
journeysهیچ‌چیز روی این میزبان. سناریو هیچ مسیر عمومی ندارد
ingestPOST /v1/events
async_exportsGET /v1/exports و POST /v1/exports
campaign_approvalPOST /v1/campaigns/{id}/submit، و دروازه تایید روی ارسال

ingest و import دقیقا یک بولین‌اند و هیچ‌وقت با هم مخالف نمی‌شوند. جدا نام‌گذاری شده‌اند چون کلاینتی که این دو را یکی بگیرد، ممکن است یک batch را به نصبی بفرستد که فقط آن یکی را سرو می‌کند.

#limits، کلید به کلید

کلیدمقدارچه چیزی را واقعا محدود می‌کند
max_page_size۱۰۰سقف ?limit= روی GET /v1/exports. تنها مسیری که limit را می‌خواند
max_preview_rows۱۰۰پیش‌نمایش سگمنت در پنل. هیچ مسیری روی این میزبان به آن پایبند نیست، چون مسیر پیش‌نمایش عمومی وجود ندارد
max_batch_size۵۰۰حداکثر رویداد در یک POST /v1/events
estimate_sample۱۰۰نرخ نمونه‌گیری شمارنده زنده پنل. هیچ مسیری روی این میزبان از آن استفاده نمی‌کند
query_timeout_sec۳۰مهلت context روی بیشتر هندلرها

query_timeout_sec مهلت دو مسیر گزارش نیست. قیف و ماندگاری ۴۵ ثانیه مهلت دارند و این عدد هیچ‌جا منتشر نمی‌شود.


#مجوزها

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

بیست‌وهشت مجوز وجود دارد. این‌ها رشته‌های دقیق روی سیم‌اند.

مجوزروی این میزبان چه چیزی را باز می‌کند
segment.readPOST /v1/audiences/validate، POST /v1/audiences/count، GET /v1/segments، GET /v1/segments/{id}
segment.writePOST /v1/segments، PUT /v1/segments/{id}
segment.deleteDELETE /v1/segments/{id}
profile.readهیچ‌چیز
profile.writePOST /v1/events
event.readGET /v1/schema/events، GET /v1/schema/traits
campaign.readGET /v1/campaigns، GET /v1/campaigns/{id}
campaign.writePOST /v1/campaigns، POST /v1/campaigns/{id}/submit، DELETE /v1/campaigns/{id}/recurrence
campaign.sendPUT /v1/campaigns/{id}/recurrence، POST /v1/campaigns/{id}/send، POST /v1/messages
campaign.approveهیچ‌چیز. مسیر تایید عمومی وجود ندارد
analytics.readPOST /v1/reports/funnel، POST /v1/reports/retention
data.exportGET /v1/exports، POST /v1/exports
journey.readهیچ‌چیز
journey.writeهیچ‌چیز
journey.publishهیچ‌چیز
template.readهیچ‌چیز
template.writeهیچ‌چیز
member.readهیچ‌چیز
member.writeهیچ‌چیز
apikey.readهیچ‌چیز
apikey.writeهیچ‌چیز
settings.readهیچ‌چیز
settings.writeهیچ‌چیز
billing.readهیچ‌چیز
billing.writeهیچ‌چیز
audit.readهیچ‌چیز
tenant.transferهیچ‌چیز، و هیچ کلیدی نمی‌تواند آن را داشته باشد
tenant.deleteهیچ‌چیز، و هیچ کلیدی نمی‌تواند آن را داشته باشد

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

#نقش‌ها

نقش‌ها به‌جای ارث‌بری، سطر به سطر نوشته شده‌اند. ارث‌بری («ادمین یعنی viewer به‌علاوه چند چیز») پرسش جالب یعنی «دقیقا یک marketer چه می‌تواند بکند» را به خواندن چهار تعریف دیگر و ترکیب‌کردنشان در ذهن تبدیل می‌کند.

نقشروی این میزبان چه می‌تواند بکند
ownerهر بیست‌ودو مسیر. ولی کلید API با این نقش ساخته نمی‌شود
adminهر بیست‌ودو مسیر
marketerهر بیست‌ودو مسیر. ده مجوزی که این میزبان استفاده می‌کند همه در این نقش هستند
analystفهرست رویدادها و ویژگی‌ها، اعتبارسنجی و شمارش مخاطب، خواندن سگمنت، خواندن کمپین، گزارش، خروجی
viewerهمان‌های analyst، منهای خروجی
approverهمان‌های viewer. campaign.approve را دارد که اینجا هیچ دری ندارد
financeهیچ‌چیز. فقط billing.read و billing.write دارد و هیچ‌کدام اینجا دری ندارند

اگر یک عامل هوش مصنوعی می‌سازید و می‌خواهید فقط گزارش بخواند، viewer بدهید نه analyst. analyst سه مجوز دارد که viewer ندارد: profile.read و data.export و audit.read، یعنی دیدن شماره تلفن آدم‌ها، بیرون‌بردن فایل، و خواندن رد ممیزی. روی این میزبان فقط data.export مسیری باز می‌کند. آن دو تای دیگر همان‌طور که جدول مجوزها در بالا می‌گوید اینجا هیچ‌چیز باز نمی‌کنند.


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

«درخواست در دقیقه» واحد غلطی است برای سطحی که یک تماسش یک struct می‌خواند و تماس بعدی‌اش یک انبار داده را اسکن می‌کند. عاملی که در دقیقه ۶۰۰ بار whoami بزند بی‌آزار است؛ عاملی که در دقیقه ۶۰۰ گزارش ماندگاری بگیرد یک قطعی خودخواسته است.

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

کلاسواحدیعنی
trivial۱چیزی نمی‌خواند، یا یک سطر با کلید اصلی می‌خواند
query۵یک کوئری کران‌دار روی انبار داده
heavy۲۵اسکنی که هزینه‌اش با تاریخچه حساب بزرگ می‌شود

سهمیه پیش‌فرض ۶۰۰ واحد در دقیقه است، با متغیر محیطی PUBLIC_API_BUDGET_PER_MINUTE. یعنی تقریبا «دو گزارش سنگین در دقیقه، یا ششصد تماس ارزان».

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

پنجره یک دقیقه تقویمی ثابت است، نه لغزان. کلید Redis برای هر ترکیب حساب و کلید و شماره دقیقه ساخته می‌شود و ۷۰ ثانیه عمر می‌کند.

وقتی بودجه تمام شود:

HTTP
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json; charset=utf-8

{"error":{"code":"budget_exhausted","message":"this key has spent its request budget for the minute"}}

هزینه پیش از رد کردن کسر می‌شود، پس کلاینتی که به یک کلید تمام‌شده فشار می‌آورد فقط شمارنده همان دقیقه را باد می‌کند؛ پنجره را طولانی‌تر نمی‌کند ولی چیزی هم به دست نمی‌آورد. چون پنجره تقویمی است، Retry-After: 60 محافظه‌کارانه است و بودجه ممکن است زودتر برگردد.

اگر خود شمارنده در دسترس نباشد، جواب 503 است:

JSON
{"error":{"code":"budget_unavailable","message":"could not verify the request budget"}}

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

نتیجه‌اش این است: قطعی Redis کل این API را با 503 می‌خواباند، از جمله GET /v1/whoami و GET /v1/capabilities. تنها مسیری که زنده می‌ماند GET /v1/status است.

هدر X-RateLimit-Limit، X-RateLimit-Remaining و X-RateLimit-Reset روی بودجه وجود ندارد. کلاینت نمی‌تواند ببیند چقدر مانده و whoami هم نمی‌گوید. تنها هدرهای نرخ روی این میزبان مال POST /v1/messages است و آن‌ها محدودکننده جداگانه پیام را توصیف می‌کنند، نه بودجه را.

403 بابت نبود مجوز پیش از کسر بودجه می‌آید، پس تماس رد شده بابت مجوز هزینه‌ای ندارد.


#صفحه‌بندی

این بخش را کامل بخوانید، چون آنچه در کد هست با آنچه انتظار دارید فرق می‌کند.

پاکت صفحه این شکل است:

JSON
{
  "data": [],
  "has_more": false
}

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

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

امروز صفحه‌بندی کار نمی‌کند. ?limit= فقط روی GET /v1/exports خوانده می‌شود. ?cursor= هیچ‌جا خوانده نمی‌شود. هیچ پاسخی اصلا کلید next_cursor ندارد، پس کلاینتی که آن را بخواند به‌جای رشته تهی هیچ‌چیز می‌گیرد، و has_more همیشه false است، حتی وقتی سطر بیشتری هست. GET /v1/segments و GET /v1/campaigns اصلا از این پاکت استفاده نمی‌کنند، و «بدون صفحه‌بندی» هم نیستند: هرکدام در خود SQL روی ORDER BY updated_at DESC LIMIT 200 سقف خورده‌اند. این سقف بی‌صداست. نه تعدادی هست، نه has_more، نه هشداری، پس حسابی که ۲۵۰ سگمنت دارد ۲۰۰ تای تازه‌تر را می‌گیرد و روی هیچ سطحی مسیری ندارد که به آن ۵۰ تای دیگر برسد.

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


#قفل نرم

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

دو محرک دارد:

مقدار reasonچه وقتیعنی
overdue_75یک فاکتور صادرشده، ۷۵ روز کامل یا بیشتر از سررسیدش گذشتهیک نفر باید فاکتور را بپردازد
usage_300مصرف به سه برابر پروفایل یا رویداد گنجانده‌شده در پلن رسیده، هرکدام بزرگ‌تر باشدپلن باید ارتقا پیدا کند

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

روی این میزبان دقیقا چهار مسیر پشت قفل‌اند: GET /v1/exports، POST /v1/exports، POST /v1/reports/funnel و POST /v1/reports/retention.

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

حساب قفل‌شده این را می‌بیند:

JSON
{
  "error": {
    "code": "account_locked",
    "message": "پرداخت این حساب ۷۵ روز از سررسید گذشته است",
    "details": { "reason": "overdue_75" }
  }
}

وضعیت 403 است، نه 402. «Payment Required» در هیچ کلاینتی معنای توافق‌شده ندارد، و نیمی از این رد اصلا درباره پرداخت نیست؛ سه‌برابر سهمیه یک بدهی نیست.

سه چیز که یک یکپارچه‌سازی باید بداند:

  • account_locked یعنی «گزارش و خروجی بسته است»، نه «حساب خاموش است». POST /v1/events و POST /v1/messages و ارسال کمپین همچنان کار می‌کنند. با این کد سراغ خاموش‌کردن کل اتصال نروید.
  • روی details.reason شاخه بزنید، نه روی متن. متن از کاتالوگ می‌آید و فارسی است.
  • حکم قفل تا یک دقیقه cache می‌شود. مشتری‌ای که همین حالا فاکتورش را پرداخت کرده، ممکن است تا یک دقیقه هنوز رد شود.

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


#GET /v1/status

بدون احراز هویت، بدون هزینه، بدون تماس با دیتابیس. تنها مسیری که وقتی همه‌چیز دیگر 503 می‌دهد هنوز جواب می‌دهد.

Shell
curl -i https://api.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":"api","version":"1.4.2"}

X-Server-Time را با ساعت خودتان مقایسه کنید. اگر ساعت شما بیش از یک ساعت جلوتر باشد، هر timestamp که روی POST /v1/events بفرستید بی‌صدا به زمان دریافت منتقل می‌شود. رد نمی‌شود و هشدارش هم روی آن مسیر دور ریخته می‌شود، پس تنها راه دیدنش همین مقایسه است.


#شمای داده

دو مسیر که می‌گویند این حساب واقعا چه فرستاده است. هر دو event.read می‌خواهند و هزینه ۵ دارند. هیچ‌کدام پارامتر نمی‌گیرند.

#GET /v1/schema/events

Shell
curl -s https://api.segmentic.net/v1/schema/events \
  -H "Authorization: Bearer sk_seg_..."
JSON
{
  "events": [
    {
      "name": "order_completed",
      "volume": 184203,
      "prop_keys": ["revenue", "order_id", "currency"],
      "last_seen": "2026-08-06"
    }
  ]
}

events همیشه آرایه است و هیچ‌وقت null نمی‌شود. prop_keys و last_seen وقتی خالی باشند حذف می‌شوند.

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

شکست: 503 با بدنه {"error":"schema unavailable"}. این پاکت پنل است، نه پاکت این API. رجوع کنید به پاکت خطا.

#GET /v1/schema/traits

Shell
curl -s https://api.segmentic.net/v1/schema/traits \
  -H "Authorization: Bearer sk_seg_..."
JSON
{
  "traits": ["city", "email", "lifetime_value"],
  "schema": [
    { "name": "city", "kind": "string", "users": 812043 },
    { "name": "lifetime_value", "kind": "number", "users": 61233 }
  ]
}

traits عمدا آرایه ساده نام‌ها مانده است. این یک مسیر عمومی است و چیزی بیرون آن را به‌عنوان رشته پیمایش می‌کند؛ عوض‌کردن شکل عنصر، آن یکپارچه‌سازی را روی یک ارتقا و بدون هیچ خطایی جایی، می‌شکند. جواب کامل‌تر یک کلید دوم کنارش است.

kind یا string است یا number. ویژگی‌ای که هر دو شکل فرستاده شده، یک‌بار و به‌صورت number می‌آید: ستون عددی همان است که بازه را پشتیبانی می‌کند و ستون رشته‌ای هنوز به تساوی جواب می‌دهد.

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

traits و schema می‌گویند رویدادهای خود شما چه چیزی حمل می‌کنند. کنارشان، builtin می‌گوید هر حسابی بدون فرستادن هیچ‌چیز روی چه چیزهایی می‌تواند فیلتر بگذارد، و engagement می‌گوید یک شرط تعامل با چه چیزهایی مقایسه می‌شود:

JSON
{
  "builtin": [
    {
      "name": "has_push",
      "kind": "boolean",
      "operators": ["eq", "neq"],
      "label": "امکان پوش",
      "computed": true,
      "description": "اینکه این آدم اصلا می‌تواند پوش بگیرد یا نه. مخاطب پوشی که این شرط را نداشته باشد، بیشترش آدم‌هایی‌اند که هیچ‌وقت آن را نمی‌بینند."
    },
    {
      "name": "birthday",
      "kind": "date",
      "operators": ["is_set", "is_not_set"],
      "label": "تاریخ تولد",
      "computed": false,
      "description": "فقط وجود داشتن را جواب می‌دهد. برای خود سالگرد از days_until_birthday استفاده کنید.",
      "use": "days_until_birthday"
    }
  ],
  "engagement": {
    "metrics": [ { "name": "open_rate", "kind": "number", "operators": ["gt", "gte", "lt", "lte", "between"], "label": "نرخ باز کردن", "computed": true } ],
    "bands": ["champion", "dormant"]
  }
}

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

ویژگیبه چه چیزی جواب می‌دهد
has_pushاینکه این آدم اصلا می‌تواند پوش بگیرد. مخاطبی که این را نداشته باشد بیشترش آدم‌هایی‌اند که پیام را نمی‌بینند
days_until_birthdayچند روز تا تولد بعدی، امروز صفر. birthday ذخیره‌شده یک تاریخ در گذشته است و بعد از سال اول هیچ‌کس را نمی‌گیرد، برای همین birthday فقط وجود داشتن را جواب می‌دهد و در use همین را نام می‌برد
days_since_last_seenپنجره غیرفعالی که با تقویم جلو می‌رود، به جای زمانی که روز نوشتن مخاطب منجمد شده

operators همان مجموعه‌ای است که روی آن ویژگی کامپایل می‌شود، پس کسی که از داخل آن انتخاب کند نمی‌تواند شرطی بنویسد که سرور ردش کند. توجه کنید که ویژگی عددی in و not_in ندارد: مقدار فهرستی، فهرستی از رشته‌هاست.

computed برای ویژگی‌ای درست است که پلتفرم خودش حسابش می‌کند. label و description زبان درخواست را دنبال می‌کنند، پس برای خواندنشان به انگلیسی Accept-Language: en بفرستید. description فقط جایی هست که خود نام همه ماجرا را نمی‌گوید.

شکست: 503 با بدنه {"error":"schema unavailable"}.


#GET /v1/ingest/quality

چه چیزی را از شما رد کردیم و چه چیزی را اصلاح کردیم، به تفکیک روز.

Shell
curl -s "https://api.segmentic.net/v1/ingest/quality?days=7"   -H "Authorization: Bearer sk_seg_..."
JSON
{
  "days": 7,
  "from": "2026-08-16",
  "to": "2026-08-23",
  "totals": { "rejected": 126867, "warned": 4102 },
  "rows": [
    {
      "day": "2026-08-22",
      "kind": "reject",
      "code": "missing_identity",
      "sdk": "segmentic-android",
      "app_id": 3,
      "count": 126867,
      "label": "نه user_id داشت نه anonymous_id، پس معلوم نیست رویداد چه کسی است"
    },
    {
      "day": "2026-08-22",
      "kind": "warn",
      "code": "generated_message_id",
      "field": "message_id",
      "sdk": "segmentic-js",
      "app_id": 1,
      "count": 4102,
      "label": "message_id فرستاده نشده بود، پس اگر همین رویداد دوباره بیاید تکراری شناخته نمی‌شود"
    }
  ]
}

kind یا reject است یا warn، و تفاوتشان مهم است: رد شدن یعنی رویداد از دست رفت، و هشدار یعنی نگهش داشتیم و چیزی را در آن عوض کردیم. days پیش‌فرض ۷ است و سقفش ۹۰ روز است، یعنی همان مدتی که این جدول سطر نگه می‌دارد.

label زبان درخواست را دنبال می‌کند، پس برای خواندنش به انگلیسی Accept-Language: en بفرستید. code این کار را نمی‌کند: آن نیمه ثابت ماجراست و یکپارچه‌سازی باید روی آن شرط بگذارد نه روی جمله.

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

شکست: 503 با بدنه {"error":"schema unavailable"}.

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


#مخاطب بدون ذخیره

دو مسیر که روی یک فیلتر کار می‌کنند بدون اینکه چیزی ذخیره کنند. شیء ذخیره‌شده «سگمنت» است؛ این‌ها عملیات لحظه‌ای روی یک تعریف‌اند.

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

JSON
{
  "definition": {
    "version": 1,
    "root": {
      "kind": "group",
      "op": "and",
      "children": [
        {
          "kind": "trait",
          "trait": "city",
          "operator": "eq",
          "value": { "type": "string", "str": "تهران" }
        }
      ]
    }
  }
}

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

#POST /v1/audiences/validate

مجوز segment.read، هزینه ۱. دیتابیس را اصلا لمس نمی‌کند.

Shell
curl -s -X POST https://api.segmentic.net/v1/audiences/validate \
  -H "Authorization: Bearer sk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{"definition":{"version":1,"root":{"kind":"trait","trait":"city","operator":"eq","value":{"type":"string","str":"تهران"}}}}'
JSON
{
  "valid": true,
  "description_fa": "کاربرانی که شهرشان تهران است"
}

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

فیلتر نامعتبر 422 است:

JSON
{
  "error": {
    "code": "filter_invalid",
    "message": "segment: unsupported operator: \"nonsense\""
  }
}

پنل برای همین فیلتر 200 با valid:false جواب می‌دهد، که برای فرمی که کاربر در آن تایپ می‌کند درست است و برای یکپارچه‌سازی‌ای که مدیریت خطایش روی status شاخه می‌زند غلط. اینجا 422 است.

JSON بدشکل اینجا 400 با بدنه {"error":"malformed JSON"} می‌گیرد، یعنی پاکت پنل نه پاکت این API.

#POST /v1/audiences/count

مجوز segment.read، هزینه ۲۵. شمارش دقیق است، نه نمونه‌گیری‌شده.

Shell
curl -s -X POST https://api.segmentic.net/v1/audiences/count \
  -H "Authorization: Bearer sk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{"definition":{"version":1,"root":{"kind":"trait","trait":"city","operator":"eq","value":{"type":"string","str":"تهران"}}}}'
JSON
{
  "count": 61432,
  "approximate": false,
  "description": "کاربرانی که شهرشان تهران است",
  "took_ms": 812
}

approximate روی این مسیر همیشه false است و sample_rate هیچ‌وقت ست نمی‌شود، پس در پاسخ نمی‌آید.

نام کلید جمله فارسی اینجا description است، ولی روی POST /v1/audiences/validate و روی نوشتن سگمنت description_fa است. دو نام برای یک چیز. این یک ناسازگاری واقعی است و اگر یک تابع مشترک بنویسید که هر دو پاسخ را می‌خواند، باید هر دو کلید را بگردد.

خطاها همه در پاکت پنل‌اند: فیلتری که کامپایل نمی‌شود 400 با {"error":"segment: ..."} می‌گیرد (یعنی همان فیلتری که validate برایش 422 می‌داد)، و شکست انبار داده 503 با {"error":"count unavailable"}.


#سگمنت

سگمنت شیء ذخیره‌شده است. پنج مسیر دارد و همه فقط وقتی ثبت می‌شوند که features.segments روشن باشد.

#GET /v1/segments

مجوز segment.read، هزینه ۱.

Shell
curl -s https://api.segmentic.net/v1/segments \
  -H "Authorization: Bearer sk_seg_..."
JSON
{
  "segments": [
    {
      "id": 12,
      "name": "تهرانی‌ها",
      "kind": "dynamic",
      "definition": { "version": 1, "root": { "kind": "trait", "trait": "city", "operator": "eq", "value": { "type": "string", "str": "تهران" } } },
      "description_fa": "کاربرانی که شهرشان تهران است",
      "last_size": 61432,
      "last_computed_at": "2026-08-06T09:00:00Z",
      "updated_at": "2026-08-06T09:00:00Z"
    }
  ]
}

kind یکی از dynamic، static یا realtime است.

last_size و last_computed_at روی سیم هستند و پر نمی‌شوند. قرار بود اندازه مخاطب را از آخرین باری که چیزی شمرده نگه دارند، تا صفحه فهرست برای باز شدن دویست کوئری روی انبار داده نزند. انبار تابعی دارد که این دو را می‌نویسد و هیچ‌جای محصول منتشرشده صدایش نمی‌زند، پس last_size روی هر سگمنتی 0 است و last_computed_at روی هر سگمنتی نمی‌آید. نمونه بالا شکل را نشان می‌دهد، نه آنچه دریافت می‌کنید. برای عدد واقعی، POST /v1/audiences/count را با تعریف همان سگمنت صدا بزنید و ۲۵ واحد بپردازید.

segments همیشه آرایه است. ?limit= و ?cursor= روی این مسیر بی‌صدا نادیده گرفته می‌شوند، و فهرست روی ۲۰۰ سگمنت تازه‌تر سقف می‌خورد بدون اینکه چیزی در پاسخ این را بگوید. شکست: 503 با {"error":"segments unavailable"}.

#GET /v1/segments/{id}

مجوز segment.read، هزینه ۱. پاسخ یک شیء برهنه با همان شکل بالاست، بدون پوشش.

شناسه غیرعددی یا صفر 400 با {"error":"invalid segment id"} می‌گیرد. شناسه ناشناس یا مال حساب دیگر 404 با {"error":"segment not found"} می‌گیرد. شمول در store روی حساب است، پس شناسه حدس‌زده‌شده از شناسه حذف‌شده قابل تشخیص نیست.

#POST /v1/segments

مجوز segment.write، هزینه ۱. سقف بدنه ۸ مگابایت.

Shell
curl -s -X POST https://api.segmentic.net/v1/segments \
  -H "Authorization: Bearer sk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "تهرانی‌ها",
    "definition": {
      "version": 1,
      "root": {
        "kind": "trait",
        "trait": "city",
        "operator": "eq",
        "value": { "type": "string", "str": "تهران" }
      }
    }
  }'
JSON
{
  "id": 12,
  "name": "تهرانی‌ها",
  "description_fa": "کاربرانی که شهرشان تهران است"
}

وضعیت 201. دو فیلد بیشتر ندارد: name که پس از trim نباید خالی باشد، و definition که باید از اعتبارسنجی رد شود.

وضعیتکدچه وقت
400malformed_jsonبدنه JSON معتبر نیست
400name_requiredنام خالی است. پیام: a segment needs a name
422filter_invalidتعریف از اعتبارسنجی رد نشد
503segment_unavailablestore نتوانست ذخیره کند

فیلد kind در این struct وجود ندارد. هر سگمنتی که از این API ساخته شود dynamic است. ساختن فهرست ثابت یا realtime از این API ممکن نیست.

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

فیلدهای ناشناس در بدنه پذیرفته و نادیده گرفته می‌شوند. تنها مسیری روی این میزبان که فیلد ناشناس را رد می‌کند POST /v1/messages است. یعنی روی بقیه، غلط املایی در نام یک فیلد نامرئی است.

#PUT /v1/segments/{id}

مجوز segment.write، هزینه ۱. همان بدنه ساخت.

این جایگزینی کامل شیء است، نه ادغام. PATCH وجود ندارد و هیچ If-Match یا نشانه نسخه‌ای در کار نیست، پس دو نویسنده هم‌زمان بی‌صدا روی هم می‌نویسند.

سه رفتار که باید بدانید:

  1. اعتبارسنجی تعریف پیش از بررسی وجود سگمنت اجرا می‌شود. فیلتر نامعتبر روی شناسه‌ای که وجود ندارد هم 422 می‌گیرد.
  2. سگمنت اول خوانده می‌شود. شناسه ناشناس یا مال حساب دیگر 404 با کد not_found می‌گیرد، نه یک نوشتن که بی‌صدا سگمنت تازه بسازد.
  3. name خالی یا حذف‌شده، نام قبلی را نگه می‌دارد. پاکش نمی‌کند و خطا هم نمی‌دهد.

پاسخ 200 با همان سه کلید ساخت. شناسه غیرمثبت 400 با کد bad_id و پیام the path must carry a positive integer id می‌گیرد.

#DELETE /v1/segments/{id}

مجوز segment.delete، هزینه ۱. مجوز خودش را دارد چون برداشتن مخاطبی که سناریوی یک نفر به آن ارجاع می‌دهد، همان کار ویرایش‌کردن یکی نیست.

Shell
curl -s -i -X DELETE https://api.segmentic.net/v1/segments/12 \
  -H "Authorization: Bearer sk_seg_..."

پاسخ 204 بدون بدنه. برای شناسه ناشناس 404 وجود ندارد: حذف کورکورانه صدا زده می‌شود و یک no-op موفق هم 204 می‌گیرد. رد segment_in_use هم وجود ندارد؛ حذف مخاطبی که یک کمپین زمان‌بندی‌شده به آن اشاره می‌کند، یک تماس معمولی است.

نقشی که این مجوز را ندارد 403 با need: "segment.delete" می‌گیرد. شکست store، 503 با کد segment_unavailable.


#کمپین

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

#GET /v1/campaigns

مجوز campaign.read، هزینه ۱. صفحه‌بندی نمی‌شود، و روی ۲۰۰ کمپین تازه‌تر سقف می‌خورد بدون اینکه چیزی در پاسخ این را بگوید.

JSON
{
  "campaigns": [
    {
      "id": 5,
      "name": "پوش نوروز",
      "channel": "push",
      "status": "draft",
      "estimated": 61432,
      "processed": 0,
      "sent": 0,
      "scheduled_at": "2026-03-20T06:00:00Z",
      "updated_at": "2026-08-06T09:00:00Z"
    }
  ]
}

وضعیت‌های ممکن: draft، scheduled، running، paused، completed، cancelled، failed. شکست: 503 با {"error":"campaigns unavailable"}.

#GET /v1/campaigns/{id}

مجوز campaign.read، هزینه ۵. این گزارش کمپین است، نه فقط رکورد آن.

JSON
{
  "campaign": {
    "id": 5,
    "tenant_id": 7,
    "name": "پوش نوروز",
    "channel": "push",
    "template_id": 3,
    "segment_id": 12,
    "status": "completed",
    "goal_event": "order_completed"
  },
  "progress": {
    "campaign_id": 5,
    "cursor": "u-98213",
    "estimated": 61432,
    "processed": 61432,
    "sent": 58210,
    "suppressed": 1802,
    "deferred": 0,
    "failed": 1420,
    "holdout": 0,
    "started_at": "2026-03-20T06:00:00Z",
    "updated_at": "2026-03-20T06:41:00Z",
    "finished_at": "2026-03-20T06:41:00Z"
  },
  "percent": 100,
  "reach": [
    { "status": "suppressed", "reason": "no_address", "reason_fa": "نشانی ندارد", "count": 1802 }
  ],
  "delivery": [
    { "delivery": "delivered", "delivery_fa": "تحویل شد", "count": 55012 }
  ],
  "engagement": [
    {
      "channel": "push",
      "channel_fa": "اعلان",
      "issued": 58210,
      "withheld": 0,
      "measurable_open": 58210,
      "measurable_click": 58210,
      "opened": 19204,
      "clicked": 4102,
      "opened_unmeasurable": 0,
      "clicked_unmeasurable": 0
    }
  ],
  "engagement_rejects": [],
  "uplift": {
    "verdict": "too_early",
    "verdict_fa": "در حال جمع‌آوری نتیجه",
    "goal": "order_completed",
    "treated_users": 0,
    "treated_conversions": 0,
    "control_users": 0,
    "control_conversions": 0,
    "contaminated": 0,
    "lift": 0,
    "lift_low": 0,
    "lift_high": 0,
    "extra_low": 0,
    "extra": 0,
    "extra_high": 0,
    "median_order": 0,
    "currency": "",
    "extra_revenue": 0,
    "extra_revenue_low": 0,
    "extra_revenue_high": 0,
    "money_known": false,
    "needed_per_arm": 0,
    "window_closed_at": "2026-03-27T06:41:00Z",
    "computed_at": "0001-01-01T00:00:00Z"
  }
}

سه چیز که این پاسخ را از یک progress bar جدا می‌کند:

  • reach جواب پرسشی است که چنین سکویی مدام از آن پرسیده می‌شود و معمولا نمی‌تواند جواب بدهد: سگمنت شصت‌هزار گفت، چرا چهل‌ویک‌هزار نفر گرفتند.
  • هر نرخ در engagement به‌صورت صورت و مخرج نام‌دار می‌آید، نه درصد. کمپینی که پیامش لینک نداشته، کمپین با نرخ کلیک صفر نیست، و measurable_click همان چیزی است که این را می‌گوید. یک درصد ترکیبی واحد، عددی است که مشتری نمی‌تواند بازتولیدش کند.
  • percent روی اجرای تمام‌شده همیشه صد است و از صد بالاتر نمی‌رود. تخمین نمونه‌گیری‌شده است، پس یک اجرا می‌تواند از آن رد شود، و نمایش ۱۱۸ درصد مثل یک باگ خوانده می‌شود.

uplift به‌محض تمام‌شدن کمپین می‌آید، نه پس از بسته‌شدن پنجره انتساب. تا وقتی اندازه‌گیری ذخیره نشده باشد این بخش ساخته می‌شود، دقیقا همان‌طور که نمونه بالا نشان می‌دهد: verdict برابر too_early است، verdict_fa و goal پر می‌شوند، window_closed_at برابر finished_at به‌علاوه هفت روز است، computed_at به شکل 0001-01-01T00:00:00Z روی سیم می‌آید، و هر فیلد عددی صفر است. برای گرفتن جواب روی window_closed_at حساب نکنید. این تاریخ زودترین زمان ممکن آمدن جواب است، نه زمان آمدنش: اندازه‌گیری هفت روز از آخرین پیامی که واقعا بیرون رفته صبر می‌کند، و کمپین با ساعت محلی تا یک روز و نیم بعد از تمام‌شدن اجرا هنوز پیام می‌فرستد، پس بخش too_early می‌تواند بعد از گذشتن آن تاریخ هم سرو شود. فقط positive و negative و inconclusive اثر افزودهٔ اندازه‌گیری‌شده دارند. no_control و contaminated محاسبه را پیش از کم‌کردن دو نرخ متوقف می‌کنند، پس lift و lift_low و lift_high و همه فیلدهای extra روی آن سطرها هم صفر می‌آیند؛ صفر آنجا یعنی چیزی برای مقایسه نیست، نه اینکه اثر صفر بوده. ولی شمارش‌های کنارش روی هر حکمی واقعی‌اند: شمار تیمارشده و کنترل پیش از متوقف‌شدن محاسبه نوشته می‌شوند، و روی سطر contaminated خود شمار آلوده دلیل وجود همان حکم است. نصبی که ورکر کمپین را اجرا نمی‌کند هیچ‌وقت اندازه‌گیری ذخیره نمی‌کند، پس همیشه همین بخش ساختگی را سرو می‌کند. reach، delivery، engagement و uplift همه best-effort اند: یک اختلال در انبار داده هزینه‌اش آن بخش است، نه کل صفحه.

توجه کنید که tenant_id اینجا روی سیم هست، برخلاف شیء سگمنت.

خطاها: 400 با {"error":"invalid campaign id"} و 404 با {"error":"campaign not found"}، هر دو در پاکت پنل.

#POST /v1/campaigns

مجوز campaign.write، هزینه ۱.

Shell
curl -s -X POST https://api.segmentic.net/v1/campaigns \
  -H "Authorization: Bearer sk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "پوش نوروز",
    "channel": "push",
    "template_id": 3,
    "segment_id": 12,
    "scheduled_at": "2026-03-20T06:00:00Z",
    "control_group_pct": 5,
    "goal_event": "order_completed"
  }'
JSON
{ "id": 5, "status": "draft" }

وضعیت 201.

فیلدنوعلازمقاعده
nameرشتهخیراصلا اعتبارسنجی نمی‌شود. نام خالی پذیرفته می‌شود
channelرشتهعملا بلهباید از فهرست پایین باشد. رشته خالی یعنی «الگو تصمیم می‌گیرد»
template_idعددبلهصفر یعنی campaign: a template is required
channelsآرایهخیرکل زنجیره به ترتیب، هر کدام {channel, template_id}، و ورودی اولش باید خود channel باشد. پایین‌تر توضیح داده شده
segment_idعددخیرصفر یعنی مخاطب همان definition درون‌خطی است
definitionشیءخیراینجا اعتبارسنجی نمی‌شود، برخلاف سگمنت
topic_idعددخیرصفر یعنی بدون موضوع اشتراک
scheduled_atRFC3339خیرمقدار غیرقابل‌خواندن بی‌صدا حذف می‌شود، خطا نمی‌دهد
use_local_timeبولینخیرپیش‌فرض false
local_hourعددخیرفقط وقتی use_local_time روشن باشد باید بین ۰ و ۲۳ باشد
throttle_minutesعددخیراعتبارسنجی ندارد
control_group_pctعدد اعشاریخیرباید بین ۰ و ۱۰۰ باشد
audience_pctعدد اعشاریخیربرش پایلوت. باید بین ۰ و ۱۰۰ باشد، و ۰ یعنی همه، نه هیچ‌کس
goal_eventرشتهخیرخالی یعنی order_completed

channels کل زنجیره است، نه جایگزین‌های بعد از کانال اول. اگر بدهیدش، ورودی اولش باید دقیقا همان چیزی باشد که در channel نوشته‌اید، وگرنه 422 می‌گیرید. دلیل این سخت‌گیری یک اشتباه بی‌صداست: وقتی channels پر باشد، مسیر ارسال فقط همان را می‌خواند و channel را اصلا نگاه نمی‌کند، پس channel: "sms" به‌همراه channels: [push, inapp] کمپینی می‌سازد که پوش و درون‌برنامه می‌فرستد و هیچ‌وقت پیامک نمی‌فرستد، و هیچ خطایی هم جایی ثبت نمی‌شود.

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

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

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

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

این گروه کنترل با عددهای برعکس نیست. holdout گروهی است که از آن دریغ می‌شود تا اثر کمپین در برابرش سنجیده شود؛ پایلوت گروهی است که دریافت می‌کند. این دو مستقل از هم قرعه می‌خورند، پس یک کمپین می‌تواند هر دو را داشته باشد.

کسانی که بیرون پایلوت می‌مانند در شمارنده‌ی خودشان، progress.outside_pilot، گزارش می‌شوند و به suppressed اضافه نمی‌شوند: پایلوتی که دقیقا همان کاری را کرده که به آن گفته شده، نباید مثل کمپینی خوانده شود که governance جلویش را گرفته است.

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

کانال‌های پذیرفته‌شده: push، webpush، sms، email، inapp، messenger، webhook. مستعارهای web، p، s، e، w و i هم resolve می‌شوند. سه پیام‌رسان bale، eitaa و rubika پذیرفته و به messenger تا می‌شوند، چون بازاریاب نمی‌تواند بداند هرکدام از دو میلیون نفر کدام اپ را نصب کرده. هر چیز دیگری رد می‌شود، نه اینکه به پیش‌فرض بیفتد: کانال اشتباهی که موفقیت گزارش می‌کند بدتر از یک 400 است که نام فیلد را می‌گوید.

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

وضعیت هرچه بفرستید draft می‌شود. کلید status در بدنه اصلا خوانده نمی‌شود.

وضعیتکدچه وقت
400malformed_jsonبدنه JSON معتبر نیست
400invalid_channelپیام unknown channel "bogus"، و details برابر {"field":"channel"}
422campaign_invalidمتن خطای اعتبارسنجی کمپین
503campaign_unavailablestore نتوانست ذخیره کند

#PUT /v1/campaigns/{id}/recurrence

مجوز campaign.send، هزینه ۱. این مسیر برنامه تکرار خودکار یک کمپین ذخیره‌شده را شروع یا جایگزین می‌کند. هر نوبت، کمپین تازه‌ای با همان مخاطب و محتوا می‌سازد.

همه فیلدهای تقویمی با ساعت تهران خوانده می‌شوند. روز ماه، روز تقویم جلالی است. در برنامه هفتگی، شنبه 0 و جمعه 6 است.

Shell
curl -s -X PUT https://api.segmentic.net/v1/campaigns/5/recurrence \
  -H "Authorization: Bearer sk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "recurrence": {
      "cadence": "weekly",
      "hour": 9,
      "day_of_week": 0,
      "max_occurrences": 4
    }
  }'

مقدار cadence یکی از daily، weekly، monthly یا yearly است. یا hour را بدهید، یا hours را با حداکثر شش مقدار از 0 تا 23. برنامه هفتگی از day_of_week استفاده می‌کند. day_of_month بین 1 تا 31 را هم برنامه ماهانه و هم سالانه می‌خواند، و month بین 1 برای فروردین تا 12 برای اسفند فقط برای سالانه لازم است و بقیه نادیده‌اش می‌گیرند. مقدار اختیاری ends_at با قالب RFC3339 یا max_occurrences مثبت، مجموعه را متوقف می‌کند. بدون این دو، برنامه تا زمان پاک شدن ادامه دارد.

برنامه سالانه همان چیزی است که برای یک تاریخ تقویمی می‌خواهید: روز یک صنف، نوروز، سالگرد باز شدن یک حساب. همه‌چیز اینجا جلالی و به وقت تهران است، پس month: 12, day_of_month: 5 یعنی ۵ اسفند هر سال. روزی که از انتهای یک ماه کوتاه بگذرد روی آخرین روز همان ماه می‌نشیند نه اول ماه بعد، پس day_of_month: 30 در اسفند سال عادی روی ۲۹ اسفند می‌رود: منظور کسی که آن را تایپ کرده «آخر سال» بوده، و نوروز تنها روزی است که پیام آخر سال نباید در آن برسد.

درخواست نمی‌تواند occurrences را تعیین کند. این شمارنده صفر می‌شود و خود کارگر آن را نگه می‌دارد.

JSON
{ "status": "ok" }
وضعیتکدچه وقت
400bad_id یا malformed_jsonشناسه یا بدنه خوانده نمی‌شود
422recurrence_invalidتناوب، ساعت، روز هفته یا روز ماه نامعتبر است
503recurrence_unavailableبرنامه ذخیره نشد

#DELETE /v1/campaigns/{id}/recurrence

مجوز campaign.write، هزینه ۱، نه campaign.send. این مسیر تکرارهای خودکار آینده را متوقف می‌کند. کمپین‌هایی که برنامه قبلا ساخته، تغییر نمی‌کنند و پاک نمی‌شوند.

شروع کردن یک برنامه campaign.send می‌خواهد، چون هر نوبتش کمپین تازه‌ای است که می‌تواند به کل مخاطب برسد. متوقف کردنش فقط از حجم ارسال کم می‌کند، پس بیش از مجوز ویرایش کمپین نمی‌خواهد، و این همان تفکیکی است که پنل برای pause و cancel قائل است. قبلا این مسیر هم campaign.send می‌خواست و هزینه‌اش دقیقا برعکس می‌افتاد: کلیدی که عمدا بدون campaign.send ساخته شده باشد، یعنی همان کلیدی که کار روزمره باید با آن انجام شود، تنها کلیدی بود که نمی‌توانست برنامه‌ای را که هر روز کمپین می‌ساخت متوقف کند.

Shell
curl -s -X DELETE https://api.segmentic.net/v1/campaigns/5/recurrence \
  -H "Authorization: Bearer sk_seg_..."
JSON
{
  "status": "ok",
  "note": "campaigns already created by this schedule are unchanged"
}

خطاها 400 bad_id و 503 recurrence_unavailable هستند.

#POST /v1/campaigns/{id}/send

مجوز campaign.send، هزینه ۱. بدنه‌ای خوانده نمی‌شود. این تماس برگشت‌ناپذیر است.

Shell
curl -s -X POST https://api.segmentic.net/v1/campaigns/5/send \
  -H "Authorization: Bearer sk_seg_..."
JSON
{ "id": 5, "status": "scheduled" }

وضعیت 202.

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

وضعیتکدچه وقت
400bad_idشناسه غیرمثبت در مسیر
409approval_requiredحساب تایید می‌خواهد و تاییدی نیست، یا رد شده، یا هنوز تایید نشده
409approval_staleکمپین بعد از تایید عوض شده. دوباره برای تایید بفرستید
404not_foundکمپین خوانده نشد
503approval_unavailableخواندن قاعده یا وضعیت تایید شکست خورد. بسته شکست می‌خورد
503campaign_unavailableزمان‌بندی شکست خورد

approval_stale کد جداگانه دارد چون این دو، یکپارچه‌سازی را به دو جای متفاوت می‌فرستند: یکی به «تایید بگیر»، دیگری به «یک نفر بعد از تایید این را ویرایش کرده». اثر انگشت روی شناسه سگمنت، تعریف درون‌خطی، الگو، کانال، موضوع، رویداد هدف، درصد گروه کنترل، زمان‌بندی و فهرست مرتب‌شده واریانت‌ها گرفته می‌شود و ۳۲ نویسه شانزده‌شانزدهی است.

هیچ راهی برای مکث، ادامه یا لغو کمپین روی این میزبان وجود ندارد. هر سه روی پنل هستند و روی این mux ثبت نشده‌اند. وقتی بک‌اند شما یک ارسال را زمان‌بندی کرد، فقط پنل می‌تواند جلویش را بگیرد.

#POST /v1/campaigns/{id}/submit

مجوز campaign.write، نه campaign.approve: نویسنده دارد درخواست می‌کند، نه تصمیم می‌گیرد. هزینه ۱. فقط وقتی features.campaign_approval روشن باشد ثبت می‌شود. بدنه‌ای خوانده نمی‌شود.

JSON
{
  "approval_id": 88,
  "state": "pending",
  "fingerprint": "3f1a9c02b7de4415aa0e8c1d2f6b3790"
}

وضعیت 202. مقادیر state: pending، approved، rejected، stale.

وضعیتکدچه وقت
400bad_idشناسه غیرمثبت
404not_foundکمپین وجود ندارد
409not_submittableوضعیت کمپین draft یا paused نیست
422campaign_invalidکمپین از اعتبارسنجی رد نشد
503approval_unavailableثبت درخواست شکست خورد

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

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


#POST /v1/events

مجوز profile.write، هزینه ۵، سقف بدنه ۸ مگابایت. فقط وقتی features.ingest روشن باشد ثبت می‌شود.

profile.write انتخاب شده و نه یک مجوز تازه، چون این کاری است که این مسیر می‌کند: روی پرونده آدم‌ها و تاریخچه رویدادشان می‌نویسد، و ساختن نام دوم برای همان توانایی باعث می‌شد کسی یکی را بدهد به این خیال که دیگری را نگه داشته است. نقش‌هایی که دارندش: owner، admin، marketer.

Shell
curl -s -X POST https://api.segmentic.net/v1/events \
  -H "Authorization: Bearer sk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "type": "track",
        "event": "order_completed",
        "user_id": "u-1",
        "message_id": "srv-order-8821",
        "timestamp": "2026-08-06T09:12:41Z",
        "properties": { "revenue": 480000, "order_id": "8821", "currency": "IRR" }
      }
    ]
  }'
JSON
{ "accepted": 1 }

وضعیت 202، نه 200. رویدادها در صف‌اند، نه ذخیره‌شده: در همان خط لوله‌ای هستند که رویدادهای SDK از آن می‌گذرند و چند ثانیه بعد قابل کوئری می‌شوند. گفتن 200 کسی را دعوت می‌کرد که بلافاصله آن‌ها را بخواند و نتیجه بگیرد گم شده‌اند.

اگر بخشی از batch رد شود:

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

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

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

ترتیب ردها:

وضعیتکدچه وقت
400malformed_jsonبدنه JSON معتبر نیست
400batch_emptyآرایه events خالی است
413batch_too_largeبیش از ۵۰۰ رویداد. details برابر {"limit":500,"sent":501}
402quota_cancelled یا quota_trial_over یا quota_event_capسقف تجاری. کل batch رد می‌شود
422all_events_rejectedهیچ آیتمی قابل قبول نبود. details همان آرایه ردهاست
503ingest_unavailableصف در دسترس نبود. پیام: could not queue these events; retry

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

#هشت تفاوت با میزبان ورود داده

این مسیر و POST /v1/batch روی in.segmentic.net یک کار نمی‌کنند. اگر تاریخچه مهاجرت می‌دهید، سطر آخر را حتما بخوانید.

POST /v1/batch روی میزبان ورودPOST /v1/events اینجا
کلیدwk_seg_... عمومیsk_seg_... محرمانه
کلید آرایه در بدنهbatchevents
وضعیت موفق200202
rejectedیک عدد، آرایه زیر errorsخود آرایه
هشدارهابرمی‌گردند، حداکثر ۵۰کامل دور ریخته می‌شوند
حذف تکراری با message_idبلهخیر. batch تکراری اینجا دوبار شمرده می‌شود
IP و User-Agentخوانده و برای موقعیت جغرافیایی استفاده می‌شودعمدا ست نمی‌شود. این تماس سرور به سرور است، پس نشانی مال دیتاسنتر مشتری است و نسبت‌دادن شهر گیرنده از رویش، همه کاربران او را در یک نقطه می‌گذارد
پنجره زمان گذشتهاز سیاست نگه‌داری همان حسابست نمی‌شود، پس پیش‌فرض ۳۰ روز اعمال می‌شود

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

#نکته درباره reason

کدهای پایدار رد این‌ها هستند: unknown_type، missing_identity، missing_event_name، event_name_too_long، event_name_invalid_chars، id_too_long، missing_previous_id، timestamp_too_old.

ولی مقدار reason در پاسخ، متن کامل خطاست و دو مورد با مقدار خود شما پیچیده می‌شوند. مثلا unknown_type: "not_a_type".

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


#قالب پیام

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

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

حذف ندارد و ویرایش با URL هم ندارد. حذف یک قالب، سناریوی در جریانی را که به آن اشاره می‌کند بی‌صدا می‌شکند، و پنل هم حذف ندارد. ویرایش همان POST است با id، دقیقا مثل پنل: یک در، و همان یک در.

#GET /v1/templates

مجوز template.read، هزینه ۱.

Shell
curl -s https://api.segmentic.net/v1/templates \
  -H "Authorization: Bearer sk_seg_..."
JSON
{
  "templates": [
    { "id": 42, "name": "خوش‌آمد پیامکی", "channel": "sms", "category": "marketing",
      "title": "", "body": "سلام {{name}}، خوش آمدید." }
  ]
}

#GET /v1/templates/{id}

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

JSON
{
  "id": 42,
  "channel": "sms",
  "category": "marketing",
  "title": "",
  "body": "سلام {{name}}، خوش آمدید.",
  "variables": ["name"],
  "pattern_code": "welcome_v2",
  "pattern_approved": true,
  "pattern_tokens": { "name": "1" }
}

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

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

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

#POST /v1/templates

مجوز template.write، هزینه ۱. سقف بدنه ۸ مگابایت.

Shell
curl -s -X POST https://api.segmentic.net/v1/templates \
  -H "Authorization: Bearer sk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "خوش‌آمد پیامکی",
    "channel": "sms",
    "category": "marketing",
    "body": "سلام {{name}}، خوش آمدید."
  }'
JSON
{ "id": 42, "name": "خوش‌آمد پیامکی", "channel": "sms" }

وضعیت 201 برای ساخت و 200 وقتی id بدهید و قالب موجود جایگزین شود. نام در هر حساب یکتاست.

وضعیتکدچه وقت
400name_requiredنام پس از trim خالی است
400content_requiredنه عنوان دارد و نه متن
400invalid_channelکانال شناخته نشد
503template_unavailableذخیره نشد

#POST /v1/templates/render

مجوز template.read، هزینه ۱. قالب را با مقدارهایی که می‌دهید پر می‌کند و نشان می‌دهد چه چیزی فرستاده می‌شد. چیزی فرستاده نمی‌شود و چیزی ذخیره نمی‌شود.

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

Shell
curl -s -X POST https://api.segmentic.net/v1/templates/render \
  -H "Authorization: Bearer sk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{ "id": 42, "vars": { "name": "سارا" } }'
JSON
{
  "title": "",
  "body": "سلام سارا، خوش آمدید.",
  "missing": [],
  "sendable": true,
  "variables": ["name"],
  "sms": { "encoding": "ucs2", "parts": 1, "remaining": 47 }
}

missing فهرست متغیرهایی است که مقدار نگرفتند، و sendable می‌گوید با همین مقدارها فرستادنی است یا نه. متغیر بی‌مقدار به رشته خالی تبدیل نمی‌شود: پیامی که «سلام ،» می‌رود اشتباه رفته و پیامی که رد شده نرفته.

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

#سناریو

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

ذخیره و انتشار دو کار جداست و دو مجوز دارد. ذخیره پیش‌نویس چیزی را برای کسی نمی‌فرستد. انتشار، گراف را از همان لحظه جلوی هر کسی می‌گذارد که ماشه‌اش را بزند، و برگشت ندارد: نمونه‌هایی که وارد شده‌اند وارد شده‌اند. برای همین journey.publish را نقش‌های کمتری دارند تا journey.write.

#GET /v1/journeys

مجوز journey.read، هزینه ۱. فهرست سناریوها با وضعیت و نسخه منتشرشده و شمار کسانی که همین حالا داخلشان هستند.

Shell
curl -s https://api.segmentic.net/v1/journeys \
  -H "Authorization: Bearer sk_seg_..."
JSON
{
  "journeys": [
    { "id": 4, "name": "خوش‌آمد", "status": "active", "version": 3, "active": 812, "waiting": 40 }
  ]
}

status یکی از draft و active و paused و archived است. active تعداد نمونه‌های در جریان است و waiting آن‌هایی که روی یک گره صبر پارک شده‌اند.

#GET /v1/journeys/{id}

مجوز journey.read، هزینه ۵. نسخه منتشرشده را برمی‌گرداند، به‌علاوه شمارنده هر گره.

JSON
{
  "graph": { "journey_id": 4, "version": 3, "entry_id": "n1", "nodes": [] },
  "stats": { "n1": { "entered": 900, "exited": 860, "suppressed": 12, "waiting": 28 } }
}

اگر سناریو هیچ نسخه منتشرشده‌ای نداشته باشد، 404 با کد not_found می‌گیرید. همان جواب را برای شناسه‌ای که وجود ندارد هم می‌گیرید، و این عمدی است: تفاوت آن دو یعنی گفتن اینکه کدام شناسه‌ها واقعی‌اند.

شمارنده‌ها بهترین‌کوشش‌اند. اگر انبار داده جواب ندهد، stats خالی برمی‌گردد و graph سر جایش است، چون گراف چیزی است که خواستید.

#POST /v1/journeys

مجوز journey.write، هزینه ۱. پیش‌نویس را ذخیره می‌کند. چیزی منتشر نمی‌شود.

Shell
curl -s -X POST https://api.segmentic.net/v1/journeys \
  -H "Authorization: Bearer sk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "خوش‌آمد",
    "graph": {
      "entry_id": "n1",
      "nodes": [
        { "id": "n1", "kind": "trigger", "trigger": { "event": "signed_up" }, "next": "n2" },
        { "id": "n2", "kind": "send", "send": { "channel": "sms", "template_id": 42 } }
      ]
    }
  }'
JSON
{ "id": 4, "name": "خوش‌آمد", "live_version": 0, "status": "draft", "problems": [] }

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

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

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

وضعیتکدچه وقت
400name_requiredname نیامده
400graph_requiredgraph نیامده
409name_takenاین حساب از قبل ژورنی‌ای با همین نام دارد
503journey_unavailablestore نتوانست ذخیره کند

نام ژورنی در هر حساب یکتاست. ساختی که با نام موجود برخورد کند 409 می‌گیرد و خود نام در پیام می‌آید، چون راه چاره همان است: یا شناسه‌ی همان ژورنی را بدهید تا پیش‌نویسش جایگزین شود، یا نام دیگری بگذارید. تا پیش از این جدا شدن، جوابش 503 بود که یعنی «سرویس بالا نیست»، و کلاینتی که روی آن دوباره تلاش کند تا ابد همان را می‌گیرد، در حالی که ژورنی موردنظرش تمام مدت وجود داشته.

#GET /v1/journeys/{id}/draft

مجوز journey.read، هزینه ۱. پیش‌نویس ذخیره‌شده به‌علاوه دو فهرست:

JSON
{
  "name": "خوش‌آمد",
  "graph": { "entry_id": "n1", "nodes": [] },
  "problems": [],
  "warnings": ["این سناریو با «بازگشت مشتری» مخاطب مشترک دارد"]
}

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

#POST /v1/journeys/validate

مجوز journey.read، هزینه ۱. گرافی را که هنوز ذخیره نکرده‌اید می‌سنجد. چیزی ذخیره نمی‌شود و چیزی عوض نمی‌شود.

JSON
{ "valid": false, "problems": ["گره n2 قالبی ندارد"], "warnings": [] }

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

#POST /v1/journeys/{id}/publish

مجوز journey.publish، هزینه ۱. نسخه تازه را منتشر می‌کند و شماره‌اش را برمی‌گرداند.

JSON
{ "version": 4, "status": "active" }

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

اگر پیش‌نویس آماده نباشد، 422 با کد not_publishable و همان فهرست details.problems را می‌گیرید، چون آن یک خرابی سرور نیست، خود گراف است.

#POST /v1/journeys/{id}/{action}

مجوز journey.write، هزینه ۱. سه کار مجاز است و بس:

کاروضعیت بعدشمعنی
pausepausedورود تازه متوقف می‌شود. کسانی که داخل‌اند سر جایشان می‌مانند
resumeactiveورود از سر گرفته می‌شود
archivearchivedاز فهرست کار روزمره بیرون می‌رود
JSON
{ "status": "paused" }

هر چیز دیگری 400 با کد unknown_action می‌گیرد و فهرست کارهای مجاز در details.allowed می‌آید، چون «کار نامعلوم» می‌گوید اشتباه کردید و نمی‌گوید چه کار کنید.

#خروجی

دو مسیر، هر دو data.export می‌خواهند و هر دو پشت قفل نرماند. data.export از هر مجوز خواندنی جداست چون خروجی از ساختمان بیرون می‌رود.

#GET /v1/exports

هزینه ۱. تنها مسیری روی این میزبان که پاکت صفحه را استفاده می‌کند و تنها مسیری که ?limit= را می‌خواند. ?cursor= خوانده و دور ریخته می‌شود.

Shell
curl -s "https://api.segmentic.net/v1/exports?limit=50" \
  -H "Authorization: Bearer sk_seg_..."
JSON
{
  "data": [
    {
      "id": 42,
      "kind": "events",
      "format": "ndjson",
      "spec": { "segment_id": 12 },
      "status": "queued",
      "rows_written": 0,
      "bytes": 0,
      "attempts": 1,
      "expires_at": "2026-08-13T09:00:00Z",
      "requested_by": "api-key:3",
      "created_at": "2026-08-06T09:00:00Z"
    }
  ],
  "has_more": false
}

status یکی از queued، running، ready، failed یا expired است. attempts منتشر می‌شود چون «شکست خورد» و «سه بار شکست خورد و ایستاد» دو جواب متفاوت به تنها پرسشی هستند که مشتری درباره یک خروجی می‌پرسد.

has_more همیشه false است، حتی وقتی سطر بیشتری هست. data همیشه آرایه است.

شکست: 503 با کد export_unavailable و پیام could not read the export list.

GET /v1/exports/{id} و مسیر دانلود روی این میزبان وجود ندارند. یک یکپارچه‌سازی می‌تواند خروجی را در صف بگذارد و فهرست را ببیند، و بعد یک آدم باید فایل را از پنل بردارد. location روی سیم هست ولی یک مسیر ذخیره‌سازی است، نه لینک امضاشده.

#POST /v1/exports

هزینه ۲۵.

Shell
curl -s -X POST https://api.segmentic.net/v1/exports \
  -H "Authorization: Bearer sk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "events",
    "format": "ndjson",
    "spec": { "segment_id": 12, "from": "2026-07-01", "to": "2026-08-01" }
  }'
JSON
{
  "id": 42,
  "status": "queued",
  "kind": "events",
  "expires_after_hours": 168
}

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

فیلدلازمقاعده
kindبلهیکی از events، messages، profiles، segment
formatخیرهر چیزی که دقیقا csv نباشد ndjson می‌شود، از جمله یک غلط املایی و از جمله فرمت‌های ناشناس
specخیردست‌نخورده و بدون اعتبارسنجی پاس داده می‌شود

NDJSON پیش‌فرض است چون خروجی رویدادها با ویژگی‌های تودرتو یک مستطیل نیست، و صاف‌کردنش در CSV بی‌صدا تودرتویی را از بین می‌برد.

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

expires_after_hours برابر ۱۶۸ است، یعنی هفت روز. فایلی که ایمیل همه مشتری‌ها را دارد و برای همیشه روی یک share می‌ماند، همان چیزی است که یک خروجی بی‌احتیاط را به یک نشت تبدیل می‌کند، و کسی یادش نمی‌ماند پاکش کند، پس سکو پاکش می‌کند.

requested_by به‌صورت api-key:3 ثبت می‌شود. «چه کسی نشانی همه مشتری‌ها را خروجی گرفت» پرسشی است که ممیزی بعدا می‌پرسد، و جوابش باید چیزی را نام ببرد که قابل ابطال باشد.

وضعیتکدچه وقت
400malformed_jsonبدنه JSON معتبر نیست
422export_kind_invalidپیام: kind must be one of events, messages, profiles, segment
503export_unavailableصف نتوانست بپذیرد

#گزارش

دو مسیر، هر دو analytics.read می‌خواهند، هزینه هرکدام ۲۵، هر دو پشت قفل نرم. سقف بدنه ۱ مگابایت و مهلت ۴۵ ثانیه (نه query_timeout_sec).

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

#POST /v1/reports/funnel

Shell
curl -s -X POST https://api.segmentic.net/v1/reports/funnel \
  -H "Authorization: Bearer sk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "steps": [
      { "name": "product_viewed" },
      { "name": "checkout_started" },
      { "name": "order_completed", "filters": [{ "prop": "revenue", "op": "gte", "value": "500000" }] }
    ],
    "range": { "from": "2026-05-01T00:00:00Z", "to": "2026-06-01T00:00:00Z" },
    "window": "7d",
    "split_by": "city"
  }'
JSON
{
  "steps": [
    { "index": 0, "name": "product_viewed", "label": "product_viewed", "users": 700, "from_start": 1, "from_previous": 1, "dropped_here": 0 },
    { "index": 1, "name": "checkout_started", "label": "checkout_started", "users": 300, "from_start": 0.4286, "from_previous": 0.4286, "dropped_here": 400 }
  ],
  "buckets": [
    { "value": "تهران", "steps": [], "entered": 400, "completed": 180, "conversion": 0.45 }
  ],
  "entered": 700,
  "completed": 300,
  "conversion": 0.4286,
  "description": "..."
}
فیلدلازمقاعده
stepsبلهبین ۲ و ۱۲ مرحله
steps[].nameبلهنام رویداد، خالی نباشد، حداکثر ۲۵۶ نویسه
steps[].labelخیربرچسب نمودار
steps[].filtersخیرحداکثر ۱۰ فیلتر برای هر مرحله
steps[].filters[].opبلهمتنی: eq، ne، contains، prefix. عددی: gt، gte، lt، lte، num_eq، num_ne
range.from و range.toبلهRFC3339، from کوچک‌تر از to، بازه حداکثر ۷۳۰ روز
windowبله، و باید بزرگ‌تر از صفر باشدمثل "7d"، "1.5d"، "36h". نباید از خود بازه بزرگ‌تر باشد
strictخیرپیش‌فرض false
split_byخیراز فهرست پایین، یا prop:<کلید>

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

فهرست مجاز split_by: platform، os، device، app_version، country، city، region، province، utm_source، utm_campaign، browser. به‌علاوه شکل prop: برای هر کلید ویژگی رویداد، مثل prop:category.

نرخ‌ها کسرند، نه درصد. buckets فقط وقتی می‌آید که تفکیک خواسته شده باشد.

#POST /v1/reports/retention

Shell
curl -s -X POST https://api.segmentic.net/v1/reports/retention \
  -H "Authorization: Bearer sk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "start": { "name": "signed_up" },
    "return": { "name": "order_completed" },
    "range": { "from": "2026-01-01T00:00:00Z", "to": "2026-07-01T00:00:00Z" },
    "granularity": "week",
    "periods": 12
  }'
JSON
{
  "granularity": "week",
  "period_label": "هفته",
  "cohorts": [
    {
      "cohort": "1405-02-11",
      "label": "...",
      "size": 4021,
      "cells": [
        { "period": 0, "users": 4021, "rate": 1, "observable": true },
        { "period": 1, "users": 1802, "rate": 0.448, "observable": true },
        { "period": 8, "users": 0, "rate": 0, "observable": false }
      ]
    }
  ],
  "average": [{ "period": 0, "users": 0, "rate": 1, "observable": true }],
  "description": "..."
}
فیلدلازمپیش‌فرضقاعده
startخیرخالینام خالی یعنی «هر فعالیتی»
returnخیرخالیهمان
rangeبلهنداردبازه حداکثر ۷۳۰ روز
granularityخیرdayیکی از day، week، month
periodsخیر۳۰حداکثر ۶۰

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

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

مرزهای کوهورت در Go و به وقت تهران و روی تقویم ایرانی حساب می‌شوند. توابع تقویمی ClickHouse برای این بازار درست نیستند: toStartOfMonth میلادی است و toStartOfWeek نمی‌تواند شنبه شروع کند.

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


#POST /v1/messages

مجوز campaign.send، هزینه ۱. فقط وقتی features.transactional روشن باشد ثبت می‌شود. سقف بدنه ۲۵۶ کیلوبایت.

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

Shell
curl -s -X POST https://api.segmentic.net/v1/messages \
  -H "Authorization: Bearer sk_seg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "u_9137",
    "channel": "sms",
    "category": "transactional",
    "template_id": 42,
    "vars": { "code": "8391" },
    "idempotency_key": "order-8821-shipped"
  }'
JSON
{
  "message_id": "t7.order-8821-shipped",
  "status": "sent",
  "sent_at": "2026-08-06T09:12:41Z"
}

وضعیت 200 هم برای ارسال تازه و هم برای پخش دوباره.

فیلدلازمقاعده
user_idبلهخالی نباشد
channelبلهیکی از push، sms، email، webpush، inapp، bale، eitaa، rubika
categoryخیرtransactional (پیش‌فرض) یا critical. marketing رد می‌شود
template_idبلهناصفر. متن درون‌خطی اصلا پذیرفته نمی‌شود
varsخیرحداکثر ۴۰ کلید
idempotency_keyبلهالگوی ^[A-Za-z0-9._:-]{8,200}$

اینجا web کار نمی‌کند، با اینکه در ساخت کمپین کار می‌کند و بعضی ابزارها آن را تبلیغ می‌کنند. این مسیر رشته را مستقیم با فهرست بالا مقایسه می‌کند و جدول مستعارها روی این راه نیست. webpush بنویسید. messenger و webhook هم اینجا رد می‌شوند؛ فقط برای کمپین‌اند.

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

رد marketing تزیینی نیست. این مسیر از سقف‌های تکرار و ساعت‌های سکوت رد می‌شود، پس پذیرفتن یک پیام بازاریابی اینجا به هر مشتری یک راه مستند برای دور زدن قواعد ارسال خودش می‌داد، و اولین باری که مهم می‌شد یک پیامک تبلیغاتی ساعت سه بامداد به کل یک فهرست بود.

#کلید یکتایی

idempotency_key می‌تواند در بدنه بیاید یا به‌صورت هدر Idempotency-Key. اگر هر دو باشند، بدنه برنده است. بزرگی و کوچکی حروف حفظ می‌شود: یکی‌کردنشان Order-8821 را با order-8821 ادغام می‌کرد، دو کلید که یک تماس‌گیرنده سخت‌گیر ممکن است واقعا برای دو چیز متفاوت استفاده کند.

message_id ساخته نمی‌شود، مشتق می‌شود: t به‌علاوه شناسه حساب، نقطه، کلید شما.

حالتجواب
بار اول200 با نتیجه
تکرار پس از تمام‌شدن اولی200 با همان بدنه ذخیره‌شده، به‌علاوه "replayed": true
تکرار حین اجرای اولی409 با Retry-After: 1 و بدنه {"error":"transactional: a message with this idempotency key is already in flight"}
تکرار پس از شکست ارسال اولیاجازه اجرا دارد. رزرو آزاد شده است

replayed به تماس‌گیرنده اجازه می‌دهد «قبلا این کار را کردیم» را از «همین الان این کار را کردیم» تشخیص بدهد، که وقتی تلاش اول time out شده و نمی‌دانید کدامش اتفاق افتاده، اهمیت دارد.

کلیدهای یکتایی هیچ‌وقت جارو نمی‌شوند. یک مقدار پیکربندی به نام «نگه‌داری هفت روزه» وجود دارد و هیچ کدی آن را نمی‌خواند و هیچ فرمانی جارو را صدا نمی‌زند. دو نتیجه عملی: کلیدی مثل order-8821-shipped یک سال بعد هم پخش دوباره همان نتیجه یک‌ساله را می‌دهد، نه یک ارسال تازه. و رزروی که یک پردازه crash‌کرده رها کرده، هیچ‌وقت خودکار پاک نمی‌شود، پس هر تلاش دوباره روی آن کلید تا ابد 409 می‌گیرد تا وقتی یک نفر دستی سطرش را پاک کند. کلیدهایتان را یکتا و برای همیشه یکتا انتخاب کنید.

#دو محدودکننده روی یک مسیر

این مسیر دوبار متر می‌شود و دو رد متفاوت با دو بدنه متفاوت دارد:

  • بودجه درخواست، یک واحد، به‌ازای هر کلید. رد: 429 با کد budget_exhausted در پاکت این API.
  • محدودکننده نرخ پیام، یک درخواست، به‌ازای هر حساب در هر دقیقه تقویمی. رد: 429 با Retry-After: 60 و بدنه مسطح {"error":"rate limit exceeded: N requests per minute"}.

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

وقتی سقفی تنظیم شده باشد، دو هدر X-RateLimit-Limit و X-RateLimit-Remaining روی هر پاسخ این مسیر می‌آیند، نه فقط روی ردها. تماس‌گیرنده‌ای که نمی‌تواند ببیند چقدر به سقف نزدیک است، راهی برای آهسته‌شدن پیش از رد شدن ندارد.

#خطاها

همه در پاکت مسطح، نه پاکت این API:

وضعیتبدنه
400{"error":"malformed JSON: <detail>"} برای JSON بد یا فیلد ناشناس
400{"error":"transactional: user_id is required"} و خواهرهایش
409{"error":"transactional: a message with this idempotency key is already in flight"}
429{"error":"rate limit exceeded: N requests per minute"}
503{"error":"message not sent"}
200نتیجه، حتی وقتی دفتر ثبت پس از ارسال شکست خورده. پیام واقعا رفته و تماس‌گیرنده‌ای که خلافش را بشنود دومی را می‌فرستد

متن‌های دقیق خطای تماس‌گیرنده: transactional: user_id is required، transactional: template_id is required، transactional: idempotency_key is required، transactional: unknown channel، transactional: this endpoint does not send marketing; use a campaign، transactional: too many variables، و پیام کلید بدشکل که بازه ۸ تا ۲۰۰ نویسه را نام می‌برد.

هیچ مسیری برای خواندن وضعیت یک پیام وجود ندارد. GET /v1/messages/{idempotency_key} ساخته نشده است. تنها راه دیدن نتیجه یک ارسال، پاسخ همان تماس یا گزارش پیام‌ها در پنل است.


#پاکت خطا

شکل پاکت این است:

JSON
{
  "error": {
    "code": "forbidden",
    "message": "this key does not carry data.export, see GET /v1/whoami for what it does carry",
    "details": { "limit": 500, "sent": 501 },
    "need": "data.export"
  }
}

code پایدار و ماشین‌خوان است. قرارداد همان است؛ message نیست. یکپارچه‌سازی‌ای که روی متن پیام شاخه بزند، اولین باری که ما جمله را بهتر کنیم می‌شکند. details وقتی هست که مشکل فیلدی خاص باشد، و need فقط روی 403 می‌آید.

مسیری که وجود ندارد این را می‌گیرد:

JSON
{
  "error": {
    "code": "unknown_endpoint",
    "message": "no such endpoint: POST /v1/team/keys, see GET /v1/capabilities"
  }
}

#پاکت یک شکل ندارد

این مهم‌ترین جمله این صفحه برای کسی است که مدیریت خطا می‌نویسد.

یازده تا از بیست‌ودو مسیر، هندلر مشترک با پنل دارند، و آن هندلرها در پاکت پنل جواب می‌دهند: {"error":"<رشته>"} یا {"error":"<رشته>","code":"..."}. یعنی error گاهی شیء است و گاهی رشته.

مسیرچه شکستیبدنه واقعی
GET /v1/schema/events503{"error":"schema unavailable"}
GET /v1/schema/traits503{"error":"schema unavailable"}
POST /v1/audiences/validate400 JSON بد{"error":"malformed JSON"}
POST /v1/audiences/count400 و 503{"error":"segment: ..."} و {"error":"count unavailable"}
GET /v1/segments503{"error":"segments unavailable"}
GET /v1/segments/{id}400 و 404{"error":"invalid segment id"} و {"error":"segment not found"}
GET /v1/campaigns503{"error":"campaigns unavailable"}
GET /v1/campaigns/{id}400 و 404{"error":"invalid campaign id"} و {"error":"campaign not found"}
POST /v1/reports/funnel400 و 503{"error":"<فارسی>","code":"invalid_report"} و {"error":"<فارسی>"}
POST /v1/reports/retention400 و 503همان
POST /v1/messagesهر شکستی{"error":"<رشته>"} مسطح

به‌علاوه چهار رد که پیش از اجرای هندلر تولید می‌شوند و همه در پاکت مسطح‌اند: wrong_surface روی 401، و ip_not_allowed، impersonation_read_only و impersonation_forbidden روی 403.

پس مدافعانه parse کنید. اول نوع error را ببینید: اگر شیء است، error.code را بخوانید؛ اگر رشته است، آن را به‌عنوان پیام لاگ کنید و روی status تصمیم بگیرید.

متن این خطاها همیشه فارسی است. میان‌افزار زبان روی این شنونده نصب نشده، پس Accept-Language روی این میزبان اصلا خوانده نمی‌شود. هدر Accept-Language: en هیچ اثری ندارد. کلاینتی که متن خطا را لاگ می‌کند باید UTF-8 و راست‌به‌چپ را تحمل کند.

#همه کدها

کدوضعیتیعنی
unauthenticated401اعتبارنامه نیست یا resolve نشد
api_key_required401نشست فرستاده شده
write_key_rejected401توکن wk_ فرستاده شده
key_expired401کلید منقضی شده
forbidden403مجوز نیست. need را بخوانید
account_locked403قفل نرم. details.reason را بخوانید
budget_exhausted429بودجه دقیقه تمام شده. صبر کنید و دوباره بزنید
budget_unavailable503شمارنده بودجه در دسترس نیست. گذرا
unknown_endpoint404مسیر یا متد اشتباه
malformed_json400بدنه JSON نیست
bad_id400شناسه مسیر عدد مثبت نیست
not_found404سگمنت یا کمپین وجود ندارد
filter_invalid422تعریف سگمنت رد شد
name_required400نام سگمنت خالی است
segment_unavailable503store سگمنت شکست خورد
invalid_channel400کانال کمپین resolve نشد
campaign_invalid422اعتبارسنجی کمپین رد شد
campaign_unavailable503store کمپین شکست خورد
not_submittable409کمپین draft یا paused نیست
approval_required409تایید لازم است و نیست
approval_stale409کمپین بعد از تایید عوض شده
approval_unavailable503خواندن یا نوشتن تایید شکست خورد
export_kind_invalid422kind در فهرست نیست
export_unavailable503صف خروجی شکست خورد
batch_empty400آرایه events خالی است
batch_too_large413بیش از ۵۰۰ رویداد
all_events_rejected422هیچ رویدادی قابل قبول نبود
ingest_unavailable503صف ورود داده در دسترس نیست
quota_cancelled402اشتراک سرویس نمی‌دهد
quota_trial_over402دوره آزمایشی تمام شده
quota_event_cap402سقف سخت رویداد
quota_message_cap402تعریف شده و هرگز برگردانده نمی‌شود. تنها 402 اینجا مال POST /v1/events است و آن سهمیه را با سنجه رویداد می‌پرسد؛ سقف پیام فقط برای سنجه پیام خوانده می‌شود

خانواده quota_* را تکرار نکنید: تا کسی پرداخت نکند یا دوره عوض نشود چیزی تغییر نمی‌کند. صفحه کدهای خطا همین فهرست را با کار پیشنهادی برای هرکدام دارد.


#چیزهایی که این API ندارد

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

  • صفحه‌بندی کارآمد. ?cursor= هیچ‌جا خوانده نمی‌شود و هیچ پاسخی کلید next_cursor ندارد.
  • PATCH روی هیچ‌چیز. فقط PUT با جایگزینی کامل، بدون If-Match و بدون نشانه نسخه.
  • یکتایی روی هر مسیری جز POST /v1/messages. تلاش دوباره روی POST /v1/segments که time out شده، سگمنت دوم می‌سازد.
  • مکث، ادامه یا لغو کمپین.
  • تایید کمپین، صف تایید، تاریخچه تایید.
  • دانلود خروجی، و GET /v1/exports/{id}.
  • خواندن وضعیت یک پیام تراکنشی.
  • خواندن یا نوشتن پرونده یک کاربر. GET /v1/profiles/{user_id} وجود ندارد.
  • رضایت و لغو اشتراک از راه API.
  • الگو، سناریو، ممیزی و قوانین ارسال. همه فقط در پنل.
  • پیش‌نمایش سگمنت، اندازه سگمنت، و تخمین نمونه‌گیری‌شده.
  • گزارش مسیر. POST /v1/reports/paths روی این میزبان ثبت نشده است.
  • ساختن کلید باریک (scoped). ستون در دیتابیس هست و هیچ راهی برای نوشتنش نیست.
  • بودجه گیرنده و سقف سطر PII به‌ازای کلید. ستون‌هایشان در دیتابیس هستند و هیچ کدی آن‌ها را نمی‌خواند.
  • هدر باقی‌مانده بودجه.
  • هشدار پیش از قفل نرم. وضعیت «معوق» روز سی‌ویکم روی این سطح دیده نمی‌شود.
  • preflight برای مرورگر. این API برای تماس سرور به سرور است.

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

قبلینقاط ورود دادهبعدیکدهای خطا

در این صفحه

  • میزبان و کلید
  • احراز هویت
  • GET /v1/whoami
  • GET /v1/capabilities
  • مجوزها
  • بودجه درخواست
  • صفحه‌بندی
  • قفل نرم
  • GET /v1/status
  • شمای داده
  • مخاطب بدون ذخیره
  • سگمنت
  • کمپین
  • POST /v1/events
  • قالب پیام
  • سناریو
  • خروجی
  • گزارش
  • POST /v1/messages
  • پاکت خطا
  • چیزهایی که این API ندارد

سگمنتیک

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