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

Users API

إنشاء ملفات المستخدمين وتحديثها وإدارتها برمجياً.

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

المصادقة​

كل الطلبات تتطلب مصادقة بمفتاح API:

Authorization: Bearer jry_live_your_api_key_here
Content-Type: application/json

Track​

باب الإدخال الوحيد: من هو الشخص، وماذا فعل، وماذا اشترى - في طلب واحد.

كل حقل يقبل كائناً مفرداً أو مصفوفة، فيغطي الطلب الواحد شخصاً واحداً أو ألفاً.

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

POST /users/track

جسم الطلب​

الحقلالنوعمطلوبالوصف
attributesكائن أو مصفوفةلاحقول الملف الشخصي. وهي تبقى على الشخص
eventsكائن أو مصفوفةلاأشياء حدثت. تُضاف إلى الخط الزمني
purchasesكائن أو مصفوفةلاطلبات، بالشكل نفسه الذي يقبله POST /orders

يلزم واحد على الأقل من الثلاثة. الحدود لكل طلب: 1000 سمة، و500 حدث، و500 عملية شراء. والطلب الذي يتجاوز الحد يُرفض ولا يُقتطع - فمعالجة أول 500 من 5000 بصمت تُضيّع الباقي دون أي أثر.

السمات تُطبَّق أولاً

داخل الطلب الواحد تُكتب attributes قبل events وpurchases، فيمكن لحدث أن يشير إلى شخص أُنشئ في الطلب نفسه. «أنشئ المستخدم وسجّل حدث تسجيله» يعمل كطلب واحد.

الهوية​

الحقول الثلاثة نفسها تعرّف الشخص في المصفوفات الثلاث:

الحقلالوصف
externalIdمعرّفك أنت، وهو المفتاح الوحيد للإنشاء أو التحديث
userIdمعرّف Joryio الداخلي من 24 خانة hex. للتحديث فقط - لا يُنشئ أبداً ولا يطابق externalId
anonymousIdجلسة قبل تحديد الهوية. اربطها لاحقاً بـ POST /users/identify
userId لنا وexternalId لك

هذان ليسا مترادفين. إرسال userId ليس من 24 خانة hex يُرفض بـ 400. وللإنشاء أو التحديث بمعرّفك أنت - أياً كان شكله - استخدم externalId.

المطابقة بالبريد الإلكتروني​

عنصر في attributes يحمل email دون externalId ودون userId تتم مطابقته بالبريد الإلكتروني (بأحرف صغيرة ودون مسافات في الطرفين)، داخل مساحة العمل. عنوان البريد الإلكتروني ليس فريداً - قد يتشاركه زوجان أو عائلة، وقد يوجد شخص واحد تحت معرّفين - لذلك:

  • لا توجد جهة اتصال بهذا البريد: تُنشأ جهة اتصال جديدة.
  • توجد واحدة بالضبط: تُحدَّث جهة الاتصال هذه.
  • توجد عدة جهات: تُحدَّث واحدة فقط - الأحدث نشاطاً، أي جهة الاتصال التي تغيّر ملفها مؤخراً (وعند التعادل، الأحدث إنشاءً). الطلب نفسه يختار جهة الاتصال نفسها في كل مرة. وتبقى الأخرى كما هي: Joryio لا تدمج جهات الاتصال أبداً ولا تحدّثها جميعاً.
  • إلغاء الاشتراك في قائمة ضمن العنصر يسري على كل جهات الاتصال التي تحمل هذا البريد. أما الاشتراك في قائمة فيسري على جهة الاتصال المحدَّثة فقط.

العنصر الذي طابق عدة جهات اتصال يُدرج في attributes.matchedSeveral - تنبيه وليس خطأ: الكتابة نجحت.

{
"attributes": {
"processed": 1,
"failed": [],
"matchedSeveral": [
{ "index": 0, "userId": "6650f1c2a9e4b3d2c1f0e9a8", "count": 2 }
]
}
}

userId هو جهة الاتصال التي حُدِّثت، وcount عدد جهات الاتصال التي تتشارك العنوان. يظهر matchedSeveral فقط عند حدوث ذلك. لتحديث جهة اتصال محددة بدقة، أرسل externalId (تتم المطابقة بـ externalId وحده، حتى عندما يكون البريد مشتركاً) أو userId الخاص بجهة الاتصال. استيراد الملفات ومزامنة مستودعات البيانات تطابق بالطريقة نفسها.

السمات (Attributes)​

الحقلالنوعالوصف
emailstring
phonestring
attributesobjectحقول مخصصة (بحد أقصى 200 مفتاح، و50 كيلوبايت، وعمق تداخل 5)
subscriptionsarrayالاشتراك في القوائم - راجع الاشتراك في القوائم

الأحداث (Events)​

الحقلالنوعمطلوبالوصف
namestringنعماسم الحدث (بحد أقصى 255 حرفاً)
propertiesobjectلاخصائص هذه الواقعة. وهي لا تحدّث الملف الشخصي - أرسل تلك في attributes
timestampstringلاISO 8601. وقت وقوع الحدث فعليًا. الافتراضي هو الآن
timestringلااسم قديم لـ timestamp، ما زال مقبولًا
sessionIdstringلاجلسة العميل التي ينتمي إليها الحدث
deviceIdstringلاالجهاز الذي جاء منه الحدث
clientEventIdstringلامعرّفك لهذا الحدث، لتُحذف تكراراته عند إعادة المحاولة
لا ترسل Order Completed كحدث

