דלג לתוכן הראשי

API של קמפיינים

יצירה, שיגור וניטור של קמפיינים באופן תכנותי.

כל נקודות הקצה בעמוד זה יחסיות לכתובת הבסיס: https://api-eu1.joryio.com/api - ראו סקירת API.

אימות​

כל הבקשות דורשות אימות באמצעות מפתח API:

Authorization: Bearer jry_live_your_api_key_here
Content-Type: application/json

המפתח חייב לשאת את ההרשאה (scope) שכל נקודת קצה דורשת:

הרשאהנקודות קצה
campaigns:readרשימה, שליפה, סטטיסטיקות, נמענים, גרסאות, היסטוריה
campaigns:writeיצירה, עדכון, שכפול, ארכוב, תיוג, שחזור גרסה
campaigns:sendשליחה, השהיה, חידוש, ביטול, ניסיון חוזר, שליחות בדיקה, שליחה טרנזקציונית
campaigns:deleteמחיקה ומחיקה באצווה

ראו מפתחות API לניהול הרשאות.


יצירת קמפיין​

נקודת קצה​

POST /campaigns

גוף הבקשה​

שדהסוגחובהתיאור
namestringכןשם הקמפיין (עד 255 תווים)
descriptionstringלאתיאור (עד 1000 תווים)
channelstringכןemail, sms, push, webhook, whatsapp, in_app או ai_optimized
variantsarrayכן*וריאנטים של ההודעה (חובה לכל ערוץ מלבד in_app)
targetingobjectלאקהל: userIds, filterGroups, excludeFilterGroups, filterOperator, subscriptionPreference
sendTypestringלאimmediate, scheduled, triggered, intelligent, recurring או ai_optimized
scheduledAtstringלאתאריך ISO 8601 עבור sendType: "scheduled"
scheduledTimezonestringלאאזור זמן IANA שבו מתפרש מועד התזמון (נשמר עם הקמפיין), או recipient_local: כל אדם בשעה של scheduledAt באזור הזמן שלו (של סביבת העבודה למי שאין לו); הקמפיין מתחיל כש-UTC+14 מגיע אליה. שמות לא מוכרים נדחים עם 400.
triggerConfigobjectלאכלל טריגר עבור sendType: "triggered"‏ - type, eventName, conditions, reEntry, cooldownHours
recurringScheduleobjectלאעבור sendType: "recurring"‏ - frequency‏ (daily/weekly/monthly/custom), cron, dayOfWeek, dayOfMonth, timeOfDay, timezone, endDate, maxOccurrences. endCondition‏ (never/date/after) שומר רק את הסיום שהוא מציין: never מסיר את endDate ואת maxOccurrences, date מסיר את maxOccurrences, ו-after מסיר את endDate ושומר maxOccurrences של 10 כשהוא חסר. בלי dayOfWeek‏ (0 = יום ראשון) תזמון שבועי רץ ביום שני; בלי dayOfMonth תזמון חודשי רץ ב-1 לחודש. ‏timeOfDay בפורמט HH:MM בשעון 24 שעות (09:00, 17:30); כל ערך אחר נדחה עם 400.
conversionTrackingobjectלאprimaryConversion / secondaryConversions (שם אירוע + תנאי מאפיינים), conversionWindowHours, attributionModel‏ (first_touch/last_touch/linear)
emailConfigIdstringלאזהות שולח אימייל שמורה (לערוץ האימייל)
subscriptionCategoryIdstringלאקטגוריית הסכמה (רשימת מנויים) שתחתיה הקמפיין נשלח
sendVolumeLimitobjectלאenabled, maxSends, cadence‏ (lifetime/per_send)

תוכן הודעת In-App​

כל וריאנט של In-App נושא את התוכן שנכתב עבורו ב־customContent. השדה mode קובע אילו שדות רלוונטיים:

modeשדותמוצג כ־
nativetitle, body, imageUrl, buttons, closeButton, backdropDismissible, styleהרכיבים של האפליקציה עצמה - ללא WebView
html (ברירת מחדל)html, css, maxWidthקוד שנכתב על ידי המחבר בתוך WebView
drag_drophtml, css, grapejsData, maxWidthכמו html; השדה grapejsData הוא מצב העורך הוויזואלי

אפשר להשמיט את mode, ואז המשמעות היא html.

maxWidth (מצבי HTML, בקמפיינים מסוג modal ו-slideup בלבד) הוא הרוחב המקסימלי של ההודעה בפיקסלים: מספר שלם בין 240 ל-1200. השמיטו אותו לברירת המחדל של הפריסה - 600 ל-Modal ו-400 ל-Slide-up. ה-SDK נותן להודעה את הקטן מבין maxWidth לבין רוחב המסך פחות שוליים לפני שהתוכן נפרס, כך ששאילתות המדיה של ההודעה רואות את הרוחב האמיתי שלה. ראו רוחב מקסימלי ו-HTML רספונסיבי.

{
"name": "Weekend offer",
"channel": "in_app",
"channelConfig": { "type": "modal", "triggers": [] },
"variants": [
{
"id": "v1",
"name": "Native",
"weight": 100,
"customContent": {
"mode": "native",
"title": "Weekend only",
"body": "Hi {{ firstName }}, members get 20% off through Sunday.",
"imageUrl": "https://cdn.example.com/weekend.png",
"buttons": [
{ "id": "cta", "text": "See offer", "action": "url", "url": "https://example.com/offer" },
{ "id": "later", "text": "Not now", "action": "dismiss" }
],
"closeButton": true,
"backdropDismissible": true,
"style": {
"backgroundColor": "#0A1240",
"textColor": "#FFFFFF",
"primaryButtonColor": "#00C8B7",
"cornerRadius": 18
}
}
}
]
}

