تخطّ إلى المحتوى الرئيسي

Campaigns API

أنشئ الحملات وأطلقها وراقبها برمجياً.

كل نقاط النهاية في هذه الصفحة نسبية إلى عنوان الأساس: https://api-eu1.joryio.com/api. راجع نظرة عامة على API.

المصادقة​

تتطلب جميع الطلبات مصادقة بمفتاح API:

Authorization: Bearer jry_live_your_api_key_here
Content-Type: application/json

يجب أن يحمل مفتاحك النطاق المطلوب لكل نقطة نهاية:

النطاقنقاط النهاية
campaigns:readالعرض والقائمة والإحصاءات والمستلمون والإصدارات والسجل
campaigns:writeالإنشاء والتحديث والتكرار والأرشفة والوسوم واسترجاع الإصدارات
campaigns:sendالإرسال والإيقاف والاستئناف والإلغاء وإعادة المحاولة وإرسال الاختبارات والإرسال المعاملي
campaigns:deleteالحذف والحذف الجماعي

راجع مفاتيح API لإدارة النطاقات.


إنشاء حملة​

نقطة النهاية​

POST /campaigns

نص الطلب​

الحقلالنوعمطلوبالوصف
namestringنعماسم الحملة، بحد أقصى 255 حرفاً
descriptionstringلاالوصف، بحد أقصى 1000 حرف
channelstringنعمemail أو sms أو viber أو 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 يعمل الجدول الشهري في اليوم الأول من الشهر. ‏timeOfDay بصيغة HH:MM بنظام 24 ساعة (09:00، 17:30)؛ وتُرفض أي قيمة أخرى بالرمز 400.
conversionTrackingobjectلاprimaryConversion / secondaryConversions، اسم حدث وشروط خصائص، وconversionWindowHours وattributionModel، أي first_touch/last_touch/linear
emailConfigIdstringلاهوية مرسل Email محفوظة لقناة Email
subscriptionCategoryIdstringلافئة الموافقة أو قائمة الاشتراك التي تُرسل الحملة ضمنها
sendVolumeLimitobjectلاenabled وmaxSends وcadence، أي lifetime/per_send

محتوى رسالة In-app​

تحمل كل نسخة in-app محتواها المنشأ في customContent. ويحدد mode الحقول المطبقة:

modeالحقوليُعرض بوصفه
nativetitle وbody وimageUrl وbuttons وcloseButton وbackdropDismissible وstyleمكوّنات التطبيق نفسه - دون web view
html (الافتراضي)html وcss وmaxWidthشيفرة كتبها المؤلف داخل web view
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
}
}
}
]
}

الحقول الأصلية نص وليست markup. تُسلَّم إلى التطبيق دون escaping لأن التطبيق يعرضها في عناصر نصية، فيصل A & B كما هو وليس A & B. يعمل التخصيص عبر Liquid في title وbody وفي text وurl للأزرار.

الحقل buttons محدود بثلاثة. والقيمة action واحدة من dismiss أو url أو deep_link؛ ويُشترط url للأخيرتين.

style - تجاوزات عرض اختيارية​

كل حقل اختياري، والحقل الغائب يعني الوراثة: لون سطح التطبيق ولون نصه ولون تمييزه وخطه. هذه الوراثة هي جوهر المحتوى الأصلي، فأرسل الحقل فقط حين تحتاجه الحملة.

الحقلالنوعيسري علىالمعنى
backgroundColorstringweb وiOS وAndroidخلفية البطاقة
textColorstringweb وiOS وAndroidالعنوان والنص (النص أخف قليلًا)
primaryButtonColorstringweb وiOS وAndroidلون الزر الأول
primaryButtonTextColorstringweb وiOS وAndroidنص الزر. إن غاب اختير الأسود أو الأبيض حسب التباين مع اللون
cornerRadiusnumberweb وiOS وAndroidمن 0 إلى 48. يُتجاهل في fullscreen حيث تكشف الزوايا المستديرة التطبيق خلفها
fontSizenumberweb وiOS وAndroidمن 10 إلى 32. حجم نص الرسالة، ويُشتق منه حجم العنوان. وعلى الهاتف يُطبَّق فوقه إعداد حجم النص لدى المستخدم
titleWeightstringweb وiOS وAndroidregular أو medium أو semibold أو bold. للعنوان فقط، ويبقى النص عاديًا لسهولة القراءة
textAlignstringweb وiOS وAndroidauto (الافتراضي) أو start أو center أو end. يتبع auto لغة الرسالة نفسها، فتُقرأ العربية والعبرية من اليمين داخل تطبيق بالإنجليزية
fontFamilystringالويب؛ وعلى الهاتف قدر الإمكانيسري على الهاتف فقط إن كان التطبيق يرفق الخط (iOS: مسجَّل، وAndroid: res/font أو عائلة نظام). وإن غاب احتفظ التطبيق بخطه بدل استبداله
customCssstringالويب فقطCSS مكتوب يدويًا، حتى 20000 حرف. تعيد حزمة الويب كتابة كل محدِّد ليقع داخل الرسالة قبل الحقن، فلا تصل أي قاعدة إلى الصفحة المضيفة، ويُحذف @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 عند التهيئة، لأن تلك الرسالة تُعرض في web view داخل التطبيق. لا تعمل السكربتات المكتوبة في الرسالة هناك؛ فجسر حزمة SDK نفسها يتولى أزرارها ونماذجها. أما المحتوى الأصلي فيُعرض دائمًا. راجع أدلة Android وiOS والويب.