هذا الحدث يُصدَر تلقائياً عند إرسال عملية شراء، ومعه تطبيع العملة الذي تحتاجه تقارير الإيرادات. وإرساله هنا يُرفض برسالة تشير إلى purchases، لأن طريقتي الخطأ كلتيهما صامتتان: وحده يسجّل إيراداً بلا طلب خلفه، فلا تراه صفحة الطلبات ولا RFM ولا القيمة مدى الحياة، ومع عملية شراء حقيقية يحتسب البيع مرتين.

المشتريات (Purchases)​

الشكل نفسه الذي يقبله POST /orders - orderId وtotal وcurrency وitems[] وبقية الحقول - ويمر عبر خط الأنابيب نفسه، فتراه صفحة الطلبات وRFM ونسبة الإيرادات جميعاً. وإعادة إرسال orderId تحدّث ذلك الطلب بدل إنشاء ثانٍ.

الاشتراك في القوائم​

يمكن لعنصر attributes أيضاً أن يضبط اشتراكات الشخص في القوائم، فيُنشئ طلب واحد الشخص أو يحدّثه، ويسجّل أحداثه، ويشركه في قائمة.

الحقلالنوعإلزاميالوصف
subscriptions[].listIdstringنعممعرّف قائمة الاشتراك (Subscriptions API)
subscriptions[].channelstringلاemail (افتراضي) أو sms أو whatsapp أو push أو viber
subscriptions[].statusstringلاsubscribed (افتراضي) أو unsubscribed

حتى 100 لكل عنصر. تُطبَّق بعد كتابة الشخص، عبر مسار الموافقة نفسه الذي يستخدمه POST /subscriptions/contacts/:userId/lists/:listId، وتُسجَّل بالمصدر api في سجل الموافقة.

  • لا يُعاد إحياء إلغاء اشتراك قائم أبداً. إذا كان الشخص قد ألغى اشتراكه من هذه القائمة على هذه القناة، يُتخطّى طلب subscribed ويُبلَّغ عنه. وإعادة اشتراكه استدعاء مقصود لنقطة نهاية الاشتراك. أما status: "unsubscribed" فيُطبَّق دائماً.
  • يجب أن تنتمي القائمة إلى مساحة العمل هذه. معرّف القائمة غير المعروف أو المؤرشف، أو قائمة من مساحة عمل أخرى، يُتخطّى ويُبلَّغ عنه ولا يُكتب أبداً.
  • لا توجد رسالة تأكيد. يسجّل الـ API الاشتراك مباشرة، حتى في قائمة تتطلب التأكيد المزدوج (double opt-in) - فخطوة التأكيد هذه خاصة بنماذج التسجيل. أرسل عبر الـ API فقط موافقة جمعتها بالفعل.
  • الإبلاغ بمؤشر العنصر. حين يرسل أي عنصر subscriptions، تضيف نتيجة attributes الحقل subscriptions: { applied, skipped[] }، ويذكر كل تخطٍّ index العنصر وlistId وchannel وreason. والاشتراك المتخطّى لا يُفشل العنصر - إذ يُنشأ الشخص أو يُحدَّث رغم ذلك.
{
"attributes": {
"processed": 1,
"failed": [],
"subscriptions": {
"applied": 0,
"skipped": [
{
"index": 0,
"listId": "3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d",
"channel": "email",
"reason": "Existing opt-out is not resurrected by user creation; use POST /subscriptions/contacts/:userId/lists/:listId for an explicit re-opt-in"
}
]
}
}
}

مثال على الطلب​

curl -X POST https://api-eu1.joryio.com/api/users/track \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"attributes": {
"externalId": "user_123",
"email": "[email protected]",
"phone": "+1234567890",
"attributes": { "firstName": "John", "plan": "premium" }
},
"events": {
"externalId": "user_123",
"name": "Product Viewed",
"properties": { "productId": "SKU-456", "price": 49.99 }
},
"purchases": {
"orderId": "ord_789",
"externalId": "user_123",
"total": 129.90,
"currency": "USD",
"items": [
{ "productId": "SKU-456", "name": "Blue T-Shirt", "quantity": 1, "price": 129.90, "total": 129.90 }
]
}
}'

هذا ليس نفسه إرسال anonymousId في /users/track. فالتتبّع يُنشئ أو يحدّث شخصاً، أما identify فيربط السجل المجهول بشخص معروف. عند أول تسجيل دخول - حين لا يوجد بعد ملف بهذا externalId - يصبح الملف المجهول نفسه هو الملف المعروف، محتفظاً بمعرّفه وتاريخه. وإذا كان للشخص ملف بالفعل، يُدمج الملف المجهول فيه ويُنقل إليه تاريخه. وتسجيل anonymousId في استدعاء تتبّع لا يدمج شيئاً.

الاستجابة​

كل مصفوفة تُبلغ عن نفسها باستقلال. وعنصر واحد خاطئ لا يُفشل الدفعة أبداً - تُفصَّل الإخفاقات بمؤشر الطلب، ويستمر كل ما هو صالح في المعالجة.