שדות Native הם טקסט, לא markup. הם נשלחים לאפליקציה ללא escaping, כי האפליקציה מציגה אותם ב־View של טקסט - כך ש־A & B מגיע כ־A & B ולא כ־A & B. פרסונליזציה ב־Liquid פועלת ב־title, ב־body וב־text וב־url של הכפתורים.

buttons מוגבל ל־3. הערך action הוא אחד מ־dismiss, url או deep_link; עבור שני האחרונים נדרש url.

style - דריסות עיצוב אופציונליות​

כל שדה הוא אופציונלי, ושדה חסר משמעו ירושה - צבע הרקע של האפליקציה, צבע הטקסט שלה, צבע ההדגשה והגופן שלה. הירושה הזו היא כל העניין בתוכן Native, לכן שלחו שדה רק כשהקמפיין באמת זקוק לו.

שדהטיפוסחל עלמשמעות
backgroundColorstringweb, iOS, Androidרקע הכרטיס
textColorstringweb, iOS, Androidכותרת וגוף ההודעה (הגוף מעט מרוכך)
primaryButtonColorstringweb, iOS, Androidצבע הכפתור הראשון
primaryButtonTextColorstringweb, iOS, Androidהטקסט שעליו. אם לא נשלח, נבחר שחור או לבן לפי הניגודיות מול צבע הכפתור
cornerRadiusnumberweb, iOS, Android0-48. לא חל על fullscreen, שם פינות מעוגלות היו חושפות את האפליקציה בפינות
fontSizenumberweb, iOS, Android10-32. גודל גוף ההודעה; הכותרת נגזרת ממנו. בנייד עדיין מוחלת גם הגדרת גודל הטקסט של המשתמש
titleWeightstringweb, iOS, Android‏regular, medium, semibold, bold. לכותרת בלבד - גוף ההודעה נשאר רגיל לקריאוּת
textAlignstringweb, iOS, Android‏auto (ברירת מחדל), start, center, end. ‏auto הולך לפי שפת ההודעה עצמה, כך שעברית וערבית נקראות מימין לשמאל גם באפליקציה באנגלית
fontFamilystringweb; בנייד מיטביבנייד חל רק אם האפליקציה ארזה את הגופן (iOS: רשום באפליקציה; Android: ‏res/font או משפחה מערכתית). אם הוא חסר, נשמר הגופן של האפליקציה במקום תחליף
customCssstringweb בלבדCSS בכתיבה ידנית, עד 20000 תווים. ה-SDK כותב מחדש כל סלקטור כך שיחול רק בתוך ההודעה לפני ההזרקה, ולכן כלל לא יכול להגיע לעמוד המארח, ו-@import מוסר. בטלפונים אין מנוע CSS והם מתעלמים ממנו

צבעים מועברים כפי שנכתבו - hex, ‏rgb() או מילת מפתח של CSS. ערך שהרנדרר לא מצליח לפרש חוזר לערך שנורש, במקום להכשיל את ההודעה.

ב-customCss אפשר לכוון לכרטיס עצמו, ל-h2, ל-p, ל-button.primary ול-button.secondary, והשדות שלמעלה נחשפים גם כמשתני CSS: --joryio-inapp-bg, --joryio-inapp-fg, --joryio-inapp-primary, --joryio-inapp-primary-fg, --joryio-inapp-radius ו---joryio-inapp-font.

תוכן HTML מחייב הסכמה מפורשת של האפליקציה. ערכות ה־SDK למובייל ולווב לא יציגו הודעת HTML אלא אם האפליקציה מגדירה allowHtmlJsInAppMessages באתחול, כי הודעה כזו מוצגת ב־WebView בתוך האפליקציה. סקריפטים שנכתבו בהודעה לא רצים שם; הגשר של ה־SDK עצמו מטפל בכפתורים ובטפסים שלה. תוכן Native מוצג תמיד. ראו את המדריכים ל־Android, ל־iOS ול־Web.

| sendRateLimit | object | לא | enabled, maxPerMinute | | quietTimeOverride | object | לא | דריסת שעות שקט ברמת הקמפיין | | utmSettings | object | לא | עקיפה ברמת הקמפיין להגדרות UTM ולתיוג קישורים | | channelConfig | object | לא | הגדרות ספציפיות לערוץ (המבנה תלוי בערוץ) | | stoConfig / abTestConfig | object | לא | הגדרות אופטימיזציית זמן שליחה / בדיקת A/B | | resendPolicy | string | לא | מדיניות השליחה החוזרת של Send again בקמפיין חד־פעמי: only_new (ברירת מחדל), everyone או cooldown | | resendCooldownDays | number | לא | חלון זמן (בימים) עבור resendPolicy: "cooldown"; עם cooldown, ערך חסר או 0 נשמר כ-30 | | tags | string[] | לא | תגיות | | status | string | לא | draft, scheduled, active, paused, completed או cancelled |

מבנה וריאנט​

כל פריט ב-variants:

שדהסוגחובהתיאור
idstringכןמזהה הווריאנט
namestringכןשם הווריאנט
weightnumberכןנתח תעבורה, 0–100 (סכום המשקלים חייב להתאים לכללי הערוץ)
messageobjectלאהודעת הערוץ. אימייל: subject, preheader, from, fromName, html או templateId, text. SMS‏: body, from, shortenLinks; בחשבון Delivyr (תבניות מאושרות בלבד) smsTemplateId ו-variableMapping במקום body - ראו תבניות SMS. פוש: title, body, icon, image, data. וואטסאפ: messageType‏ (template/reply), templateId, wabaId, variableMapping, replyText. Webhook‏: url, method, headers, body, auth, bodyType
isControlGroupbooleanלאמסמן את הווריאנט כקבוצת ביקורת שאינה נכללת בשליחה (ומאפשר למדוד שיפור יחסי)

דוגמת בקשה​

curl -X POST https://api-eu1.joryio.com/api/campaigns \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "July Newsletter",
"channel": "email",
"sendType": "scheduled",
"scheduledAt": "2026-07-20T10:00:00.000Z",
"scheduledTimezone": "America/New_York",
"variants": [
{
"id": "variant-a",
"name": "Variant A",
"weight": 100,
"message": {
"subject": "Your July update",
"from": "[email protected]",
"fromName": "Example",
"html": "<h1>Hello {{ user.firstName }}</h1>"
}
}
],
"targeting": {
"filterGroups": [
{
"filters": [
{ "type": "attribute", "field": "plan", "operator": "equals", "value": "premium" }
],
"operator": "AND"
}
]
}
}'

תגובה​

הפעולה מחזירה את אובייקט הקמפיין שנוצר:

{
"id": "8f14e45f-ceea-467f-a11d-2f4b6a1c9e3b",
"name": "July Newsletter",
"channel": "email",
"status": "scheduled",
"sendType": "scheduled",
"scheduledAt": "2026-07-20T14:00:00.000Z",
"variants": [ ... ],
"targeting": { ... },
"tags": [],
"createdAt": "2026-07-12T09:00:00.000Z",
"updatedAt": "2026-07-12T09:00:00.000Z"
}

ביצועי קמפיינים​

אילו קמפיינים באמת גרמו להמרות, נמדד מול קבוצת הביקורת של כל קמפיין - אותם נמענים שבמכוון לא נשלחה אליהם הודעה.

מדורג לפי המרות נוספות, לא לפי הכנסה, כדי שיהיה משמעותי גם לסביבת עבודה שממטבת יעד הרשמה או הפעלה ולא הזמנות. הכנסה מוצגת לצד זה היכן שיש הזמנות.

Endpoint​

GET /campaigns/performance

פרמטרי שאילתה​

פרמטרסוגברירת מחדלתיאור
sinceDaysnumber90טווח לאחור לבחירת הקמפיינים הנכללים

תגובה​

שדהסוגתיאור
rankedarrayקמפיינים שהמדידה שלהם עברה את סף המובהקות, הטובים ביותר תחילה
stillCollectingarrayנמדדו אך טרם הוכרעו. רשימה נפרדת במכוון - אין להם פסק דין, ומיקום מתחת למפסידים היה אומר שהם הפסידו
noHoldoutnumberקמפיינים ללא קבוצת ביקורת. לא ניתן למדוד את השפעתם, ולכן הם נספרים ולא מוצגים
noGoalnumberקמפיינים עם קבוצת ביקורת אבל בלי יעד המרה. אין מה להשוות, ולכן הם נספרים ולא מוצגים
sinceDaysnumberהטווח שהוחל

מה נחשב כאן המרה. כל שורה סופרת את יעד ההמרה של הקמפיין עצמו (conversionGoal, שם היעד, למשל email_clicked) - אותו שיפור שמוצג בדף הקמפיין: שתי הקבוצות נבדקות אם האדם הגיע ליעד בתוך חלון ההמרה של הקמפיין אחרי שנשלחה אליו הודעה (או הייתה נשלחת), בלי מודל ייחוס, כי לקבוצת הביקורת אין הודעה לזקוף לה. incrementalRevenue מגיע מהזמנות - הכנסות רכישה בחלון של כל קבוצה - בלי קשר ליעד. עד 2026-10-14 השדות incrementalConversions, conversionRateLift ו-significance ספרו קונים לכל קמפיין, בלי קשר ליעד שלו.

המספרים אינם מסתכמים​

במכוון אין סך הכול. כל קמפיין נמדד מול קבוצת ביקורת שעדיין קיבלה את כל השאר ששלחתם, ולכן ההשפעות חופפות - סכימת השורות תספור את אותה המרה תחת יותר מקמפיין אחד. התגובה כוללת totalsAreNotSummable: true כדי שהמבנה עצמו יאמר זאת.

מדוע המובהקות חשובה כאן​

קמפיין עם קבוצת ביקורת קטנה עלול להציג עלייה עצומה בגלל רכישה של אדם אחד. רק קמפיינים עם לפחות minConvertersPerArm המרות בכל אחת מהקבוצות מדורגים; השאר מוחזרים תחת stillCollecting ללא מספר. המבחן מתבצע על שיעור ההמרה, שהוא פרופורציה וניתן לבדיקה תקפה; הכנסה לנמען היא בעלת זנב כבד ומדווחת לצד זה כהקשר ולא כהוכחה.

curl -X GET "https://api-eu1.joryio.com/api/campaigns/performance?sinceDays=90" \
-H "Authorization: Bearer YOUR_API_KEY"

רשימת קמפיינים​

נקודת קצה​

GET /campaigns