| sendRateLimit | object | لا | enabled وmaxPerMinute | | quietTimeOverride | object | لا | تجاوز وقت هادئ خاص بالحملة | | utmSettings | object | لا | تجاوز UTM/وسم الروابط خاص بالحملة | | channelConfig | object | لا | إعداد خاص بالقناة، ويعتمد شكله على القناة | | stoConfig / abTestConfig | object | لا | إعداد تحسين وقت الإرسال أو اختبار A/B | | resendPolicy | string | لا | سياسة الإرسال مجدداً لحملة لمرة واحدة: 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لارسالة القناة. Email: subject وpreheader وfrom وfromName وhtml أو templateId وtext. SMS: body وfrom وshortenLinks؛ وفي حساب Delivyr (القوالب المعتمدة فقط) smsTemplateId وvariableMapping بدلًا من body - راجع قوالب SMS. Push: title وbody وicon وimage وdata. WhatsApp: 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 تحويلاً على الأقل في كل مجموعة. يجرى الاختبار على معدل التحويل، وهو نسبة يمكن اختبارها بشكل صحيح؛ أما الإيراد لكل مستلم فذو ذيل ثقيل ويُذكر كسياق لا كدليل.

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/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/linksإحصاءات نقر الروابط، startDate وendDate - في حملة داخل التطبيق تكون الصفوف هي نقرات الرسالة المسماة (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/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 لحملات 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 أو viber
messageobjectنعمsubject لـ Email وbody للنص العادي وhtml لـ Email. يمكن أن يكون {} لـ viber؛ فنص القالب الموافق عليه هو الرسالة
viberTemplateIdstringViber فقطقالب موافق عليه من سجل قوالب Viber. يفرض Rakuten Viber قوالب معتمدة مسبقاً للرسائل المعاملية وOTP منذ يوليو 2026؛ ويُحاسب المحتوى غير المعتمد بسعر ترويجي، لذلك يرفض API الإرسال من دونه
variablesobjectلاViber فقط؛ قيم الحقول الديناميكية للقالب، وتُدمج في سياق التخصيص
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فحص محتوى Email ضد الرسائل المزعجة قبل الإرسال
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 }. لا يرسل شيئاً - فرسائل داخل التطبيق تعتمد على السحب، فلا يوجد جهاز يُرسَل إليه. يحل محل 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إطلاق وضع التحسين بالذكاء الاصطناعي، RL
GET/campaigns/:campaignId/rl-statsإحصاءات لوحة حملة محسنة بالذكاء الاصطناعي
POST/campaigns/:campaignId/pause-rl / resume-rlإيقاف أو استئناف حملة محسنة بالذكاء الاصطناعي
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 (ملاحظة من مراجعة الذكاء الاصطناعي؛ params.title وparams.detail). تحمل الاستجابة أيضًا ai: { status: 'ok' | 'skipped' | 'off', reason?, version, cached? }؛ يعمل مرور الذكاء الاصطناعي على هذا المسار فقط، لا داخل بوابة الإرسال أبدًا، والمرور الفاشل سبب 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. اقتراحات الذكاء الاصطناعي (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تعارض في دورة الحياة، مثل استئناف حملة لم تعد متوقفة، أو إرسال حملة لم تعد في حالة قابلة للإرسال

ذو صلة​

لا يمكن لمفتاح API أن يوافق

تتطلب الموافقة مستخدمًا مسجَّل الدخول. فالموافقة تسجّل من راجع الحملة، ويُحكم على فصل المهام بمقارنة المُوافِق بالمُقدِّم - وكلاهما متعذّر على المفتاح. يُرفض الطلب المُصادق بمفتاح API بالرمز 403.