{
"attributes": { "processed": 1, "failed": [] },
"events": { "processed": 1, "failed": [] },
"purchases": { "processed": 1, "failed": [] }
}

التحذيرات: مقبول، لكنه غير قابل للوصول​

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

ولأن ذلك غير ظاهر في القيمة المخزَّنة، فإن الاستجابة تصرّح به:

{
"attributes": {
"processed": 2,
"failed": [],
"warnings": [
{
"index": 1,
"field": "email",
"reason": "Stored, but flagged undeliverable and excluded from email sends. Correct the address to make this contact reachable by email."
}
]
}
}

يظهر الحقل warnings فقط عندما يوجد ما يُبلَّغ عنه. تُنشأ جهة الاتصال ذات التحذير وتُحدَّث بشكل طبيعي، وتظل قابلة للوصول عبر كل قناة أخرى - عنوان البريد وحده هو ما لن يستقبل شيئًا أبدًا.

والفشل الجزئي يبدو هكذا:

{
"attributes": { "processed": 2, "failed": [] },
"events": {
"processed": 1,
"failed": [
{ "index": 1, "reason": "name should not be empty" }
]
},
"purchases": { "processed": 0, "failed": [] }
}
الحقول غير المعروفة تُرفض ولا تُتجاهَل

المفتاح المكتوب خطأً - emial أو evenName - يُبلَّغ عنه بمؤشره بدل إسقاطه بصمت. فقبوله بـ 200 كان سيترك الدمج يبدو سليماً بينما الحقل لا يُحدَّث أبداً.

أرقام الهاتف​

أرسل أرقام الهاتف مع + ورمز الدولة

أرسل أرقام الهاتف دائمًا بالصيغة الدولية الكاملة (E.164): + ثم رمز الدولة ثم الرقم - مثل +972501234567. الرقم الذي لا يبدأ بـ + يُقرأ وفق الدولة الافتراضية لأرقام الهاتف في مساحة العمل؛ وإذا لم تُعيَّن، أو أمكن قراءة الأرقام بطريقتين، يُحفظ غير موثّق ولا يُستخدم للرسائل. كيف تُعالَج أرقام الهاتف

أرسل phone بالتنسيق الدولي (+972501234567). يُقرأ الرقم المكتوب دون رمز الدولة (0501234567) وفق الدولة الافتراضية لأرقام الهاتف في مساحة العمل - مع إسرائيل يُحفظ بالشكل +972501234567. لا يخمّن Joryio الدولة أبدًا: إذا لم تُحدَّد دولة افتراضية فلا يُحوَّل مثل هذا الرقم.

الهاتف الذي ليس رقمًا صالحًا يُحتفظ به ولا يُحذف: يُخزَّن في حقل منفصل لجهة الاتصال هو phoneUnverified، ولا يتغير phone الحالي لجهة الاتصال. لا يُستخدم الرقم غير المُتحقَّق منه أبدًا لإرسال الرسائل أو الحظر أو مطابقة جهات الاتصال أو مرشحات الهاتف في الشرائح. إرسال هاتف صالح لاحقًا يحل محله. تُقبل الأرقام التي تتضمن رمز الدولة دون + (972501234567) عندما تكون صالحة - باستثناء سلسلة من 10 أرقام، فقد تكون رقمًا محليًا أو أمريكيًا، لذا تُحفظ غير مُتحقَّق منها.

عند تعيين دولة افتراضية، تكون الأرقام الصالحة بالطريقتين - كرقم محلي لتلك الدولة وكرقم لدولة أخرى برمزها (13123456789 مع الصين رقم جوال صيني وأيضًا +1 312...) - ملتبسة: لا يختار Joryio أيًا منهما أبدًا، ويحتفظ بها غير موثّقة. أرسل هذه الأرقام مع +.

تُبلغ الاستجابة عن ذلك في مصفوفة warnings نفسها:

{
"attributes": {
"processed": 1,
"failed": [],
"warnings": [
{
"index": 0,
"field": "phone",
"reason": "Phone saved as unverified and not used for messages - the number is not valid as written. Add the country code (for example +972501234567), or set a default country for phone numbers in workspace settings."
}
]
}
}

تتضمن جهات الاتصال التي يُرجعها الـ API الحقل phoneUnverified عند وجوده. يتبع استيراد الملفات ومزامنة مستودع البيانات القواعد نفسها.

في صفحة جهة الاتصال، يمكن تصحيح الرقم غير الموثّق (يُحفظ كهاتف - وإذا كان لجهة الاتصال هاتف بالفعل فإنه يُستبدل، وتنبّه الصفحة إلى ذلك أولًا) أو تجاهله. وعبر واجهة API، يؤدي PUT /users/{userId} مع {"clearPhoneUnverified": true} إلى إزالة الرقم غير الموثّق وترك phone كما هو.

Identify​

ربط سلوك سُجِّل على جلسة مجهولة بشخص معروف.

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