פרמטרי שאילתה​

פרמטרסוגברירת מחדלתיאור
statusstring-סינון לפי סטטוס (מופרד בפסיקים לריבוי ערכים)
channelstring-סינון לפי ערוץ (מופרד בפסיקים)
tagsstring-סינון לפי תגיות (מופרד בפסיקים)
qstring-חיפוש טקסט חופשי
createdBy / editedBystring-סינון לפי יוצר / עורך אחרון (מזהי משתמשים מופרדים בפסיקים)
createdFromstring-רק קמפיינים שנוצרו בתאריך ISO זה או אחריו
pagenumber1מספר עמוד
limitnumber20תוצאות לעמוד (לכל היותר 100)

דוגמת בקשה​

curl -X GET "https://api-eu1.joryio.com/api/campaigns?status=active&channel=email&limit=50" \
-H "Authorization: Bearer jry_live_your_api_key"

תגובה​

{
"data": [
{ "id": "8f14e45f-ceea-467f-a11d-2f4b6a1c9e3b", "name": "July Newsletter", "channel": "email", "status": "active" }
],
"pagination": {
"total": 23,
"page": 1,
"limit": 50,
"offset": 0,
"totalPages": 1,
"hasMore": false
}
}

שליפת קמפיין​

GET /campaigns/:campaignId
curl -X GET https://api-eu1.joryio.com/api/campaigns/8f14e45f-ceea-467f-a11d-2f4b6a1c9e3b \
-H "Authorization: Bearer jry_live_your_api_key"

מחזירה את אובייקט הקמפיין המלא. סטטוס הקמפיין הוא אחד מ: draft, scheduled, active, paused, completed, cancelled, archived, failed (שגיאת שליחה קבועה - ראו failureReason).


עדכון קמפיין​

PUT /campaigns/:campaignId

הגוף מקבל את אותם שדות כמו יצירת קמפיין - כולם אופציונליים; רק השדות שנשלחו מתעדכנים.

curl -X PUT https://api-eu1.joryio.com/api/campaigns/8f14e45f-ceea-467f-a11d-2f4b6a1c9e3b \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "name": "July Newsletter v2" }'
עריכת קמפיין פעיל

קמפיינים מבוססי טריגר וקמפיינים חוזרים נשארים active לאורך כל חייהם. עריכת קמפיין active אינה משנה את מה שנשלח כעת - העריכות נשמרות כטיוטה ממתינה. קראו ל־POST /campaigns/:campaignId/publish כדי להחיל אותן באופן אטומי על הקמפיין הפעיל, או ל־POST /campaigns/:campaignId/discard-draft כדי לבטל אותן. קמפיינים במצב טיוטה מתעדכנים במקום, ללא שלב פרסום.


מחיקת קמפיין​

DELETE /campaigns/:campaignId

מחזירה 204 No Content. רק טיוטות שמעולם לא רצו נמחקות סופית; קמפיינים עם היסטוריית שליחה עדיף לארכב (POST /campaigns/:campaignId/archive) - הארכוב עוצר את השליחה ומשמר את האנליטיקה.


מחזור חיים של קמפיין​

שיטהנתיבתיאור
POST/campaigns/:campaignId/sendשיגור הקמפיין (מתחיל שליחה / מפעיל קמפיין מבוסס-טריגר)
POST/campaigns/:campaignId/pauseהשהיית קמפיין רץ
POST/campaigns/:campaignId/resumeחידוש קמפיין מושהה
POST/campaigns/:campaignId/publishהחלת עריכות שנשמרו במאגר על קמפיין פעיל באופן אטומי (400 אם אין עריכות ממתינות)
POST/campaigns/:campaignId/discard-draftביטול עריכות שנשמרו במאגר עבור קמפיין פעיל (ללא פעולה אם אין)
POST/campaigns/:campaignId/cancelביטול קמפיין
POST/campaigns/:campaignId/resendשליחה חוזרת ("שלח שוב") של קמפיין חד-פעמי שהושלם, לפי resendPolicy שלו
POST/campaigns/:campaignId/retryניסיון חוזר לקמפיין failed (מחזיר אותו ל-scheduled)
POST/campaigns/:campaignId/preview-launchסיכום טרום-שיגור (גודל קהל, בדיקות) ללא שליחה
POST/campaigns/:campaignId/duplicateשכפול קמפיין כטיוטה חדשה: כל ההגדרות מועתקות (שולח, אזור הזמן של התזמון, תזמון חוזר, טריגר, הגדרות A/B), מצב הריצה מתאפס (סטטוס, נתונים, אישורים, scheduledAt). השדה duplicatedFrom של העותק מכיל את campaignId, name, scheduledAt, scheduledTimezone ו-scheduledLocalAt של המקור
POST/campaigns/:campaignId/archiveארכוב (עוצר שליחה, שומר היסטוריה)
POST/campaigns/:campaignId/unarchiveשחזור למצב לא-שולח (יש להפעיל מחדש במפורש כדי לשלוח שוב)
POST/campaigns/:campaignId/stop-recurringעצירת מופעים עתידיים של קמפיין מחזורי
curl -X POST https://api-eu1.joryio.com/api/campaigns/8f14e45f-ceea-467f-a11d-2f4b6a1c9e3b/send \
-H "Authorization: Bearer jry_live_your_api_key"
שליחה חוזרת (מדיניות שליחה חוזרת)

