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
نص الطلب
| الحقل | النوع | مطلوب | الوصف |
|---|---|---|---|
name | string | نعم | اسم الحملة، بحد أقصى 255 حرفاً |
description | string | لا | الوصف، بحد أقصى 1000 حرف |
channel | string | نعم | email أو sms أو viber أو push أو webhook أو whatsapp أو in_app أو ai_optimized |
variants | array | نعم* | متغيرات الرسالة؛ مطلوبة لكل قناة عدا in_app |
targeting | object | لا | الجمهور: userIds وfilterGroups وexcludeFilterGroups وfilterOperator وsubscriptionPreference |
sendType | string | لا | immediate أو scheduled أو triggered أو intelligent أو recurring أو ai_optimized |
scheduledAt | string | لا | تاريخ ISO 8601 لـ sendType: "scheduled" |
scheduledTimezone | string | لا | المنطقة الزمنية IANA التي يفسر بها الوقت المجدول (تُحفظ مع الحملة)، أو recipient_local: كل شخص في وقت scheduledAt حسب منطقته الزمنية (منطقة مساحة العمل لمن ليست لديه)؛ تبدأ الحملة عندما تصل UTC+14 إليه. تُرفض الأسماء غير المعروفة بالخطأ 400. |
triggerConfig | object | لا | قاعدة تشغيل لـ sendType: "triggered"، وتشمل type وeventName وconditions وreEntry وcooldownHours |
recurringSchedule | object | لا | لـ 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. |
conversionTracking | object | لا | primaryConversion / secondaryConversions، اسم حدث وشروط خصائص، وconversionWindowHours وattributionModel، أي first_touch/last_touch/linear |
emailConfigId | string | لا | هوية مرسل Email محفوظة لقناة Email |
subscriptionCategoryId | string | لا | فئة الموافقة أو قائمة الاشتراك التي تُرسل الحملة ضمنها |
sendVolumeLimit | object | لا | enabled وmaxSends وcadence، أي lifetime/per_send |
محتوى رسالة In-app
تحمل كل نسخة in-app محتواها المنشأ في customContent. ويحدد mode الحقول المطبقة:
mode | الحقول | يُعرض بوصفه |
|---|---|---|
native | title وbody وimageUrl وbuttons وcloseButton وbackdropDismissible وstyle | مكوّنات التطبيق نفسه - دون web view |
html (الافتراضي) | html وcss وmaxWidth | شيفرة كتبها المؤلف داخل web view |
drag_drop | html و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 - تجاوزات عرض اختيارية
كل حقل اختياري، والحقل الغائب يعني الوراثة: لون سطح التطبيق ولون نصه ولون تمييزه وخطه. هذه الوراثة هي جوهر المحتوى الأصلي، فأرسل الحقل فقط حين تحتاجه الحملة.
| الحقل | النوع | يسري على | المعنى |
|---|---|---|---|
backgroundColor | string | web وiOS وAndroid | خلفية البطاقة |
textColor | string | web وiOS وAndroid | العنوان والنص (النص أخف قليلًا) |
primaryButtonColor | string | web وiOS وAndroid | لون الزر الأول |
primaryButtonTextColor | string | web وiOS وAndroid | نص الزر. إن غاب اختير الأسود أو الأبيض حسب التباين مع اللون |
cornerRadius | number | web وiOS وAndroid | من 0 إلى 48. يُتجاهل في fullscreen حيث تكشف الزوايا المستديرة التطبيق خلفها |
fontSize | number | web وiOS وAndroid | من 10 إلى 32. حجم نص الرسالة، ويُشتق منه حجم العنوان. وعلى الهاتف يُطبَّق فوقه إعداد حجم النص لدى المستخدم |
titleWeight | string | web وiOS وAndroid | regular أو medium أو semibold أو bold. للعنوان فقط، ويبقى النص عاديًا لسهولة القراءة |
textAlign | string | web وiOS وAndroid | auto (الافتراضي) أو start أو center أو end. يتبع auto لغة الرسالة نفسها، فتُقرأ العربية والعبرية من اليمين داخل تطبيق بالإنجليزية |
fontFamily | string | الويب؛ وعلى الهاتف قدر الإمكان | يسري على الهاتف فقط إن كان التطبيق يرفق الخط (iOS: مسجَّل، وAndroid: res/font أو عائلة نظام). وإن غاب احتفظ التطبيق بخطه بدل استبداله |
customCss | string | الويب فقط | 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:
| الحقل | النوع | مطلوب | الوصف |
|---|---|---|---|
id | string | نعم | معرّف المتغير |
name | string | نعم | اسم المتغير |
weight | number | نعم | حصة الحركة من 0 إلى 100؛ يجب أن يجمع الوزن وفق قواعد القناة |
message | object | لا | رسالة القناة. 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 |
isControlGroup | boolean | لا | يعلّم المتغير كمجموعة ضابطة مستبعدة، لتمكين قياس الزيادة |
مثال لطلب
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
معاملات الاستعلام
| المعامل | النوع | الافتراضي | الوصف |
|---|---|---|---|
sinceDays | number | 90 | فترة المراجعة لتحديد الحملات المشمولة |
الاستجابة
| الحقل | النوع | الوصف |
|---|---|---|
ranked | array | حملات تجاوز قياسها حد الدلالة الإحصائية، الأفضل أولاً |
stillCollecting | array | قيست ولم تُحسم بعد. قائمة منفصلة عمداً - لا حكم لها، ووضعها أسفل الخاسرين يعني أنها خسرت |
noHoldout | number | حملات بلا مجموعة تحكم. لا يمكن قياس أثرها، لذا تُعد ولا تُدرج |
noGoal | number | حملات لديها مجموعة تحكم بلا هدف تحويل. لا يوجد ما يُقارن، لذا تُعد ولا تُدرج |
sinceDays | number | الفترة المطبقة |
ما هو التحويل هنا. يحتسب كل صف هدف التحويل الخاص بالحملة (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
معاملات الاستعلام
| المعامل | النوع | الافتراضي | الوصف |
|---|---|---|---|
status | string | - | تصفية بالحالة؛ افصل الحالات المتعددة بفاصلة |
channel | string | - | تصفية بالقناة؛ افصل القنوات المتعددة بفاصلة |
tags | string | - | تصفية بالوسوم؛ مفصولة بفاصلة |
q | string | - | بحث نصي حر |
createdBy / editedBy | string | - | تصفية بالمنشئ أو آخر محرر؛ معرّفات مستخدمين مفصولة بفاصلة |
createdFrom | string | - | الحملات التي أُنشئت في أو بعد تاريخ ISO هذا |
page | number | 1 | رقم الصفحة |
limit | number | 20 | النتائج في الصفحة، بحد أقصى 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
نص الطلب
| الحقل | النوع | مطلوب | الوصف |
|---|---|---|---|
userId | string | نعم | معرّف المستخدم المستهدف |
channel | string | نعم | email أو sms أو push أو viber |
message | object | نعم | subject لـ Email وbody للنص العادي وhtml لـ Email. يمكن أن يكون {} لـ viber؛ فنص القالب الموافق عليه هو الرسالة |
viberTemplateId | string | Viber فقط | قالب موافق عليه من سجل قوالب Viber. يفرض Rakuten Viber قوالب معتمدة مسبقاً للرسائل المعاملية وOTP منذ يوليو 2026؛ ويُحاسب المحتوى غير المعتمد بسعر ترويجي، لذلك يرفض API الإرسال من دونه |
variables | object | لا | Viber فقط؛ قيم الحقول الديناميكية للقالب، وتُدمج في سياق التخصيص |
triggerData | object | لا | سياق متاح للتخصيص: type وname وproperties وmetadata |
idempotencyKey | string | لا | مفتاح إزالة تكرار يورده المستدعي؛ لا يُسلّم طلب معاد بالمفتاح نفسه مرتين |
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 | تعارض في دورة الحياة، مثل استئناف حملة لم تعد متوقفة، أو إرسال حملة لم تعد في حالة قابلة للإرسال |
ذو صلة
- إنشاء الحملات
- تحليلات الحملات
- Segments API - أنشئ الجماهير التي تستهدفها الحملات
- Canvas API - رحلات متعددة الخطوات
تتطلب الموافقة مستخدمًا مسجَّل الدخول. فالموافقة تسجّل من راجع الحملة، ويُحكم على فصل المهام بمقارنة المُوافِق بالمُقدِّم - وكلاهما متعذّر على المفتاح. يُرفض الطلب المُصادق بمفتاح API بالرمز 403.