POST /users/identify
الحقلالنوعمطلوبالوصف
anonymousIdstringنعمالجلسة التي سُجِّل عليها السلوك
externalIdstringنعممعرّفك الذي تُربط به
attributesobjectلاحقول ملف شخصي تُضبط أثناء تحديد الهوية
curl -X POST https://api-eu1.joryio.com/api/users/identify \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "anonymousId": "anon_abc123", "externalId": "user_123" }'

أجهزة الإشعارات​

ربط رموز الإشعارات دفعةً واحدة - وهو المسار المخصص للانتقال من مزوّد آخر.

POST /api/users/track
Content-Type: application/json

{
"devices": [
{ "externalId": "user-123", "platform": "ios", "pushToken": "a1b2c3…", "appId": "<ios-app-id>",
"deviceInfo": { "model": "iPhone15,2" } },
{ "externalId": "user-456", "platform": "android", "pushToken": "d4e5f6…", "appId": "<android-app-id>" }
]
}
الحقلالنوعمطلوبالوصف
externalIdstringنعممعرّفك للشخص. أرسل userId بدلاً منه إن كان لديك معرّف Joryio الداخلي
platformios | android | webنعم
pushTokenstringنعمالرمز كما سجّله مزوّدك السابق تماماً
appIdstringلا*إلى أي من تطبيقاتك ينتمي هذا الرمز. تقوم إرسالات الحملات بالتصفية بناءً عليه، فالرمز المستورد بدونه يُحفظ ولا يُستخدم أبداً. اختياري عندما تحتوي مساحة العمل على تطبيق واحد فقط على تلك المنصة - عندها يُحدَّد تلقائياً. مطلوب عند وجود عدة تطبيقات. احصل على المعرفات من GET /apps
deviceInfoobjectلاالطراز وإصدار النظام وإصدار التطبيق - حر الشكل

حتى 1000 جهاز لكل طلب. وتُطبَّق الأجهزة بعد attributes، فيستطيع استدعاء واحد أن ينشئ الأشخاص ويربط رموزهم.

جهة اتصال غير موجودة تُفشل ذلك العنصر بدل إنشائها - فجهاز بلا ملف شخصي يستطيع تلقّي إشعار ولا ينتمي إلى أحد. أرسلهم في attributes ضمن الطلب نفسه.

ما ينجو فعلاً من عملية الانتقال​

ليس كل شيء، وهذا يحدّد الخطة أكثر مما تحدّدها الـ API:

  • iOS / APNs - تُصدَر الرموز لكل (bundle id، بيئة)، لا لكل مزوّد. فبنفس bundle id وبملف .p8 الخاص بك تنتقل الرموز.
  • Android / FCM - الرموز محصورة بمُرسِل Firebase. فبنفس المشروع تعمل، وبمشروع مختلف كانت كلها ميتة أصلاً.
  • إشعارات الويب - الاشتراك مرتبط بمفتاح VAPID الذي أنشأه، ولا يمكن نقله. هؤلاء المستخدمون يجب سؤالهم من جديد، مهما وعدت أي واجهة برمجية.

رمز ينتمي بالفعل إلى شخص آخر​

ينتقل. فرمز الإشعار يعرّف جهازاً لا شخصاً، وإعادة التثبيت والأجهزة المُباعة والأجهزة اللوحية المشتركة كلها أمور معتادة - فمن يحمل الجهاز هو من يتلقى الإشعار.

ويُسجَّل الانتقال على الطرفين. يُوسم جهاز جهة الاتصال السابقة بـ deactivatedReason: "reassigned" مع reassignedToUserId وdeactivatedAt، فيكون لسؤال «لماذا توقف هذا الشخص عن تلقي الإشعارات؟» جواب، بدل صف غير نشط قد يعني بالقدر نفسه إلغاء تثبيت.

الموافقة والتحقق​

تسجيل رمز لا يمنح موافقة على الإشعارات أبداً. فامتلاك رمز من مزوّد سابق ليس دليلاً على إذن، وقواعد الاشتراك تبقى كما هي - الاستبعاد ما زال سارياً، والمنح يظل إجراء امتثال. وهذه القاعدة نفسها التي يتبعها استيراد الملفات.

وتُؤخذ الرموز كما هي ولا تُختبر. يُفترض أن الرمز صالح حتى يرتد إرسال، وعندها تُعطّله عملية التسليم.

الحصول على مستخدم بالمعرّف​

استرجاع ملف المستخدم وسماته. يوجد مساران للبحث:

  • GET /users/by-user-id/:userId - البحث بمعرّفك أنت (الـ externalId الذي أنشأت به المستخدم). وهذا هو المسار الموصى به لعمليات الدمج.
  • GET /users/:userId - البحث بمعرّف Joryio الداخلي (الـ id من 24 خانة hex الذي تعيده الاستجابات).

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

GET /users/by-user-id/:userId

معاملات المسار​

المعاملالنوعالوصف
userIdstringمعرّفك الخارجي (externalId)

مثال على الطلب​

curl -X GET https://api-eu1.joryio.com/api/users/by-user-id/user_123 \
-H "Authorization: Bearer jry_live_your_api_key"

الاستجابة​