קמפיין חד-פעמי שהושלם ניתן לשליחה חוזרת עם POST /campaigns/:campaignId/resend. מי מקבל אותו נקבע לפי resendPolicy של הקמפיין:

  • only_new (ברירת מחדל) - מדלג על מי שכבר קיבל את הקמפיין; רק משתמשים שטרם קיבלו אותו נכללים.
  • everyone - שליחה חוזרת לכל הקהל, כולל מי שכבר קיבל.
  • cooldown - שליחה חוזרת לכולם חוץ ממי שקיבל הודעה ב-resendCooldownDays הימים האחרונים.

כללי הסכמה וחסימה נאכפים תמיד. מגבלות תדירות פועלות לפי הדגל ignoreTouchingRules של הקמפיין. קמפיינים מבוססי טריגר משתמשים ב־triggerConfig.reEntry; קמפיינים מחזוריים נשלחים שוב לפי לוח הזמנים. הפעולה מחזירה 400 עבור סוגים אלה, עבור in-app או עבור קמפיין שטרם סיים לשלוח.


סטטיסטיקות קמפיין​

GET /campaigns/:campaignId/stats

פרמטרי שאילתה: startDate, endDate (אופציונליים). ערך של תאריך בלבד (2026-07-20) מציין את כל היום לפי אזור הזמן של סביבת העבודה; ערך ISO מלא עם שעה נלקח כרגע עצמו, וזה מה שהופך את חלונות "השעה / 6 השעות / 24 השעות הראשונות" בעמוד הקמפיין לאמיתיים.

תגובה​

{
"campaignId": "8f14e45f-ceea-467f-a11d-2f4b6a1c9e3b",
"name": "July Newsletter",
"channel": "email",
"status": "completed",
"stats": {
"queued": 1200,
"recipients": 1180,
"sent": 1180,
"delivered": 1150,
"failed": 30,
"opened": 640,
"uniqueHumanOpened": 590,
"clicked": 210,
"uniqueHumanClicked": 180,
"bounced": 12,
"hardBounced": 9,
"softBounced": 3,
"otherBounced": 0,
"autoReplied": 7,
"unsubscribed": 4,
"deliveryRate": 97.46,
"openRate": 51.3,
"clickRate": 15.65,
"clickToOpenRate": 30.51,
"rateBasis": "recipients",
"rateOf": "delivered",
"messageOpens": null,
"messageClicks": null
},
"conversionStats": { ... },
"revenue": { ... },
"uplift": null,
"storeConnected": false,
"conversionTracking": { ... },
"startedAt": "2026-07-20T14:00:00.000Z",
"completedAt": "2026-07-20T14:12:00.000Z",
"createdAt": "2026-07-12T09:00:00.000Z"
}

storeConnected הוא true כשלסביבת העבודה מחוברת אינטגרציית חנות (Shopify, WooCommerce או Magento), false כשאין כזו, ו-null כשלא ניתן היה לקבוע זאת.

conversionStats ו-revenue מאוכלסים כאשר מוגדר מעקב המרות; uplift מאוכלס רק כאשר וריאנט מסומן isControlGroup.

שני דברים שכדאי לקרוא בעיון ב-stats:

  • שיעור הוא null, לעולם לא 0, כשהערוץ אינו יכול למדוד אותו. openRate הוא null ב-sms, push, in_app, webhook ו-wallet; clickRate ו-clickToOpenRate הם null ב-webhook. null פירושו "לא נמדד", וזו טענה שונה מ"אף אחד לא פתח" - הציגו אותו ככזה.
  • rateBasis מציין את המכנה. "recipients" (קמפיין שנשלח פעם אחת) מחלק אנשים ייחודיים במספר ההודעות שנמסרו. "messages" (חוזר, מופעל־טריגר או נשלח שוב - כל מה שנשלח יותר מפעם אחת) הוא השיעור לכל הודעה, משוקלל לפי גודל השליחה, ו-messageOpens / messageClicks נושאים את המונים שלו. תחת "recipients" השניים האלה הם null ו-uniqueHumanOpened / uniqueHumanClicked הם הספירות הכנות.
  • rateOf מציין מתוך מה מחושבים openRate ו-clickRate (נוסף באוקטובר 2026; באימייל השיעורים לא השתנו). כל שיעור פתיחה והקלקה הוא אנשים ייחודיים מתוך delivered באימייל, SMS, WhatsApp ו-Viber; מתוך sent ב-Push, שבו אישורי המסירה מגיעים מהמכשיר וחסרים ב-iOS, ובכל קמפיין שעוד לא דווחה בו מסירה. לוח הבקרה, עמוד הקמפיין וה-API הזה משתמשים באותו כלל, כך שלקמפיין אחד יש שיעור אחד בכל מקום - ראו איך מחושבים שיעורי הפתיחה וההקלקה.
  • hardBounced, softBounced ו-otherBounced מפצלים את bounced (נוסף באוקטובר 2026): הקפצות עם bounceType קשה, רך, וללא סוג רשום, שנספרות באותו כלל כמו bounced, כך ששלושתם מסתכמים אליו. autoReplied הוא מספר ההודעות שנענו במענה "מחוץ למשרד" או במענה אוטומטי אחר: דואר שנמסר, ואף פעם לא חלק מ-bounced או משיעור הקפצות. הערך הוא null כשטיפול בדואר חוזר כבוי ולקמפיין אין מענים כאלה - לא נמדד, וזו טענה שונה מ-0; ספירה שנרשמה בזמן שהטיפול היה מופעל עדיין מוחזרת.

