مرجع: 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/events | event.read | ۵ | خیر | همیشه |
GET /v1/schema/traits | event.read | ۵ | خیر | همیشه |
GET /v1/ingest/quality | event.read | ۵ | خیر | همیشه |
POST /v1/audiences/validate | segment.read | ۱ | خیر | همیشه |
POST /v1/audiences/count | segment.read | ۲۵ | خیر | همیشه |
GET /v1/segments | segment.read | ۱ | خیر | segments |
GET /v1/segments/{id} | segment.read | ۱ | خیر | segments |
POST /v1/segments | segment.write | ۱ | خیر | segments |
PUT /v1/segments/{id} | segment.write | ۱ | خیر | segments |
DELETE /v1/segments/{id} | segment.delete | ۱ | خیر | segments |
GET /v1/campaigns | campaign.read | ۱ | خیر | campaigns |
GET /v1/campaigns/{id} | campaign.read | ۵ | خیر | campaigns |
POST /v1/campaigns | campaign.write | ۱ | خیر | campaigns |
PUT /v1/campaigns/{id}/recurrence | campaign.send | ۱ | خیر | campaigns و campaign_recurrence |
DELETE /v1/campaigns/{id}/recurrence | campaign.write | ۱ | خیر | campaigns و campaign_recurrence |
POST /v1/campaigns/{id}/send | campaign.send | ۱ | خیر | campaigns |
GET /v1/templates | template.read | ۱ | خیر | templates |
GET /v1/templates/{id} | template.read | ۱ | خیر | templates |
POST /v1/templates | template.write | ۱ | خیر | templates |
POST /v1/templates/render | template.read | ۱ | خیر | templates |
GET /v1/journeys | journey.read | ۱ | خیر | journeys |
GET /v1/journeys/{id} | journey.read | ۵ | خیر | journeys |
POST /v1/journeys | journey.write | ۱ | خیر | journeys |
GET /v1/journeys/{id}/draft | journey.read | ۱ | خیر | journeys |
POST /v1/journeys/validate | journey.read | ۱ | خیر | journeys |
POST /v1/journeys/{id}/publish | journey.publish | ۱ | خیر | journeys |
POST /v1/journeys/{id}/{action} | journey.write | ۱ | خیر | journeys |
POST /v1/campaigns/{id}/submit | campaign.write | ۱ | خیر | campaigns و campaign_approval |
POST /v1/events | profile.write | ۵ | خیر | ingest |
GET /v1/exports | data.export | ۱ | بله | async_exports |
POST /v1/exports | data.export | ۲۵ | بله | async_exports |
POST /v1/reports/funnel | analytics.read | ۲۵ | بله | analytics |
POST /v1/reports/retention | analytics.read | ۲۵ | بله | analytics |
POST /v1/messages | campaign.send | ۱ | خیر | transactional |
ستون آخر نام یک کلید در features پاسخ توانمندیها است. اگر آن کلید false باشد، آن مسیرها روی این نصب اصلا ثبت نشدهاند و 404 میدهند. ستون هزینه در بودجه درخواست و ستون قفل در قفل نرم توضیح داده شده است.
احراز هویت
اعتبارنامه از سه جا خوانده میشود، به همین ترتیب:
- هدر
Authorization: Bearer <token>(نام scheme به بزرگی و کوچکی حرف حساس نیست) - هدر
X-Segmentic-Key: <token> - کوکیهای
__Host-segmentic_sessionو بعدsegmentic_session
کوکی عمدا آخر است: درخواستی که هدر Authorization صریح دارد، قصد کرده از همان استفاده کند، و ترجیحدادن بیصدای یک کوکی محیطی همانجایی است که یک مرورگر، درخواست یک کلاینت API را با هویت اشتباه انجام میدهد. روی این سطح کوکی بههرحال بیفایده است، چون نشست پذیرفته نمیشود.
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 | یک نشست معتبر آمده، کوکی یا bearer | this 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 |
{
"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 است و نام مجوز را میگوید. این عمدی است: جایگزینش این است که مشتری برای فهمیدن اینکه کدام مجوز را باید بدهد، تیکت پشتیبانی باز کند.
{
"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
مجوز نمیخواهد، هزینه ۱. هر کلید معتبری جواب میگیرد. این و توانمندیها دو تماسی هستند که یک کلاینت باید موقع راهاندازی بزند: یکی میگوید این کلید چه میتواند، دیگری میگوید این نصب چه دارد.
curl -s https://api.segmentic.net/v1/whoami \
-H "Authorization: Bearer sk_seg_..."
{
"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
مجوز نمیخواهد، هزینه ۱. این پاسخ میگوید این نصب امروز چه چیزی را سرو میکند و چه سقفهایی را اعمال میکند.
curl -s https://api.segmentic.net/v1/capabilities \
-H "Authorization: Bearer sk_seg_..."
{
"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 |
analytics | POST /v1/reports/funnel و POST /v1/reports/retention |
transactional | POST /v1/messages |
export | هیچچیز روی این میزبان. خروجیگیر CSV پنل است. اطلاعاتی است و بس |
import | هیچچیز روی این میزبان. بارگذاری CSV پنل است |
journeys | هیچچیز روی این میزبان. سناریو هیچ مسیر عمومی ندارد |
ingest | POST /v1/events |
async_exports | GET /v1/exports و POST /v1/exports |
campaign_approval | POST /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.read | POST /v1/audiences/validate، POST /v1/audiences/count، GET /v1/segments، GET /v1/segments/{id} |
segment.write | POST /v1/segments، PUT /v1/segments/{id} |
segment.delete | DELETE /v1/segments/{id} |
profile.read | هیچچیز |
profile.write | POST /v1/events |
event.read | GET /v1/schema/events، GET /v1/schema/traits |
campaign.read | GET /v1/campaigns، GET /v1/campaigns/{id} |
campaign.write | POST /v1/campaigns، POST /v1/campaigns/{id}/submit، DELETE /v1/campaigns/{id}/recurrence |
campaign.send | PUT /v1/campaigns/{id}/recurrence، POST /v1/campaigns/{id}/send، POST /v1/messages |
campaign.approve | هیچچیز. مسیر تایید عمومی وجود ندارد |
analytics.read | POST /v1/reports/funnel، POST /v1/reports/retention |
data.export | GET /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/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 است:
{"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 بابت نبود مجوز پیش از کسر بودجه میآید، پس تماس رد شده بابت مجوز هزینهای ندارد.
صفحهبندی
این بخش را کامل بخوانید، چون آنچه در کد هست با آنچه انتظار دارید فرق میکند.
پاکت صفحه این شکل است:
{
"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 دارد، میتوانست همان اعداد قیف و ماندگاری را که پنل تازه جلویشان را گرفته بود با یک اسکریپت بخواند و یک خروجی هم در صف بگذارد. قفلی که یک نوع اعتبارنامه رعایتش کند و نوع دیگر نه، قفل نیست؛ یک دور زدن است، و آن دور، یک اسکریپت فاصله دارد.
حساب قفلشده این را میبیند:
{
"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 میدهد هنوز جواب میدهد.
curl -i https://api.segmentic.net/v1/status
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
curl -s https://api.segmentic.net/v1/schema/events \
-H "Authorization: Bearer sk_seg_..."
{
"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
curl -s https://api.segmentic.net/v1/schema/traits \
-H "Authorization: Bearer sk_seg_..."
{
"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 میگوید یک شرط تعامل با چه چیزهایی مقایسه میشود:
{
"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
چه چیزی را از شما رد کردیم و چه چیزی را اصلاح کردیم، به تفکیک روز.
curl -s "https://api.segmentic.net/v1/ingest/quality?days=7" -H "Authorization: Bearer sk_seg_..."
{
"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"}.
روی نصبی که خواننده انبار داده ندارد اصلا سرو نمیشود. پاسخ خالی اینجا یعنی «هیچوقت چیزی رد نشده»، و این تنها حرف غلطی است که این اندپوینت میتوانست بزند.
مخاطب بدون ذخیره
دو مسیر که روی یک فیلتر کار میکنند بدون اینکه چیزی ذخیره کنند. شیء ذخیرهشده «سگمنت» است؛ اینها عملیات لحظهای روی یک تعریفاند.
هر دو بدنه یکسانی میگیرند و سقف بدنهشان ۱ مگابایت است، نه ۸ مگابایت بقیه این سطح:
{
"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، هزینه ۱. دیتابیس را اصلا لمس نمیکند.
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":"تهران"}}}}'
{
"valid": true,
"description_fa": "کاربرانی که شهرشان تهران است"
}
جمله فارسی اینجاست چون همان چیزی است که فیلتر بدخواندهشده را میگیرد: کسی که «کاربرانی که شهرشان تهران است» را میبیند در حالی که مشهد را میخواسته، باگش را پیش از خرجکردن یک کوئری پیدا کرده است.
فیلتر نامعتبر 422 است:
{
"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، هزینه ۲۵. شمارش دقیق است، نه نمونهگیریشده.
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":"تهران"}}}}'
{
"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، هزینه ۱.
curl -s https://api.segmentic.net/v1/segments \
-H "Authorization: Bearer sk_seg_..."
{
"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، هزینه ۱. سقف بدنه ۸ مگابایت.
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": "تهران" }
}
}
}'
{
"id": 12,
"name": "تهرانیها",
"description_fa": "کاربرانی که شهرشان تهران است"
}
وضعیت 201. دو فیلد بیشتر ندارد: name که پس از trim نباید خالی باشد، و definition که باید از اعتبارسنجی رد شود.
| وضعیت | کد | چه وقت |
|---|---|---|
400 | malformed_json | بدنه JSON معتبر نیست |
400 | name_required | نام خالی است. پیام: a segment needs a name |
422 | filter_invalid | تعریف از اعتبارسنجی رد نشد |
503 | segment_unavailable | store نتوانست ذخیره کند |
فیلد kind در این struct وجود ندارد. هر سگمنتی که از این API ساخته شود dynamic است. ساختن فهرست ثابت یا realtime از این API ممکن نیست.
tenant_id در بدنه نادیده گرفته میشود، رد نمیشود. حساب از روی کلید ست میشود و فیلد بدنه اصلا خوانده نمیشود: بار دادهای که حساب دیگری را نام میبرد باید هیچ اثری نداشته باشد، و این خاصیت نخواندن فیلد است نه خاصیت بررسیکردنش.
فیلدهای ناشناس در بدنه پذیرفته و نادیده گرفته میشوند. تنها مسیری روی این میزبان که فیلد ناشناس را رد میکند POST /v1/messages است. یعنی روی بقیه، غلط املایی در نام یک فیلد نامرئی است.
PUT /v1/segments/{id}
مجوز segment.write، هزینه ۱. همان بدنه ساخت.
این جایگزینی کامل شیء است، نه ادغام. PATCH وجود ندارد و هیچ If-Match یا نشانه نسخهای در کار نیست، پس دو نویسنده همزمان بیصدا روی هم مینویسند.
سه رفتار که باید بدانید:
- اعتبارسنجی تعریف پیش از بررسی وجود سگمنت اجرا میشود. فیلتر نامعتبر روی شناسهای که وجود ندارد هم
422میگیرد. - سگمنت اول خوانده میشود. شناسه ناشناس یا مال حساب دیگر
404با کدnot_foundمیگیرد، نه یک نوشتن که بیصدا سگمنت تازه بسازد. nameخالی یا حذفشده، نام قبلی را نگه میدارد. پاکش نمیکند و خطا هم نمیدهد.
پاسخ 200 با همان سه کلید ساخت. شناسه غیرمثبت 400 با کد bad_id و پیام the path must carry a positive integer id میگیرد.
DELETE /v1/segments/{id}
مجوز segment.delete، هزینه ۱. مجوز خودش را دارد چون برداشتن مخاطبی که سناریوی یک نفر به آن ارجاع میدهد، همان کار ویرایشکردن یکی نیست.
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، هزینه ۱. صفحهبندی نمیشود، و روی ۲۰۰ کمپین تازهتر سقف میخورد بدون اینکه چیزی در پاسخ این را بگوید.
{
"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، هزینه ۵. این گزارش کمپین است، نه فقط رکورد آن.
{
"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، هزینه ۱.
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"
}'
{ "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_at | RFC3339 | خیر | مقدار غیرقابلخواندن بیصدا حذف میشود، خطا نمیدهد |
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 در بدنه اصلا خوانده نمیشود.
| وضعیت | کد | چه وقت |
|---|---|---|
400 | malformed_json | بدنه JSON معتبر نیست |
400 | invalid_channel | پیام unknown channel "bogus"، و details برابر {"field":"channel"} |
422 | campaign_invalid | متن خطای اعتبارسنجی کمپین |
503 | campaign_unavailable | store نتوانست ذخیره کند |
PUT /v1/campaigns/{id}/recurrence
مجوز campaign.send، هزینه ۱. این مسیر برنامه تکرار خودکار یک کمپین ذخیرهشده را شروع یا جایگزین میکند. هر نوبت، کمپین تازهای با همان مخاطب و محتوا میسازد.
همه فیلدهای تقویمی با ساعت تهران خوانده میشوند. روز ماه، روز تقویم جلالی است. در برنامه هفتگی، شنبه 0 و جمعه 6 است.
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 را تعیین کند. این شمارنده صفر میشود و خود کارگر آن را نگه میدارد.
{ "status": "ok" }
| وضعیت | کد | چه وقت |
|---|---|---|
400 | bad_id یا malformed_json | شناسه یا بدنه خوانده نمیشود |
422 | recurrence_invalid | تناوب، ساعت، روز هفته یا روز ماه نامعتبر است |
503 | recurrence_unavailable | برنامه ذخیره نشد |
DELETE /v1/campaigns/{id}/recurrence
مجوز campaign.write، هزینه ۱، نه campaign.send. این مسیر تکرارهای خودکار آینده را متوقف میکند. کمپینهایی که برنامه قبلا ساخته، تغییر نمیکنند و پاک نمیشوند.
شروع کردن یک برنامه campaign.send میخواهد، چون هر نوبتش کمپین تازهای است که میتواند به کل مخاطب برسد. متوقف کردنش فقط از حجم ارسال کم میکند، پس بیش از مجوز ویرایش کمپین نمیخواهد، و این همان تفکیکی است که پنل برای pause و cancel قائل است. قبلا این مسیر هم campaign.send میخواست و هزینهاش دقیقا برعکس میافتاد: کلیدی که عمدا بدون campaign.send ساخته شده باشد، یعنی همان کلیدی که کار روزمره باید با آن انجام شود، تنها کلیدی بود که نمیتوانست برنامهای را که هر روز کمپین میساخت متوقف کند.
curl -s -X DELETE https://api.segmentic.net/v1/campaigns/5/recurrence \
-H "Authorization: Bearer sk_seg_..."
{
"status": "ok",
"note": "campaigns already created by this schedule are unchanged"
}
خطاها 400 bad_id و 503 recurrence_unavailable هستند.
POST /v1/campaigns/{id}/send
مجوز campaign.send، هزینه ۱. بدنهای خوانده نمیشود. این تماس برگشتناپذیر است.
curl -s -X POST https://api.segmentic.net/v1/campaigns/5/send \
-H "Authorization: Bearer sk_seg_..."
{ "id": 5, "status": "scheduled" }
وضعیت 202.
اگر حساب تایید کمپین را لازم کرده باشد، دروازه تایید اول اجرا میشود. APIای که به یک یکپارچهسازی اجازه بدهد از بازبینیای که پنل اعمال میکند رد شود، آن بازبینی را تزیینی میکند، و یکپارچهسازی دقیقا جایی است که آدم برای دور زدنش سراغش میرود.
| وضعیت | کد | چه وقت |
|---|---|---|
400 | bad_id | شناسه غیرمثبت در مسیر |
409 | approval_required | حساب تایید میخواهد و تاییدی نیست، یا رد شده، یا هنوز تایید نشده |
409 | approval_stale | کمپین بعد از تایید عوض شده. دوباره برای تایید بفرستید |
404 | not_found | کمپین خوانده نشد |
503 | approval_unavailable | خواندن قاعده یا وضعیت تایید شکست خورد. بسته شکست میخورد |
503 | campaign_unavailable | زمانبندی شکست خورد |
approval_stale کد جداگانه دارد چون این دو، یکپارچهسازی را به دو جای متفاوت میفرستند: یکی به «تایید بگیر»، دیگری به «یک نفر بعد از تایید این را ویرایش کرده». اثر انگشت روی شناسه سگمنت، تعریف درونخطی، الگو، کانال، موضوع، رویداد هدف، درصد گروه کنترل، زمانبندی و فهرست مرتبشده واریانتها گرفته میشود و ۳۲ نویسه شانزدهشانزدهی است.
هیچ راهی برای مکث، ادامه یا لغو کمپین روی این میزبان وجود ندارد. هر سه روی پنل هستند و روی این mux ثبت نشدهاند. وقتی بکاند شما یک ارسال را زمانبندی کرد، فقط پنل میتواند جلویش را بگیرد.
POST /v1/campaigns/{id}/submit
مجوز campaign.write، نه campaign.approve: نویسنده دارد درخواست میکند، نه تصمیم میگیرد. هزینه ۱. فقط وقتی features.campaign_approval روشن باشد ثبت میشود. بدنهای خوانده نمیشود.
{
"approval_id": 88,
"state": "pending",
"fingerprint": "3f1a9c02b7de4415aa0e8c1d2f6b3790"
}
وضعیت 202. مقادیر state: pending، approved، rejected، stale.
| وضعیت | کد | چه وقت |
|---|---|---|
400 | bad_id | شناسه غیرمثبت |
404 | not_found | کمپین وجود ندارد |
409 | not_submittable | وضعیت کمپین draft یا paused نیست |
422 | campaign_invalid | کمپین از اعتبارسنجی رد نشد |
503 | approval_unavailable | ثبت درخواست شکست خورد |
درخواست به سازنده کلید نسبت داده میشود، نه به خود کلید. قاعده دو نفره درباره آدمهاست، و نسبتدادن یک درخواست به «کلید شماره ۱۲» به یک نفر اجازه میداد با ساختن یک کلید هر دو نیمه را خودش نگه دارد. اگر سازنده کلید حذف شده باشد، این مقدار صفر است.
مسیری برای تایید کردن، دیدن صف تایید یا خواندن تاریخچه تایید روی این میزبان وجود ندارد. یک یکپارچهسازی میتواند بازبینی بخواهد و بعد باید منتظر یک آدم در پنل بماند. تنها راه برنامهای برای فهمیدن جواب، تلاش دوباره برای ارسال و خواندن کد 409 است.
POST /v1/events
مجوز profile.write، هزینه ۵، سقف بدنه ۸ مگابایت. فقط وقتی features.ingest روشن باشد ثبت میشود.
profile.write انتخاب شده و نه یک مجوز تازه، چون این کاری است که این مسیر میکند: روی پرونده آدمها و تاریخچه رویدادشان مینویسد، و ساختن نام دوم برای همان توانایی باعث میشد کسی یکی را بدهد به این خیال که دیگری را نگه داشته است. نقشهایی که دارندش: owner، admin، marketer.
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" }
}
]
}'
{ "accepted": 1 }
وضعیت 202، نه 200. رویدادها در صفاند، نه ذخیرهشده: در همان خط لولهای هستند که رویدادهای SDK از آن میگذرند و چند ثانیه بعد قابل کوئری میشوند. گفتن 200 کسی را دعوت میکرد که بلافاصله آنها را بخواند و نتیجه بگیرد گم شدهاند.
اگر بخشی از batch رد شود:
{
"accepted": 2,
"rejected": [
{ "index": 1, "reason": "missing_identity" }
]
}
index شماره در آرایه شماست، تا منطق تلاش دوبارهتان بتواند آیتم را بدون تطبیق محتوا پیدا کند. رویدادهای شما ممکن است اصلا هنوز شناسه نداشته باشند، که نیمی از دلیل رد شدنشان است.
هر آیتم اینجا اعتبارسنجی میشود، پیش از اینکه sink ببیندش. sink پشت این مسیر همان sinkای است که واردکننده CSV استفاده میکند و آنچه را نتواند نرمال کند دور میاندازد، که برای واردکنندهای که سطرهای خودش را از قبل اعتبارسنجی کرده درست است و برای مسیری که JSON دلخواه از اینترنت میگیرد غلط. بدون این حلقه، مشتری پانصد رویداد میفرستاد، 200 میگرفت، و یک هفته بعد چهارصدتایشان را غایب پیدا میکرد بدون اینکه هیچچیز جایی توضیحش بدهد.
ترتیب ردها:
| وضعیت | کد | چه وقت |
|---|---|---|
400 | malformed_json | بدنه JSON معتبر نیست |
400 | batch_empty | آرایه events خالی است |
413 | batch_too_large | بیش از ۵۰۰ رویداد. details برابر {"limit":500,"sent":501} |
402 | quota_cancelled یا quota_trial_over یا quota_event_cap | سقف تجاری. کل batch رد میشود |
422 | all_events_rejected | هیچ آیتمی قابل قبول نبود. details همان آرایه ردهاست |
503 | ingest_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_... محرمانه |
| کلید آرایه در بدنه | batch | events |
| وضعیت موفق | 200 | 202 |
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، هزینه ۱.
curl -s https://api.segmentic.net/v1/templates \
-H "Authorization: Bearer sk_seg_..."
{
"templates": [
{ "id": 42, "name": "خوشآمد پیامکی", "channel": "sms", "category": "marketing",
"title": "", "body": "سلام {{name}}، خوش آمدید." }
]
}
GET /v1/templates/{id}
مجوز template.read، هزینه ۱. کل قالب، بهعلاوه سه چیزی که در فهرست نیست:
{
"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، هزینه ۱. سقف بدنه ۸ مگابایت.
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}}، خوش آمدید."
}'
{ "id": 42, "name": "خوشآمد پیامکی", "channel": "sms" }
وضعیت 201 برای ساخت و 200 وقتی id بدهید و قالب موجود جایگزین شود. نام در هر حساب یکتاست.
| وضعیت | کد | چه وقت |
|---|---|---|
400 | name_required | نام پس از trim خالی است |
400 | content_required | نه عنوان دارد و نه متن |
400 | invalid_channel | کانال شناخته نشد |
503 | template_unavailable | ذخیره نشد |
POST /v1/templates/render
مجوز template.read، هزینه ۱. قالب را با مقدارهایی که میدهید پر میکند و نشان میدهد چه چیزی فرستاده میشد. چیزی فرستاده نمیشود و چیزی ذخیره نمیشود.
id بدهید تا قالب ذخیرهشده رندر شود، یا متن را درجا بفرستید تا پیشنویسی که ذخیره نکردهاید سنجیده شود.
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": "سارا" } }'
{
"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، هزینه ۱. فهرست سناریوها با وضعیت و نسخه منتشرشده و شمار کسانی که همین حالا داخلشان هستند.
curl -s https://api.segmentic.net/v1/journeys \
-H "Authorization: Bearer sk_seg_..."
{
"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، هزینه ۵. نسخه منتشرشده را برمیگرداند، بهعلاوه شمارنده هر گره.
{
"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، هزینه ۱. پیشنویس را ذخیره میکند. چیزی منتشر نمیشود.
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 } }
]
}
}'
{ "id": 4, "name": "خوشآمد", "live_version": 0, "status": "draft", "problems": [] }
id را ننویسید تا ساخته شود، یا شناسه یک سناریوی موجود را بنویسید تا پیشنویسش جایگزین شود. جایگزینی است نه ادغام: گراف کامل را بفرستید.
live_version نسخهای است که همین حالا در حال اجراست، نه چیزی که الان ذخیره کردید. ذخیره پیشنویس آن عدد را عوض نمیکند و صفر یعنی هنوز هیچ نسخهای منتشر نشده. هر دو برگردانده میشوند تا کسی ذخیره را با انتشار اشتباه نگیرد.
گراف نیمهساخته ذخیره میشود، دقیقا مثل پنل، چون ساختن یک سناریو چند نشست طول میکشد. بهجای رد کردن، فهرست ایرادها در problems همان پاسخ برمیگردد: خالی یعنی قابل انتشار است، و هر چه در آن باشد دلیلی است که فراخوان انتشار رد میکند. آن رد، همانجا میافتد که باید.
| وضعیت | کد | چه وقت |
|---|---|---|
400 | name_required | name نیامده |
400 | graph_required | graph نیامده |
409 | name_taken | این حساب از قبل ژورنیای با همین نام دارد |
503 | journey_unavailable | store نتوانست ذخیره کند |
نام ژورنی در هر حساب یکتاست. ساختی که با نام موجود برخورد کند 409 میگیرد و خود نام در پیام میآید، چون راه چاره همان است: یا شناسهی همان ژورنی را بدهید تا پیشنویسش جایگزین شود، یا نام دیگری بگذارید. تا پیش از این جدا شدن، جوابش 503 بود که یعنی «سرویس بالا نیست»، و کلاینتی که روی آن دوباره تلاش کند تا ابد همان را میگیرد، در حالی که ژورنی موردنظرش تمام مدت وجود داشته.
GET /v1/journeys/{id}/draft
مجوز journey.read، هزینه ۱. پیشنویس ذخیرهشده بهعلاوه دو فهرست:
{
"name": "خوشآمد",
"graph": { "entry_id": "n1", "nodes": [] },
"problems": [],
"warnings": ["این سناریو با «بازگشت مشتری» مخاطب مشترک دارد"]
}
تفاوت problems و warnings مهم است: اولی جلوی انتشار را میگیرد و دومی نمیگیرد. هشدار همپوشانی مخاطب از میان سناریوهای در جریان همین حساب میآید و همان چیزی است که پنل هم نشان میدهد.
POST /v1/journeys/validate
مجوز journey.read، هزینه ۱. گرافی را که هنوز ذخیره نکردهاید میسنجد. چیزی ذخیره نمیشود و چیزی عوض نمیشود.
{ "valid": false, "problems": ["گره n2 قالبی ندارد"], "warnings": [] }
همیشه 200 است، حتی وقتی گراف بیاعتبار است. درخواست موفق بوده؛ چیزی که حکم دارد گراف است. اگر این مسیر برای گراف بد 422 میداد، «این گراف قابل قبول نیست» از «سرور درخواستم را رد کرد» قابل تشخیص نبود.
POST /v1/journeys/{id}/publish
مجوز journey.publish، هزینه ۱. نسخه تازه را منتشر میکند و شمارهاش را برمیگرداند.
{ "version": 4, "status": "active" }
این کار برگشت ندارد. از همین لحظه هر کسی که ماشه سناریو را بزند وارد آن میشود. پیش از این فراخوان، گراف را با POST /v1/journeys/validate بسنجید.
اگر پیشنویس آماده نباشد، 422 با کد not_publishable و همان فهرست details.problems را میگیرید، چون آن یک خرابی سرور نیست، خود گراف است.
POST /v1/journeys/{id}/{action}
مجوز journey.write، هزینه ۱. سه کار مجاز است و بس:
| کار | وضعیت بعدش | معنی |
|---|---|---|
pause | paused | ورود تازه متوقف میشود. کسانی که داخلاند سر جایشان میمانند |
resume | active | ورود از سر گرفته میشود |
archive | archived | از فهرست کار روزمره بیرون میرود |
{ "status": "paused" }
هر چیز دیگری 400 با کد unknown_action میگیرد و فهرست کارهای مجاز در details.allowed میآید، چون «کار نامعلوم» میگوید اشتباه کردید و نمیگوید چه کار کنید.
خروجی
دو مسیر، هر دو data.export میخواهند و هر دو پشت قفل نرماند. data.export از هر مجوز خواندنی جداست چون خروجی از ساختمان بیرون میرود.
GET /v1/exports
هزینه ۱. تنها مسیری روی این میزبان که پاکت صفحه را استفاده میکند و تنها مسیری که ?limit= را میخواند. ?cursor= خوانده و دور ریخته میشود.
curl -s "https://api.segmentic.net/v1/exports?limit=50" \
-H "Authorization: Bearer sk_seg_..."
{
"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
هزینه ۲۵.
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" }
}'
{
"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 ثبت میشود. «چه کسی نشانی همه مشتریها را خروجی گرفت» پرسشی است که ممیزی بعدا میپرسد، و جوابش باید چیزی را نام ببرد که قابل ابطال باشد.
| وضعیت | کد | چه وقت |
|---|---|---|
400 | malformed_json | بدنه JSON معتبر نیست |
422 | export_kind_invalid | پیام: kind must be one of events, messages, profiles, segment |
503 | export_unavailable | صف نتوانست بپذیرد |
گزارش
دو مسیر، هر دو analytics.read میخواهند، هزینه هرکدام ۲۵، هر دو پشت قفل نرم. سقف بدنه ۱ مگابایت و مهلت ۴۵ ثانیه (نه query_timeout_sec).
خطاهای این دو مسیر در پاکت پنل و به فارسیاند، با کد invalid_report. جزئیات در پاکت خطا.
POST /v1/reports/funnel
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"
}'
{
"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
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
}'
{
"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 میتواند در دقیقه ششصد پیام بفرستد. اگر این عدد برای شما زیاد است، سقف حساب را تنظیم کنید. رجوع کنید به دو محدودکننده روی یک مسیر.
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"
}'
{
"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} ساخته نشده است. تنها راه دیدن نتیجه یک ارسال، پاسخ همان تماس یا گزارش پیامها در پنل است.
پاکت خطا
شکل پاکت این است:
{
"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 میآید.
مسیری که وجود ندارد این را میگیرد:
{
"error": {
"code": "unknown_endpoint",
"message": "no such endpoint: POST /v1/team/keys, see GET /v1/capabilities"
}
}
پاکت یک شکل ندارد
این مهمترین جمله این صفحه برای کسی است که مدیریت خطا مینویسد.
یازده تا از بیستودو مسیر، هندلر مشترک با پنل دارند، و آن هندلرها در پاکت پنل جواب میدهند: {"error":"<رشته>"} یا {"error":"<رشته>","code":"..."}. یعنی error گاهی شیء است و گاهی رشته.
| مسیر | چه شکستی | بدنه واقعی |
|---|---|---|
GET /v1/schema/events | 503 | {"error":"schema unavailable"} |
GET /v1/schema/traits | 503 | {"error":"schema unavailable"} |
POST /v1/audiences/validate | 400 JSON بد | {"error":"malformed JSON"} |
POST /v1/audiences/count | 400 و 503 | {"error":"segment: ..."} و {"error":"count unavailable"} |
GET /v1/segments | 503 | {"error":"segments unavailable"} |
GET /v1/segments/{id} | 400 و 404 | {"error":"invalid segment id"} و {"error":"segment not found"} |
GET /v1/campaigns | 503 | {"error":"campaigns unavailable"} |
GET /v1/campaigns/{id} | 400 و 404 | {"error":"invalid campaign id"} و {"error":"campaign not found"} |
POST /v1/reports/funnel | 400 و 503 | {"error":"<فارسی>","code":"invalid_report"} و {"error":"<فارسی>"} |
POST /v1/reports/retention | 400 و 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 و راستبهچپ را تحمل کند.
همه کدها
| کد | وضعیت | یعنی |
|---|---|---|
unauthenticated | 401 | اعتبارنامه نیست یا resolve نشد |
api_key_required | 401 | نشست فرستاده شده |
write_key_rejected | 401 | توکن wk_ فرستاده شده |
key_expired | 401 | کلید منقضی شده |
forbidden | 403 | مجوز نیست. need را بخوانید |
account_locked | 403 | قفل نرم. details.reason را بخوانید |
budget_exhausted | 429 | بودجه دقیقه تمام شده. صبر کنید و دوباره بزنید |
budget_unavailable | 503 | شمارنده بودجه در دسترس نیست. گذرا |
unknown_endpoint | 404 | مسیر یا متد اشتباه |
malformed_json | 400 | بدنه JSON نیست |
bad_id | 400 | شناسه مسیر عدد مثبت نیست |
not_found | 404 | سگمنت یا کمپین وجود ندارد |
filter_invalid | 422 | تعریف سگمنت رد شد |
name_required | 400 | نام سگمنت خالی است |
segment_unavailable | 503 | store سگمنت شکست خورد |
invalid_channel | 400 | کانال کمپین resolve نشد |
campaign_invalid | 422 | اعتبارسنجی کمپین رد شد |
campaign_unavailable | 503 | store کمپین شکست خورد |
not_submittable | 409 | کمپین draft یا paused نیست |
approval_required | 409 | تایید لازم است و نیست |
approval_stale | 409 | کمپین بعد از تایید عوض شده |
approval_unavailable | 503 | خواندن یا نوشتن تایید شکست خورد |
export_kind_invalid | 422 | kind در فهرست نیست |
export_unavailable | 503 | صف خروجی شکست خورد |
batch_empty | 400 | آرایه events خالی است |
batch_too_large | 413 | بیش از ۵۰۰ رویداد |
all_events_rejected | 422 | هیچ رویدادی قابل قبول نبود |
ingest_unavailable | 503 | صف ورود داده در دسترس نیست |
quota_cancelled | 402 | اشتراک سرویس نمیدهد |
quota_trial_over | 402 | دوره آزمایشی تمام شده |
quota_event_cap | 402 | سقف سخت رویداد |
quota_message_cap | 402 | تعریف شده و هرگز برگردانده نمیشود. تنها 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 و تغییرها را بخوانید.