{
"id": "665f1e2a9b3c4d5e6f7a8b9c",
"userId": "665f1e2a9b3c4d5e6f7a8b9c",
"externalId": "user_123",
"email": "[email protected]",
"phone": "+1234567890",
"attributes": {
"firstName": "John",
"lastName": "Doe",
"plan": "premium",
"lifetimeValue": 1250.50
},
"segments": [],
"rndBucket": 4821,
"createdVia": "woocommerce",
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-20T14:20:00.000Z"
}

عرض المستخدمين​

عرض كل المستخدمين مع ترقيم الصفحات، مع إمكانية حصرهم بشخص واحد عبر مطابقة تامة. النتائج مرتبة بالأحدث تحديثاً أولاً. لا يوجد فلترة أو ترتيب خارج المعاملات أدناه - استخدم Segments للتقسيم بالسمات، وGET /users/search?query=... للمطابقة الجزئية أو ببداية النص على الاسم أو Email أو الهاتف أو المعرّف.

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

GET /users

معاملات الاستعلام​

المعاملالنوعالافتراضيالوصف
limitnumber50نتائج الصفحة، بحد أقصى 1000.
offsetnumber0عدد المستخدمين المتجاوز.
emailstring-مطابقة تامة، دون حساسية لحالة الأحرف.
externalIdstring-مطابقة تامة للمعرّف الخاص بك. GET /users/by-user-id/:userId يعيد ذلك السجل مباشرة.
userIdstring-مطابقة تامة لمعرّف Joryio الداخلي (24 خانة hex).
qstring-مطابقة جزئية: بداية النص في البريد وexternalId والاسم؛ والأرقام في أي موضع من الهاتف؛ والقيمة كاملة لمعرّف 24 خانة أو anonymousId. يُتجاهَل عند وجود معامل تام. راجع العثور على شخص

مثال على الطلب​

curl -X GET "https://api-eu1.joryio.com/api/users?limit=50&offset=0" \
-H "Authorization: Bearer jry_live_your_api_key"

الاستجابة​

{
"data": [
{
"id": "665f1e2a9b3c4d5e6f7a8b9c",
"userId": "665f1e2a9b3c4d5e6f7a8b9c",
"externalId": "user_123",
"email": "[email protected]",
"attributes": {
"plan": "premium"
}
},
{
"id": "665f1e2a9b3c4d5e6f7a8b9d",
"userId": "665f1e2a9b3c4d5e6f7a8b9d",
"externalId": "user_456",
"email": "[email protected]",
"attributes": {
"plan": "premium"
}
}
],
"pagination": {
"total": 156,
"limit": 50,
"offset": 0,
"hasMore": true
}
}

تحديث مستخدم​

تحديث البريد أو الهاتف أو السمات دون استبدال الملف الشخصي كاملاً. تُدمج السمات لكل مفتاح على حدة. ويستخدم PUT (لا يوجد مسار PATCH):

  • PUT /users/by-user-id/:userId - التحديث بمعرّفك أنت.
  • PUT /users/:userId - التحديث بمعرّف Joryio الداخلي.

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

PUT /users/by-user-id/:userId

جسم الطلب​

الحقلالنوعمطلوبالوصف
emailstringلاعنوان بريد جديد
phonestringلارقم هاتف جديد
attributesobjectلاسمات تُدمج (تحديث لكل مفتاح)

مثال على الطلب​

curl -X PUT https://api-eu1.joryio.com/api/users/by-user-id/user_123 \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"attributes": {
"plan": "enterprise",
"mrr": 499
}
}'

الاستجابة​

يُعاد كائن المستخدم المحدَّث مباشرة:

{
"id": "665f1e2a9b3c4d5e6f7a8b9c",
"userId": "665f1e2a9b3c4d5e6f7a8b9c",
"externalId": "user_123",
"attributes": {
"firstName": "John",
"plan": "enterprise",
"mrr": 499,
"signupDate": "2024-01-15T10:30:00Z"
},
"updatedAt": "2024-01-20T15:30:00.000Z"
}

حذف مستخدم​

حذف مستخدم. يُنقل المستخدم إلى سلة المحذوفات (يُؤرشف مع سجل تدقيق) ويمكن استعادته - فهو لا يُمحى نهائياً.

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

DELETE /users/:userId

معامل المسار هو معرّف المستخدم الداخلي في Joryio (ابحث عنه أولاً عبر GET /users/by-user-id/:userId إن كان لديك معرّفك أنت فقط). ويتطلب صلاحية users:delete. ويمكن تمرير ?reason= اختياري لتسجيل السبب في سجل الحذف.

مثال على الطلب​

curl -X DELETE "https://api-eu1.joryio.com/api/users/665f1e2a9b3c4d5e6f7a8b9c?reason=duplicate" \
-H "Authorization: Bearer jry_live_your_api_key"

الاستجابة​

204 No Content - جسم الاستجابة فارغ.

ملاحظات​

  • يُنقل المستخدم إلى سلة المحذوفات ويمكن استعادته - راجع عرض المستخدمين المحذوفين. ويُسجَّل كل حذف بتدقيق يوضح مَن ومتى وكيف ولماذا. (تتم الاستعادة حالياً عبر نقطة نهاية داخلية - تواصل مع الدعم لاستعادة جهة اتصال محذوفة.)
  • يُزال المستخدم من الشرائح وتتوقف الحملات النشطة عن استهدافه فوراً.
  • يُحتفظ بتاريخ الأحداث، فالمستخدم المُستعاد يحتفظ بتاريخه كاملاً.
  • ولمحو مستخدم وكل بياناته نهائياً (الحق في المحو ضمن GDPR)، استخدم مسار محو البيانات (DSR) بدلاً من ذلك - فهو غير قابل للتراجع وعملية مختلفة.