נקודות קצה נוספות לסטטיסטיקות​

שיטהנתיבתיאור
GET/campaigns/:campaignId/variant-statsסטטיסטיקות A/B לפי וריאנט עם מובהקות סטטיסטית (startDate/endDate)
GET/campaigns/:campaignId/linksסטטיסטיקות הקלקה על קישורים (startDate/endDate) - בקמפיין In-App השורות הן הקליקים בשמותיהם מתוך ההודעה (action, url = הכתובת שנלחצה או השם) לפי גרסה
GET/campaigns/:campaignId/failure-reasonsכשלי מסירה מקובצים לפי קוד שגיאת DLR ([{ code, reason, count }])
GET/campaigns/:campaignId/skip-reasonsפירוט לא נשלח: אנשים שהשליחה בכוונה לא שלחה אליהם, שורה לכל אדם לפי סיבת הדילוג האחרונה ({ total, reasons: [{ reason, count }] }; סיבות כמו control_group, global_control, frequency_cap, channel_unsubscribed, quiet_time, missing_contact). את האנשים מאחורי סיבה מסוימת מקבלים עם /campaigns/:campaignId/recipients?skipReason=<reason>
GET/campaigns/:campaignId/unsubscribe-reasonsהסרות לפי סיבה: שורה לכל אדם שהסיר את עצמו דרך הקישורים של הקמפיין, לפי הסיבה בהסרה האחרונה שלו, אחרי חיתוך רווחים והמרה לאותיות קטנות ({ total, reasons: [{ reason, count }], others }). 8 הסיבות המובילות מוצגות והשאר מסוכמות ב-others; reason: null פירושו שלא ניתנה סיבה. הסרות שנרשמו לפני שהסיבה נשמרה באירוע האנליטיקה (אוקטובר 2026) נספרות כ-null
GET/campaigns/:campaignId/time-to-engageכמה זמן לקח לנמענים לפתוח או ללחוץ, בקבוצות - נמדד לכל נמען מרגע השליחה שלו, ולכן רלוונטי לקמפיינים מבוססי טריגר
GET/campaigns/:campaignId/conversion-timingמתי התרחשו ההמרות המיוחסות (granularity = day או hour; startDate/endDate מגבילים את השליחה כמו ב-/stats). אותו יעד, חלון, מודל ושאילתה כמו conversionStats, מפוצלים לפי היום או השעה של ההמרה באזור הזמן של סביבת העבודה; ה-buckets מסתכמים למספר ההמרות ש-/stats מחזיר לאותו טווח. מחזיר { configured, granularity, total, buckets: [{ date, conversions }], majorityWithinHours } - majorityWithinHours הוא מספר השעות מהשליחה המיוחסת שבתוכן המירו יותר ממחצית; configured: false כשלקמפיין אין יעד המרה
GET/campaigns/:campaignId/engagement-timingשליחות, פתיחות וקליקים לכל day או hour (granularity) באזור הזמן של סביבת העבודה, לטווח האירועים startDate/endDate כמו ב-/stats. נספרים לפי הכללים של /stats: count=people (ברירת מחדל) שם כל אדם פעם אחת בדלי של הפתיחה / הקליק הראשונים שלו, כך שהדליים מסתכמים בפתיחות / בקליקים הייחודיים; count=events סופר כל פתיחה וכל קליק. includeBots=true מוסיף פתיחות / קליקים של בוטים וסורקים (כברירת מחדל הם לא נספרים). מפתחות הדליים זהים ל-conversion-timing. מחזיר { granularity, count, includeBots, buckets: [{ date, sent, opened, clicked }], totals: { sent, opened, clicked } }
GET/campaigns/:campaignId/recovered-cartsהעגלות הנטושות שהקמפיין שחזר - הרשימה שמאחורי revenue.recoveredCarts ב-/stats, מאותה שאילתה, כך ש-total שווה לספירה הזו לאותם startDate/endDate (עגלות ששוחזרו בטווח; סוף בתאריך בלבד כולל את כל היום, לפי אזור הזמן של סביבת העבודה). limit ‏1-100 (ברירת מחדל 25), offset; מהחדשה לישנה. שורות: orderNumber (של החנות), placedAt, recoveredAt, match (recovery_link, order = אותה עגלה, reported), cartValue/cartCurrency, creditedRevenue/creditedCurrency (אחרי החזרים), contact (בעל העגלה) ו-buyer (רק כשההזמנה נשמרה תחת מישהו אחר); שמות אנשי קשר מוסתרים בלי pii:view. מחזיר { data, total, limit, offset, summary }
GET/campaigns/:campaignId/send-occurrencesפילוח לפי שליחה עבור קמפיין חוזר, לפי תאריך השליחה - מעורבות משויכת לשליחה שקדמה לה
GET/campaigns/:campaignId/recipientsנמענים בעימוד, שורה אחת לכל אדם עם השלב הרחוק ביותר שהגיע אליו (furthestStatus). סינון לפי status (sent, delivered, opened, clicked, failed, bounced, unsubscribed, skipped); עימוד עם limit (עד 200) ו-offset. מיון עם sortBy - reached (השלב הרחוק ביותר), lastActivity או events - ו-sortOrder (asc / desc, ברירת מחדל desc); בלי sortBy האירוע הראשון החדש ביותר מוביל, וכל מיון מסתיים במזהה האדם כך שעמודים לעולם לא חוזרים על אדם או מדלגים עליו. includeBots=true סופר פתיחות ולחיצות של בוטים, סורקים ומכונות כשל האדם; כברירת מחדל הן לא נספרות בשלב וב-eventCount, אותו כלל כמו שיעורי הפתיחה וההקלקה
GET/campaigns/:campaignId/recipients/:userIdציר זמן של אירועי ההודעות של משתמש בקמפיין זה. בקמפיין webhook מוחזר גם webhookResponse ({ status, contentType, body, truncated, at }) כשלקריאה האחרונה שנכשלה לאדם זה (לא 2xx) נשמרה תגובה: מוסתרת, 2 KB הראשונים של הגוף, נשמרת 30 יום. מוחזר רק למי שמורשה לראות מידע אישי (pii:view; מפתח API רק עם הרשאה זו). לא קיים כשלא נשמר דבר או כשצוות Joryio כיבה את שמירת התגובות עבור הארגון
GET/campaigns/:campaignId/in-app-statsסטטיסטיקות תצוגה של קמפיין in-app. כולל את displayFrequency - התפלגות החשיפות לכל משתמש בצורת { tailBucket, buckets: [{ displays, users, impressions }], maxPerUser }. ספירות בגובה tailBucket ומעלה מקובצות לדלי אחד, ולכן displays === tailBucket פירושו "כמות זו או יותר"; לחישוב ממוצע יש להשתמש ב-impressions של כל דלי (ולא ב-displays * users), ו-maxPerUser הוא המשתמש שנחשף הכי הרבה פעמים
GET/campaigns/:campaignId/impressionsרשימת חשיפות in-app‏ (limit, offset, startDate, endDate)

שליחת הודעה טרנזקציונית​

שליחת הודעה חד-פעמית למשתמש בודד ללא יצירת קמפיין.

POST /campaigns/transactional/send

גוף הבקשה​

שדהסוגחובהתיאור
userIdstringכןמזהה המשתמש היעד
channelstringכןemail, sms או push
messageobjectכןsubject (אימייל), body (טקסט), html (אימייל)
triggerDataobjectלאהקשר זמין לפרסונליזציה (type, name, properties, metadata)
idempotencyKeystringלאמפתח למניעת כפילויות שסיפק הקורא - ניסיון חוזר עם אותו מפתח אינו נשלח פעמיים
curl -X POST https://api-eu1.joryio.com/api/campaigns/transactional/send \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"userId": "user_123",
"channel": "email",
"message": {
"subject": "Your receipt",
"html": "<p>Thanks for your order, {{ user.firstName }}.</p>"
},
"idempotencyKey": "order-98421-receipt"
}'

נקודות קצה נוספות​

שיטהנתיבתיאור
POST/campaigns/previewתצוגה מקדימה של משתמשים שתואמים לקריטריוני קהל היעד (גוף: targeting, ולפי הצורך limit עד 500, ‏channel, ‏webAppId, ‏platforms בפוש - ה-channelConfig.platforms של הקמפיין; ‏targeting.tags הן התגיות של הקמפיין, שקובעות פטורים מרשימות חסימה כמו בזמן השליחה)
POST/campaigns/spam-checkבדיקת ספאם לתוכן אימייל לפני שליחה
GET/campaigns/:campaignId/versionsרשימת תמונות מצב שמורות של גרסאות
GET/campaigns/:campaignId/versions/compare?v1=&v2=השוואת שתי גרסאות
GET/campaigns/:campaignId/versions/:versionIdשליפת תמונת מצב של גרסה
POST/campaigns/:campaignId/versions/:versionId/rollbackשחזור לגרסה
GET/campaigns/:campaignId/historyהיסטוריית יומן ביקורת (limit, עד 200)
POST/campaigns/bulk-delete / bulk-duplicate / bulk-archive / bulk-unarchiveפעולות באצווה; גוף { "ids": [...] }, מחזיר { succeeded, failed }
POST/campaigns/bulk-tagתיוג באצווה; גוף { "ids": [...], "tags": [...] }
POST/campaigns/:campaignId/send-test-whatsapp / send-test-sms / send-test-pushשליחות בדיקה לטלפון/משתמש לפני שיגור
POST/campaigns/:campaignId/render-for-contactמרנדר את הקמפיין עבור איש קשר אחד ומחזיר { html, css }. לא שולח דבר - in-app מבוסס משיכה, ולכן אין מכשיר לשלוח אליו. מחליף את send-test-in-app, שעדיין עובד
GET/campaigns/:campaignId/sto-coverageכיסוי אופטימיזציית זמן שליחה עבור הקהל
GET/campaigns/:campaignId/recurring-statusסטטוס קמפיין מחזורי
POST/campaigns/:campaignId/retest-abאיפוס המנצח בבדיקת A/B בקמפיין מחזורי עם וריאנט מנצח
POST/campaigns/:campaignId/launch-rlשיגור במצב AI-optimized (RL)
GET/campaigns/:campaignId/rl-statsסטטיסטיקות לוח הבקרה לקמפיין AI-optimized
POST/campaigns/:campaignId/pause-rl / resume-rlהשהיה / חידוש של קמפיין AI-optimized
POST/campaigns/ml-path-warmthסטטוס חימום פרסונליזציה עבור מזהי נתיבים/וריאנטים

בדיקת שיגור​

משטח הבדיקה שלפני השליחה (ראו את מדריך בדיקת השיגור). אותה בדיקה רצה בתוך השליחה, כך שמה שמוחזר כאן הוא מה שהשליחה נשפטת לפיו.