عرض المستخدمين المحذوفين​

عرض سلة المحذوفات - المستخدمون الذين حُذفوا، مع سجل تدقيق الحذف (مَن، ومتى، وكيف، ولماذا).

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

GET /users/deleted

يتطلب صلاحية users:read.

معاملات الاستعلام​

المعاملالوصف
queryاختياري. مطابقة بالبريد أو externalId (ببداية النص)، أو بالهاتف، أو بمعرّف المستخدم الأصلي.
limitأقصى عدد نتائج (الافتراضي 50، والحد الأقصى 200).
offsetعدد النتائج المتجاوزة (الافتراضي 0).

مثال على الطلب​

curl "https://api-eu1.joryio.com/api/users/deleted?limit=50" \
-H "Authorization: Bearer jry_live_your_api_key"

الاستجابة​

{
"total": 1,
"items": [
{
"originalId": "665f1e2a9b3c4d5e6f7a8b9c",
"email": "[email protected]",
"phone": "+15551234567",
"externalId": "usr_1001",
"deletedAt": "2026-08-09T03:00:30.000Z",
"deletedBy": "[email protected]",
"deletedVia": "api",
"deletedReason": "duplicate",
"restoredAt": null
}
]
}

العمليات الجماعية، نص المصفوفة​

لا توجد نقطة نهاية منفصلة للعمليات الجماعية، ولا يوجد مسار يقبل مصفوفة مجردة من المستخدمين. تمر الدفعات عبر POST /users/track: كل حقل من حقوله - attributes وevents وpurchases وdevices - يقبل إما كائناً مفرداً أو مصفوفة، فيستطيع طلب واحد أن يحمل أشخاصاً كثيرين ونشاطهم واشتراكاتهم في القوائم.

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

POST /users/track
Content-Type: application/json

{ "attributes": [ { ... }, { ... } ], "events": [ { ... } ] }

جسم الطلب​

يجب أن يكون جسم الطلب كائن JSON يحتوي على واحد على الأقل من attributes وevents وpurchases وdevices. أما المصفوفة المجردة ([ {...}, {...} ]) فتُرفض بالرمز 400 "Body must be an object with any of: attributes, events, purchases, devices."، وكذلك الكائن الفارغ.

لكل عنصر في المصفوفة الشكل نفسه المستخدم في صيغة الكائن المفرد الموصوفة في Track. ويخضع كل عنصر للتحقق بمفرده، والمفاتيح غير المعروفة تُرفض ولا تُتجاهل.

مثال على الطلب​

إنشاء شخصين أو تحديثهما، وإشراك الأول في قائمة، وتسجيل حدث تسجيل لكل منهما في استدعاء واحد. تُطبَّق attributes قبل events، فيمكن للأحداث أن تشير إلى أشخاص أُنشئوا في الطلب نفسه.

curl -X POST https://api-eu1.joryio.com/api/users/track \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"attributes": [
{
"externalId": "user_001",
"email": "[email protected]",
"attributes": { "firstName": "John", "plan": "free" },
"subscriptions": [
{ "listId": "3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d", "channel": "email" }
]
},
{
"externalId": "user_002",
"email": "[email protected]",
"attributes": { "firstName": "Jane", "plan": "premium" }
}
],
"events": [
{ "externalId": "user_001", "name": "Signed Up", "properties": { "source": "landing_page" } },
{ "externalId": "user_002", "name": "Signed Up", "properties": { "source": "referral" } }
]
}'

الاستجابة​

ملخص واحد لكل مصفوفة. يعدّ processed العناصر التي طُبّقت، ويسرد failed البقية بمؤشرها في تلك المصفوفة. ولا يظهر warnings إلا حين يُخزَّن عنصر لكنه يحتاج إلى انتباه (راجع Track).

{
"attributes": {
"processed": 2,
"failed": [],
"subscriptions": { "applied": 1, "skipped": [] }
},
"events": { "processed": 2, "failed": [] },
"purchases": { "processed": 0, "failed": [] },
"devices": { "processed": 0, "failed": [] }
}

استجابة مع إخفاقات​

العنصر المعيب الواحد لا يُفشل الدفعة أبداً - وكل ما هو صالح تستمر معالجته:

{
"attributes": {
"processed": 1,
"failed": [
{ "index": 1, "reason": "property emial should not exist" }
],
"subscriptions": {
"applied": 0,
"skipped": [
{
"index": 0,
"listId": "3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d",
"channel": "email",
"reason": "Existing opt-out is not resurrected by user creation; use POST /subscriptions/contacts/:userId/lists/:listId for an explicit re-opt-in"
}
]
}
},
"events": {
"processed": 1,
"failed": [
{ "index": 0, "reason": "name should not be empty" }
]
},
"purchases": { "processed": 0, "failed": [] },
"devices": { "processed": 0, "failed": [] }
}

الحدود​

  • لكل طلب: 1000 لـ attributes، و500 لـ events، و500 لـ purchases، و1000 لـ devices
  • الطلب الذي يتجاوز حداً يُرفض بالرمز 400 (مثلاً "events accepts at most 500 items per request (received 600). Split the batch.") ولا يُقتطع - فلا يُعالَج منه شيء
  • للدفعات الأكبر، قسّمها إلى طلبات متتالية لا تتجاوز حد كل حقل. وإعادة المحاولة آمنة: /users/track عملية upsert حسب externalId، والمشتريات مفتاحها orderId، ويمكن للأحداث أن تحمل clientEventId لإزالة التكرار
الاشتراك في القوائم يُرسل في الاستدعاء نفسه

يمكن لكل عنصر في attributes أن يحمل subscriptions (راجع الاشتراك في القوائم). في الاستجابة أعلاه، اشترك العنصر 0 من attributes في القائمة؛ وفي الاستجابة ذات الإخفاقات، احتُفظ بإلغاء اشتراكه القائم وأُبلغ عنه، بينما حُدِّث الشخص رغم ذلك. لتغيير اشتراك لاحقاً، استخدم POST /subscriptions/contacts/:userId/lists/:listId.

صيغة المصفوفة مخصصة للدفعات البرمجية من خادمك. أما للملفات وعمليات النقل الكاملة فلا تكتب حلقات تجميع بنفسك - استخدم الاستيراد الجماعي في لوحة التحكم (CSV/JSON مع ربط الأعمدة، وإزالة التكرار، وقواعد الموافقة، وتقرير أخطاء) أو مزامنة المستودع للتحميلات المتكررة.


أحداث المستخدم​

الحصول على أحداث مستخدم​

استرجاع كل أحداث مستخدم بعينه.

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

GET /users/:userId/events

معامل المسار هو معرّف المستخدم الداخلي في Joryio.

معاملات الاستعلام​

المعاملالنوعالافتراضيالوصف
limitnumber50عدد الأحداث المعادة (بحد أقصى 1000)
offsetnumber0إزاحة ترقيم الصفحات
startDatestring-تصفية الأحداث بعد هذا التاريخ (ISO 8601)
endDatestring-تصفية الأحداث قبل هذا التاريخ (ISO 8601)
eventNamestring-تصفية باسم الحدث
groupBySessionbooleanfalseإعادة الأحداث مجمّعة بالجلسة أيضاً
includeJourneyStepsbooleantrueالقيمة false تستبعد أحداث خطوات الرحلة (canvas.step.entered / canvas.step.left) من الأحداث ومن الإجمالي

مثال على الطلب​

curl -X GET "https://api-eu1.joryio.com/api/users/665f1e2a9b3c4d5e6f7a8b9c/events?limit=20" \
-H "Authorization: Bearer jry_live_your_api_key"

الاستجابة​

تستخدم حقول الأحداث صيغة snake_case (فهي قادمة من مخزن التحليلات):

{
"data": [
{
"event_id": "9b2f6c1e-4a8d-4f0b-9c3d-2e1f5a6b7c8d",
"event_name": "Order Completed",
"properties": {
"orderId": "order_456",
"total": 99.99
},
"timestamp": "2024-01-20 14:30:00"
},
{
"event_id": "1c3e5a7b-9d2f-4b6c-8e0a-3f5d7b9c1e2a",
"event_name": "Page Viewed",
"properties": {
"page": "/pricing"
},
"timestamp": "2024-01-20 14:25:00"
}
],
"total": 156,
"limit": 20,
"offset": 0
}

السمات الشائعة​

الحقول والسمات الخاصة​

email وphone هما حقلان في الملف الشخصي على المستوى الأعلى (وليسا سمتين) - أرسلهما في المستوى الأعلى من جسم الطلب.

وهذه السمات لها معنى خاص في Joryio:

السمةالنوعالوصف
firstNamestringالاسم الأول للمستخدم (يُستخدم في البحث والعرض)
lastNamestringاسم العائلة (يُستخدم في البحث والعرض)
languagestringاللغة المفضّلة (تضبطها حزم SDK تلقائياً عند توفرها)
timezonestringالمنطقة الزمنية للمستخدم بصيغة IANA (تحكم ساعات الهدوء والتسليم بالتوقيت المحلي في الرحلات)
countrystringرمز الدولة (مفهرس للتقسيم)

السمات المخصصة​

يمكنك إضافة سمات مخصصة بلا حد:

{
"attributes": {
"plan": "premium",
"mrr": 99,
"signupSource": "google_ads",
"lifetimeValue": 1250.50,
"tags": ["vip", "early-adopter"],
"preferences": {
"emailNotifications": true,
"smsNotifications": false
}
}
}

أنواع البيانات المدعومة​

  • نص: "premium"
  • رقم: 99.99
  • قيمة منطقية: true / false
  • تاريخ: "2024-01-15T10:30:00Z" (ISO 8601)
  • مصفوفة: ["tag1", "tag2"]
  • كائن: { "nested": "value" }

استجابات الأخطاء​

كل الأخطاء تستخدم جسم الخطأ القياسي - راجع نظرة عامة على API: استجابة الخطأ.

400 Bad Request​