GET /campaigns/{campaignId}/launch-review

הרשאה: campaigns:read.

{
"rulesVersion": "lr1",
"checkedAt": "2026-09-21T10:00:00.000Z",
"requireClean": true,
"blockers": [
{
"code": "merge_field_broken",
"severity": "blocker",
"scope": { "kind": "variant", "id": "v1", "name": "Variant A", "field": "subject" },
"params": { "field": "subject", "error": "output \"{{ user.firstName }\" not closed" },
"fix": { "action": "edit_content", "variantId": "v1", "field": "subject" }
}
],
"recommendations": [
{ "code": "subject_long", "severity": "recommendation", "scope": { "kind": "variant", "id": "v1", "name": "Variant A", "field": "subject" }, "params": { "length": 92, "max": 78 }, "fix": { "action": "edit_content", "variantId": "v1", "field": "subject" } }
],
"skipped": []
}

קודים: חוסמים unsubscribe_missing, merge_field_broken, sender_not_verified, sender_not_found, link_unusable (חוסם רק בשליחה עם יעד המרה); המלצות subject_long, subject_discount_words, subject_hype_adjective, preheader_repeats_subject, experiment_opportunity, plain_text_missing, audience_overlap, at_message_limit (אנשים במגבלת התדירות שלהם: params.count, sampled, sharePct, bypassed, partial), quiet_hours_now (אנשים בתוך חלון שקט ברגע השליחה: params.count, sampled, sharePct, behaviour, until, evaluatedAt, at, partial), quiet_hours_ignored, consent_bypassed, node_unreachable, ו-model_note (הערה מביקורת ה-AI; params.title ו-params.detail). התגובה נושאת גם ai: { status: 'ok' | 'skipped' | 'off', reason?, version, cached? } - מעבר ה-AI רץ רק בנתיב הזה, לעולם לא בתוך שער השליחה, ומעבר שנכשל הוא סיבת skipped, לא שגיאה. fix.action הוא אחד מ-edit_content, edit_audience, edit_delivery, edit_node, choose_sender, verify_domain, open_subscription_settings, open_quiet_time, open_frequency_rules, edit_journey.

כל ממצא נושא key, יציב בין בדיקות (model_note מזוהה לפי ההיקף והכותרת שלו). כשממצא מצביע על משהו מדויק הוא נושא גם locator: images - כל תמונה כ-{ src, index, bytes?, id? }, כש-src הוא ערך המאפיין המלא (ה-params.src של המשפט נחתך ל-120 תווים), index מקומה בין תמונות התוכן של המייל, bytes המשקל שנמדד; words (מילים שקול המותג נמנע מהן ושההודעה משתמשת בהן) עם fields (subject, preheader, html) שבהם הן מופיעות; או text, הבלוק שממצא עיצוב עוסק בו. model_note שעוסק בחלק אחד של ההודעה מציין אותו ב-scope.field וב-fix.field. הצעות AI (model_note, experiment_opportunity) שמישהו הסיר מהרשימה בלוח הבקרה לא מופיעות בתגובה הזו; שום דבר אחר לא ניתן להסרה. גם model_note שהעדפות הביקורת של סביבת העבודה מכסות (נוצרות ב"לא להציע את זה שוב"; רשומות בהגדרות → בדיקת שיגור → העדפות ביקורת) לא מופיע, ו-model_note שכבר הוסר מהרשימה בשלושה קמפיינים או מסעות שונים או יותר ב-90 הימים האחרונים נושא params.dismissedOn, המספר הזה. ראו בדיקת שיגור.

requireClean משקף את הגדרת סביבת העבודה settings.launchReview.requireClean (ברירת מחדל true). כשהוא true ו-blockers לא ריק, POST /campaigns/{campaignId}/send עונה:

{ "statusCode": 409, "code": "launch_review_blocked", "message": "Launch Review: 1 blocker: merge_field_broken (Variant A: subject)", "blockers": [ ... ] }

קמפיין מתוזמן שנדחה בזמן השליחה נוחת בסטטוס failed עם ההודעה הזו כ-failureReason.

תגובות שגיאה​

כל השגיאות משתמשות במבנה הסטנדרטי - ראו תגובת שגיאה בסקירת ה־API למבנה המלא ולרשימת קודי המצב.

{
"statusCode": 404,
"message": "Campaign not found",
"timestamp": "2026-07-12T09:00:00.000Z",
"path": "/campaigns/8f14e45f-ceea-467f-a11d-2f4b6a1c9e3b"
}
סטטוסמתי
400התיקוף נכשל (למשל channel לא חוקי או weight חסר בווריאנט) - הגוף כולל מערך errors עם הודעה לכל שדה שנכשל
401מפתח API חסר או לא תקין
403למפתח ה-API חסרה הרשאת campaigns:* הנדרשת
404הקמפיין לא נמצא בסביבת העבודה
409התנגשות במחזור החיים - למשל חידוש קמפיין שכבר אינו מושהה ("Campaign is no longer paused.") או שיגור קמפיין שכבר אינו במצב שניתן לשלוח ממנו

קישורים קשורים​

מפתח API אינו יכול לאשר

אישור מחייב משתמש מחובר. האישור מתעד מי בדק את הקמפיין, והפרדת התפקידים נקבעת בהשוואת המאשר למגיש - שניהם בלתי אפשריים עבור מפתח. בקשה עם מפתח API נדחית עם 403.