{
"statusCode": 400,
"message": "Cannot create or update a user without a valid identifier (externalId or email to create; userId only updates an existing user)",
"timestamp": "2026-01-15T10:30:00.000Z",
"path": "/users"
}

401 Unauthorized​

{
"statusCode": 401,
"message": "Invalid or expired API key",
"timestamp": "2026-01-15T10:30:00.000Z",
"path": "/users"
}

404 Not Found​

{
"statusCode": 404,
"message": "User with userId 'user_123' not found",
"timestamp": "2026-01-15T10:30:00.000Z",
"path": "/users/by-user-id/user_123"
}

أفضل الممارسات​

1. إعادة المحاولة آمنة - POST /users/track عملية upsert​

POST /users/track مفتاحه هو externalId الخاص بك - فإعادة إرسال الطلب نفسه تحدّث الملف نفسه بدل إنشاء نسخة مكررة. ولا يوجد ترويسة Idempotency-Key، فأعد الطلب كما هو عند فشل الشبكة:

curl -X POST https://api-eu1.joryio.com/api/users/track \
-H "Authorization: Bearer jry_live_your_api_key" \
-d '{...}'

2. تسمية السمات​

استخدم أسماء سمات متسقة وواضحة:

جيد:

{
"signupDate": "2024-01-15",
"lifetimeValue": 1250.50,
"plan": "premium"
}

سيئ:

{
"sd": "2024-01-15",
"ltv": 1250.50,
"p": "premium"
}

3. صيغة رقم الهاتف​

أرسل أرقام الهاتف بصيغة E.164 - + ثم رمز الدولة ثم الرقم. راجع أرقام الهاتف لمعرفة كيفية معالجة الرقم بدون +.

جيد: "+972501234567" أو "+12125551234" تجنّب: "050-123-4567" أو "(212) 555-1234" - بدون + تعتمد على الدولة الافتراضية لمساحة العمل


حدود المعدل​

لا تملك Users API حدود معدل ثابتة لكل نقطة نهاية حالياً - راجع نظرة API العامة: حدود المعدل لسلوك المنصة وطريقة التعامل مع 429.


إحصاءات جهات الاتصال​

GET /users/stats
GET /users/stats/growth?days=30

أرقام لمطابقتها مع مصدر الحقيقة لديك - إن كان متجرك يضم 40,000 عميل ويُظهر هذا 31,000، فثمة ما لا يصل. ويعيد GET /users/stats/growth?days=30 مصفوفة JSON من { "date", "activeUsers", "newUsers" } - سجل لكل يوم (يوم محلي لمساحة العمل، الأقدم أولًا): activeUsers هم من فعلوا شيئًا في ذلك اليوم، وnewUsers من كان نشاطهم الأول في ذلك اليوم. days افتراضيه 30 ومحصور بين 1 و365؛ ولا يوجد معامل granularity - السلسلة يومية دائمًا.

{
"total": 31204,
"thisWeek": 1840,
"lastWeek": 2110,
"growthPercent": -12.8,
"growthType": "down",
"lifetimeSessions": 96420,
"activeUsers30d": 7310,
"activeUsersPrev30d": 6980
}

total هو عدد جهات الاتصال، باستثناء ملفات الزواحف. تحسب كل الأرقام الأخرى أشخاصًا فعلوا شيئًا - تلقي رسالة ليس نشاطًا - وتكون null لا 0 عندما يتعذر الوصول إلى مخزن التحليلات، فلا تُبلَّغ الفجوة أبدًا كصفر. الأسابيع بتوقيت مساحة العمل وتبدأ الأحد؛ وthisWeek أسبوع جزئي بطبيعته، لذا يبدو growthPercent منخفضًا في بداية الأسبوع. يحسب lifetimeSessions الجلسات المتمايزة لا أحداث بدء الجلسة.

الخطوات التالية​

إعادة اكتشاف أنواع السمات​

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

POST /attributes/rescan-types

تُحدَّد أنواع السمات المكتشَفة مرة واحدة، من عيّنة من جهات اتصالك، ثم تُحفَظ - فلا ينجرف النوع بين القراءات مع وصول بيانات جديدة. والثمن أن القرار قد يصبح قديمًا عندما تتغير البيانات فعلًا. تتخلص هذه النقطة من القرارات المحفوظة لتُتَّخذ من جديد من البيانات الحالية.

جسم الطلب​

الحقلالنوعمطلوبالوصف
eventNamestringلايحصر إعادة الفحص في خصائص حدث واحد. عند إهماله تُعاد سمات جهات الاتصال في مساحة العمل

الأنواع التي حددها فريقك لا تُمَسّ أبدًا - فقرار الإنسان لا يصبح قديمًا لأن البيانات تحركت. كذلك لا تتأثر الحقول القياسية (price وtotal وlastEventTime ...) لأن المنصة هي التي تحدد أنواعها.

الاستجابة​

{ "success": true, "message": "Types will be detected again from current data" }
متى تستدعي هذا

نادرًا. القرار المحفوظ هو المقصود - فهو ما يجعل الشريحة تعني اليوم ما كانت تعنيه أمس. استدعِه عندما يتغير معنى الخاصية فعلًا عند المصدر، لا وفق جدول زمني.