مرجع الواجهة البرمجية
كل ما يحتاجه المشروع للتعامل مع برافو باي: طريقة المصادقة، وما تُرسله، وما يعود إليك.
مقدمة
برافو باي خدمة مدفوعات تستهلكها المشاريع الأخرى عبر HTTP. كل ما يحتاجه مشروعك متاح من خلال هذه الواجهة البرمجية؛ أما لوحة التحكم فهي مخصّصة لمن يدير برافو باي نفسه فقط.
لم تُنشر بعد أي مسارات في
v1. الأقسام التالية تشرح القواعد التي ستلتزم بها كل المسارات — العنوان الأساسي، وطريقة المصادقة، وسلوك الأخطاء وإعادة المحاولة. ويُوثَّق كل مسار هنا فور إطلاقه.
العنوان الأساسي
كل المسارات تقع تحت بادئة الإصدار:
https://your-bravopay-host/api/v1
الإصدارات ثابتة. بمجرد نشر إصدار لا يتغيّر شكل استجاباته أبدًا. قد تُضاف حقول
جديدة، لكن لا يُحذف حقل ولا يُعاد تسميته ولا يتغيّر نوعه. وإذا لزم تغيير يكسر
التكاملات القائمة فإنه يصدر باسم v2، ويبقى v1 يعمل كما هو.
نوع المحتوى. أرسل Content-Type: application/json وAccept: application/json.
كل الاستجابات بصيغة JSON، بما فيها الأخطاء.
اللغة. استجابات الواجهة البرمجية غير مترجمة. تصلك دائمًا قيم ثابتة قابلة
للمعالجة آليًا (رموز الحالة، وقيم مثل active)، وأنت من يصوغ النص المناسب
لمستخدميك. مبدّل اللغة في هذه الصفحة يغيّر هذه الشروحات فقط، لا محتوى الاستجابات.
المبالغ ليست أرقامًا عشرية. تُرسل المبالغ أعدادًا صحيحة بالوحدة الصغرى للعملة
(مثلًا 1050 مع "currency": "SAR" تعني ١٠٫٥٠ ريال)، ومعها رمز العملة دائمًا.
اقرأ المبلغ ورمز العملة معًا، ولا تعتمد على أحدهما دون الآخر.
المصادقة
كل طلب يُصادَق عليه بوصفه مشروعًا عبر مفتاح واجهة برمجية يُصدر من لوحة تحكم برافو باي. لا يوجد مسار لتسجيل الدخول ولا تبادل للرموز — المفتاح نفسه هو بيانات الاعتماد، ولا تنتهي صلاحيته تلقائيًا.
أرسله في ترويسة X-API-Key:
X-API-Key: bp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
كما تُقبل ترويسة Authorization: Bearer <key> إن كانت أنسب لعميل HTTP لديك.
تبدأ المفاتيح دائمًا بـ bp_.
يُعرض المفتاح مرة واحدة. عند توليده تعرض اللوحة المفتاح كاملًا مرة واحدة فقط، ثم تبقى أوائل حروفه ظاهرة لا غير، لأن برافو باي يخزّن بصمة المفتاح لا المفتاح نفسه. وإن فُقد، فالحل هو إبطاله وتوليد مفتاح جديد.
احتفظ به على الخادم. المفتاح يحمل كامل صلاحيات مشروعك — لا تضعه في متصفّح ولا في تطبيق جوال ولا في أي مكان يستطيع المستخدم قراءته.
كل طلب محصور في المشروع المالك للمفتاح. لن ترى أو تُعدّل إلا بيانات ذلك المشروع، ولا يوجد أي معامل يوسّع هذا النطاق.
عند فشل المصادقة — غياب الترويسة، أو مفتاح غير معروف، أو مفتاح معطّل أو مُبطل،
أو مشروع معطّل — تكون الاستجابة دائمًا 401 نفسها بالرسالة نفسها. عدم التمييز بين
الأسباب مقصود حتى لا تكشف المحاولة الخاطئة شيئًا. فإذا وصلك 401 غير متوقع، فراجع
المفتاح من لوحة التحكم.
المعاملات
| الاسم | النوع | مطلوب | القيم المقبولة | الوصف |
|---|---|---|---|---|
X-API-Key
|
header | مطلوب | bp_… (43 characters) | مفتاح المشروع كما عُرض عند توليده تمامًا. أو أرسله بدلًا من ذلك بصيغة `Authorization: Bearer <key>`. |
الاستجابات
{
"message": "Invalid API key."
}
تعريف المشترك
مفتاح واجهتك البرمجية يحدّد أي مشروع يُجري الاتصال، لا أيّ مستخدم من
مستخدميك يخصّه هذا الاتصال — لذا تذكر ذلك بنفسك حيثما كان له معنى، عبر
external_sso_id.
GET api/v1/orders?external_sso_id=user-1043
POST api/v1/orders { "external_sso_id": "user-1043", ... }
فهو معامل استعلام في عمليات القراءة، وحقل في جسم الطلب في عمليات الكتابة. وصفحة كل مسار تبيّن أيّهما، ومتى يكون مطلوبًا.
برافو باي لا يرى بيانات اعتماد مستخدميك إطلاقًا. لا يوجد هنا تسجيل دخول للمستخدم النهائي، ولا رمز وصول تمرّره، ولا جلسة. أنت تُصادق على مستخدمك داخل تطبيقك أنت، ثم تمرّر النتيجة: المعرّف الذي تعرفه به. وهذا المعرّف ليس سرًّا ولا يمنح صلاحية بذاته — كل ما يفعله أنه يضيّق ما يستطيع مفتاحك قراءته أصلًا.
استخدم المعرّف الذي يصدره نظام الدخول الموحّد لديك، واستخدم المعرّف نفسه في كل
موضع. يتعامل برافو باي معه كنصّ مبهم: يُخزَّن كما أُرسل تمامًا، ويُطابَق كما أُرسل
تمامًا، وuser-1043 وUSER-1043 شخصان مختلفان. وإن غيّرت طريقة تعريفك للمستخدم
فلن يتبعك برافو باي — تبقى عضوياته وطلباته وفواتيره القائمة معلّقة بالقيمة القديمة.
لا شيء يُتحقَّق منه. لا يملك برافو باي نسخة من دليل مستخدميك ولا سبيلًا لمراجعته، فأي نص يُقبل. والطلب المُنشأ بمعرّف فيه خطأ مطبعي طلبٌ صحيح تمامًا يخصّ مشتركًا لن تستعلم عنه مرة أخرى — فالمعرّف الذي ترسله هو المعرّف الذي عليك إرساله لتجده.
لن تجد مسارًا للمستخدمين، ولن يوجد. لا يخزّن برافو باي اسمًا ولا بريدًا ولا
ملفًا شخصيًا لمستخدميك؛ وكل استجابة تعيد إليك external_sso_id الخام الذي أرسلته،
وأنت تحوّله إلى شخص في جهتك، حيث يقيم ذلك السجل فعلًا.
العزل. المشتركون غير مشتركين بين المشاريع. وذكر مشترك يخصّ مشروعًا آخر لا يجد
شيئًا — فالسجل غير مرئي لمفتاحك، وقراءته بالمعرّف تُجاب بـ 404 دون أن تكشف
أصلًا ما إذا كان موجودًا.
المعاملات
| الاسم | النوع | مطلوب | القيم المقبولة | الوصف |
|---|---|---|---|---|
external_sso_id
|
string | اختياري | user-1043 | المعرّف الذي يعرف نظام الدخول الموحّد لديك المستخدمَ به. معامل استعلام في القراءات، وحقل في جسم الطلب في الكتابات — مطلوب في الكتابات، اختياري كمرشّح في القراءات. |
الأخطاء وإعادة المحاولة
تُعاد الأخطاء بصيغة JSON، والمعنى تحمله حالة HTTP. اعتمد على رمز الحالة لا على نص الرسالة: الرسالة تلميح للبشر وقد تُعاد صياغتها، أما رمز الحالة فلا يتغيّر.
| الحالة | المعنى | ما العمل |
|---|---|---|
400 |
تعذّر فهم الطلب. | صحّح الطلب؛ إعادة إرساله كما هو ستفشل أيضًا. |
401 |
المفتاح مفقود أو غير صالح للاستخدام. | راجع المفتاح في لوحة التحكم، ولا تُعد المحاولة. |
403 |
المفتاح صحيح لكنه لا يملك هذه الصلاحية. | لا تُعد المحاولة. |
404 |
لا يوجد سجل بهذا المعرّف، أو أنه يخصّ مشروعًا آخر. | لا تُعد المحاولة. |
409 |
تعارض مع الحالة الراهنة للسجل. | أعد قراءة السجل ثم قرّر. |
422 |
فُهم الطلب لكن أحد الحقول غير صالح. | اقرأ errors، صحّح الحقل، ثم أعد الإرسال. |
429 |
عدد الطلبات كبير جدًا. | انتظر ثم أعد المحاولة بتباعد متزايد. |
5xx |
خلل من جهتنا. | أعد المحاولة بتباعد متزايد وبمفتاح التكرار نفسه. |
أخطاء التحقق تأتي مفصّلة حقلًا حقلًا، وأسماء الحقول مطابقة لما أرسلته، فيمكنك ربطها مباشرة بحقول النموذج لديك.
إعادة المحاولة آمنة مع مفتاح التكرار. كل طلب يغيّر البيانات يقبل ترويسة
Idempotency-Key من اختيارك. وإرسال المفتاح نفسه مرتين يعيد نتيجة العملية الأولى
بدل تنفيذها من جديد — فأي انقطاع أو مهلة أو خطأ 5xx يمكن إعادة محاولته دون أي
خطر خصم مزدوج. استخدم قيمة فريدة لكل عملية منطقية (المعرّف UUID خيار مثالي)، ولا
تُعد استخدامها إلا عند إعادة محاولة العملية نفسها.
نتيجة الدفع لا تصل في الاستجابة. الدفع عملية غير متزامنة: الاستجابة تخبرك بأن
الطلب قُبل، أما النتيجة فتصلك لاحقًا على رابط الويب‑هوك المسجّل لمشروعك. لا تعتبر
استجابة 2xx هنا دليلًا على أن الدفع تم.
المعاملات
| الاسم | النوع | مطلوب | القيم المقبولة | الوصف |
|---|---|---|---|---|
Idempotency-Key
|
header | اختياري | Any unique value you choose, e.g. a UUID | تُقبل في كل طلب يغيّر البيانات. وتكرار المفتاح يعيد نتيجة العملية الأولى بدل تنفيذها مرتين. |
الاستجابات
{
"message": "No such record."
}
{
"message": "The given data was invalid.",
"errors": {
"amount": [
"The amount must be at least 1."
]
}
}
{
"message": "Too Many Attempts."
}
إعادة المحاولة بأمان
الشبكات تنقطع، والطوابير تعيد التسليم، والطلب الذي تنتهي مهلته قد يكون نجح فعلًا
دون أن تعرف ذلك من جهتك. لذا يطلب كل مسار يغيّر شيئًا ترويسة
Idempotency-Key، وتكرارُ المفتاح نفسه يعيد لك الجواب الأول بدل تنفيذ العمل
مرتين.
POST api/v1/orders
X-API-Key: your-key
Idempotency-Key: 7f3c1e94-2b6a-4d51-9f0e-8c2d5a1b3e77
اختر قيمة تخصّ هذه المحاولة وحدها — والمعتاد أن تكون UUID — وأرسل القيمة
نفسها في كل إعادة لها. ويحمل الجواب المعاد ترويسة Idempotent-Replay: true
لتميّزه إن أردت.
ما الذي يصلك:
- المفتاح نفسه والمحتوى نفسه، وقد انتهى النداء الأول — الجواب المحفوظ كما هو، برمز حالته الأصلي. ولا يُنشأ طلب ثانٍ.
- المفتاح نفسه والمحتوى نفسه، والنداء الأول ما زال جاريًا —
409. انتظر ثم أعد المحاولة، ولا تبدأ من جديد بمفتاح آخر لئلا ينتهي بك الأمر إلى طلبين. - المفتاح نفسه بمحتوى مختلف —
422. فالمفتاح يعرّف محاولة واحدة لنداء واحد، وإعادةُ جواب النداء الأول تعني إسقاط طلبك الثاني في صمت مع إخبارك بنجاحه، فيُرفض بدل ذلك. - بلا مفتاح —
400.
والنداء الفاشل يحرّر مفتاحه. فإذا عاد الطلب بـ 4xx أو 5xx لم يُحفظ شيء:
صحّح الطلب وأعده بالمفتاح نفسه. فلا يستحقّ الإعادةَ إلا النجاح.
والمفاتيح محصورة بمشروعك، فلا تصطدم قيمتك بقيمة مستهلك آخر أبدًا، ولا يستطيع مشروع آخر قراءة جوابك.
ويُطابَق المفتاح على الفعل والمسار والمحتوى، لا على ترويساتك. فللنداء المعاد
أن يحمل معرّف تتبّعٍ جديدًا أو Accept-Language مختلفة دون أن يصير نداءً آخر. وثمّ
نتيجةٌ تستحقّ المعرفة: التكرار الذي يطلب لغةً أخرى يظلّ يتلقّى الجواب المحفوظ
مكتوبًا بلغة النداء الأول. وهذا هو معنى التكرار. فإن أردت قراءته بلغةٍ أخرى
فاستعمل GET — وهي لا تأخذ مفتاحًا أصلًا — أو أنشئ نداءً جديدًا حقًّا بمفتاح جديد.
أما مسارات القراءة فلا تأخذ الترويسة ولا تحتاجها — فـ GET لا يغيّر شيئًا،
وإعادةُ جواب محفوظ له تعطيك سجلًّا قديمًا.
وأيّ النداءات تحتاجها: كل POST في هذا المرجع. إنشاء الطلب ونداءات دورة حياته
الخمسة؛ وبدء العضوية ونداءات دورة حياتها الستة؛ واستهلاك الاستحقاق. فما كان قادرًا
على التغيير احتاج مفتاحًا.
وملاحظة في الاستهلاك خاصةً: النداء المعاد الذي يصرف استعمالًا ثانيًا يعني شخصًا حُوسب مرتين على فعل واحد، فأعد استخدام مفتاح المحاولة التي تعيدها. أما صرف استعمال ثانٍ عن قصد فمحاولة جديدة تأخذ مفتاحًا جديدًا.
المعاملات
| الاسم | النوع | مطلوب | القيم المقبولة | الوصف |
|---|---|---|---|---|
Idempotency-Key
|
header | مطلوب | Any value unique to one attempt — a UUID is the usual choice | أعد استخدام القيمة نفسها عند إعادة المحاولة. فالتكرار يعيد الجواب الأول، أما التكرار بمحتوى مختلف فيُرفض. |
الاستجابات
{
"message": "A request with this Idempotency-Key is already in progress."
}
{
"message": "This Idempotency-Key was already used for a different request. An idempotency key identifies one attempt at one call, so use a new key."
}
اختيار اللغة
يحفظ برافو باي النصّ الذي كتبه المشغّل — اسم الميزة ووصف الباقة — بالعربية
والإنجليزية. وأنت تختار أيّهما تريد بترويسة Accept-Language المعتادة،
فتصلك الاستجابة مكتوبةً بها.
GET api/v1/features
X-API-Key: your-key
Accept-Language: ar
ولكل حقل مفتاح واحد لا مفتاحان: يصلك name وحده، لا name وname_ar معًا، فلا
تحتاج إلى تفريعٍ في شفرتك يسأل «أيّ حقلٍ أقرأ؟».
واللغتان المدعومتان en وar. والتفاوض على اللغة تفاوضٌ عاديّ: تُحترم
الأوزان، ويُردّ الرمز الإقليمي إلى لغته الأمّ، فـ ar-SA,ar;q=0.9,en;q=0.8 تعطيك
العربية، وen-US تعطيك الإنجليزية.
والإنجليزية هي الأصل. فإن لم ترسل الترويسة، أو أرسلتها فارغة، أو طلبت لغةً لا
يملكها برافو باي، جاءتك الاستجابة بالإنجليزية في صمت. فطلبُ fr ليس خطأً ولا
422؛ ولغةٌ أرسلتها سهوًا لا تستطيع أن تكسر تكاملك.
وتخبرك كل استجابة باللغة التي كُتبت بها فعلًا في ترويسة Content-Language.
فاقرأها ولا تفترض، خاصةً إن كنت تمرّر ترويسة متصفّحٍ لا تتحكّم فيه.
ما الذي يتغيّر وما الذي لا يتغيّر
لا يتغيّر إلا النصّ الذي كتبه إنسانٌ ليقرأه الناس: name و**description**.
أما كل ما تقرأه شفرتك فيبقى كما هو في اللغتين:
- الحالات والأنواع —
statusوtypeوduration_typeوbilling_cycle؛ - المعرّفات — رمز الميزة
codeوأيidوexternal_sso_id؛ - المبالغ كلها بما فيها نصّ
decimal، وسائر الأرقام؛ - التواريخ والأوقات وأسعار الصرف؛
- رسائل الأخطاء. فالفشل يُصاغ بالصياغة نفسها مهما كانت لغة طلبك.
فالنداء الواحد باللغتين لا يختلف إلا في النصّ المقروء، ولا شيء غيره. وازِن بين الاستجابتين أدناه.
الحقل غير المترجَم قيمته null
إذا لم يكن للحقل نصٌّ مكتوبٌ باللغة التي طلبتها، وصلتك القيمة null — لا نصّ
اللغة الأخرى.
وهذا مقصود. فإعطاؤك الإنجليزية وقد طلبت العربية يُقحم كلماتٍ تُقرأ من اليسار في
شاشةٍ تُقرأ من اليمين، فتبدو معطوبةً لمن ينظر إليها. أما null فصادقة، وتترك لك
الاختيار: أن تُخفي الحقل أو تضع فيه صياغتك أنت. فعامِل كل name وdescription
على أنه قد يكون فارغًا.
النداء المعاد يحتفظ بلغة أول جواب
حين تعيد نداءً بالمفتاح Idempotency-Key نفسه، يصلك الجواب المحفوظ — محتوى النداء
الأول بلغة النداء الأول — ولو أرسلت Accept-Language مختلفة في المرة الثانية.
وهذا هو معنى الإعادة: الجواب نفسه للنداء نفسه لا جوابٌ جديد. فالمفتاح يقول هذا هو
النداء الذي أجريته من قبل. فإن أردت القراءة بلغةٍ أخرى فأنشئ نداءً جديدًا بمفتاح
جديد، أو اكتفِ بإعادة قراءة السجل بـ GET، فهي لا تأخذ مفتاحًا أصلًا.
ويُوسم الجواب المعاد بـ Idempotent-Replay: true. وفي هذا الجواب وحده ثِق بترويسة
Idempotent-Replay قبل Content-Language: فالمحتوى محفوظ، ونصّه بلغة النداء
الأول مهما قالت Content-Language عن الطلب الذي أرسلته للتوّ.
المعاملات
| الاسم | النوع | مطلوب | القيم المقبولة | الوصف |
|---|---|---|---|---|
Accept-Language
|
header | اختياري | en or ar — weights and regional tags accepted | اللغة التي يُكتب بها اسم الاستجابة ووصفها. وما سواها، أو لا شيء أصلًا، يُجاب عليه بالإنجليزية لا بالرفض. |
Content-Language
|
header | اختياري | en or ar | في الاستجابة لا في الطلب: اللغة التي استعملها برافو باي فعلًا. اقرأها بدل أن تفترض أن ترويستك اعتُمدت. |
الاستجابات
{
"Accept-Language: en": {
"data": {
"id": 42,
"name": "Pro",
"description": "Everything, monthly.",
"price": {
"amount": 5000,
"currency": "USD",
"minor_unit": 2,
"decimal": "50.00"
},
"duration": 1,
"duration_type": "month",
"billing_cycle": "monthly",
"is_trial": false,
"is_featured": true,
"status": "active",
"features": [
{
"code": "readPaidArticle",
"usage_limit": -1,
"status": "active"
}
]
}
},
"Accept-Language: ar": {
"data": {
"id": 42,
"name": "المحترف",
"description": "كل الميزات، شهريًا.",
"price": {
"amount": 5000,
"currency": "USD",
"minor_unit": 2,
"decimal": "50.00"
},
"duration": 1,
"duration_type": "month",
"billing_cycle": "monthly",
"is_trial": false,
"is_featured": true,
"status": "active",
"features": [
{
"code": "readPaidArticle",
"usage_limit": -1,
"status": "active"
}
]
}
}
}
قراءة القوائم صفحةً صفحة
كل قائمة في هذه الواجهة تُجيب بصفحة واحدة، ولا تتجاوز الصفحة مئة سطر. وهذا يسري على كل مسار يعيد قائمة فيما يلي، فكُتب هنا مرةً واحدة بدل تكراره في كل واحد منها.
GET api/v1/orders?status=pending&limit=50&page=2
X-API-Key: your-key
ويُختار الجزء المطلوب بمعاملَين اختياريَّين في الرابط. فإن لم ترسل أيًّا منهما وصلتك أول ٢٥ سطرًا.
و**limit سقفٌ صارم لا مجرّد اقتراح.** فطلب limit=1000 يُرفض بـ 422 يذكر الحدّ،
ولا يعود إليك مئةً في صمت — لأن صفحةً لم تطلبها هي صفحةٌ ستعدّها وتطابق عليها
وتصدّقها. أما page بعد نهاية القائمة فأمرٌ آخر: سؤالٌ وجيه جوابه "لا شيء"، فيعود
200 بـ data فارغة، ولك أن تمشي بين الصفحات حتى تفرغ إحداها.
ما الذي يعود إليك
إلى جانب data التي تقرأها أصلًا، تحمل كل قائمة meta و**links**:
{
"data": [ … ],
"meta": {
"current_page": 1,
"per_page": 25,
"from": 1,
"to": 25,
"total": 412,
"last_page": 17
},
"links": {
"first": "https://…/api/v1/orders?status=pending&page=1&limit=25",
"last": "https://…/api/v1/orders?status=pending&page=17&limit=25",
"prev": null,
"next": "https://…/api/v1/orders?status=pending&page=2&limit=25"
}
}
totalيعدّ ما طابقته مرشِّحاتك لا ما في المشروع كلّه. وهو أرخص طريقة لسؤال كم فاتورة غير مسدّدة على هذا الشخص؟ — نداءٌ واحد بـlimit=1يجيبك دون أن تقرأ سجلًّا واحدًا.fromوtoهما موضعا أول سطر وآخره في هذه الصفحة، وكلاهماnullإذا كانت الصفحة فارغة. وlast_pageيساوي1حتى حين لا يوجد شيء أصلًا.- اتبع
links.nextبدل أن تبني الرابط التالي بنفسك. فالروابط تحمل مرشِّحاتك معها، فتسأل الصفحةُ الثانية ما سألته الأولى. وnextيكونnullفي الصفحة الأخيرة، وprevفي الأولى، وهذا هو مَخرج حلقتك.
ولا يتغيّر meta ولا links بتغيّر Accept-Language. فهما أرقام وروابط، وهي واحدة
في كل لغة، شأن كل حالة ونوع ورمز في هذه الواجهة.
أيّ النداءات تأخذهما
كل قائمة: الميزات والخطط والعضويات وسجلّ العضوية والطلبات والفواتير. أما ما يجيب بسجلٍّ واحد — خطة واحدة أو طلب واحد أو فحص استحقاق — فليس قائمة ولا يأخذ المعاملَين.
والمجموعات التي داخل السجلّ أمرٌ ثالث تعود كاملة: فـ items في الطلب
وfeatures في العضوية جزءٌ من السجلّ نفسه، محدودةٌ بالخطة التي جاءت منها، ولا
تُجزّأ إلى صفحات أبدًا.
أمران تعرفهما قبل أن تدور على الصفحات
الترتيب ثابت. فكل قائمة مرتّبة على ما لا يتكرّر، فلا يظهر سطرٌ في صفحتين بينما يسقط غيره.
والصفحة صورةٌ للحظة سؤالك عنها. فإن كانت السجلّات تُنشأ وأنت تمشي بين الصفحات،
فسجلٌّ جديد يهبط في الأعلى يزحزح ما تحته موضعًا واحدًا — فقد ترى سطرًا مرتين أو
يفوتك سطر. وأظهر ما يكون ذلك في سجلّ العضوية، فهو من الأحدث إلى الأقدم وينمو
دائمًا. فإن كنت تطابق لا تعرض، فامشِ من الصفحة الأخيرة إلى الأولى، أو أعد قراءة
الصفحة الأولى في النهاية وتحقّق أن meta.total لم يتغيّر.
المعاملات
| الاسم | النوع | مطلوب | القيم المقبولة | الوصف |
|---|---|---|---|---|
page
|
query | اختياري | 1 or more. Default 1 | أيّ صفحة تقرأ، ابتداءً من ١. والصفحة بعد النهاية تُجيب بـ 200 وقائمة فارغة لا بخطأ. |
limit
|
query | اختياري | Between 1 and 100. Default 25 | كم سطرًا تحمل الصفحة. وهو سقف صارم: طلب أكثر من مئة يُرفض بـ 422 يذكر الحدّ، ولا يُخدم مئةً أبدًا. |
الاستجابات
{
"data": [
{
"code": "aiCareerCoach",
"name": "AI career coach",
"description": "Personalised career guidance."
},
{
"code": "readPaidArticle",
"name": "Read paid articles",
"description": "Access articles behind the paywall."
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"from": 1,
"to": 2,
"total": 2,
"last_page": 1
},
"links": {
"first": "https://bravopay.illaf.mobi/api/v1/features?page=1&limit=25",
"last": "https://bravopay.illaf.mobi/api/v1/features?page=1&limit=25",
"prev": null,
"next": null
}
}
{
"message": "The given data was invalid.",
"errors": {
"limit": [
"The limit field must not be greater than 100."
]
}
}
الميزات
/api/v1/features
🔒 يتطلّب مفتاح واجهة برمجية للمشروع.
الميزة استحقاق يعرّفه مشروعك — رمز code يتحقق منه تطبيقك قبل أن يسمح للمشترك
بفعل شيء. الرموز رموزك أنت: تسمّيها ويخزّنها برافو باي كما كتبتها تمامًا، فـ
readPaidArticle وreadpaidarticle ميزتان مختلفتان. لا توجد قائمة مشتركة؛ لكل
مشروع رموزه الخاصة.
كِلا المسارين أدناه قراءة فقط. تُنشأ الميزات وتُعدّل من لوحة تحكم برافو باي لا عبر الواجهة البرمجية.
لعرض كل ميزات مشروعك:
GET api/v1/features
تُعاد الميزات مرتّبةً حسب code، وتحمل كل واحدة اسمها ووصفها بلغةٍ واحدة: هي
التي طلبتها بترويسة Accept-Language، أو الإنجليزية إن لم تطلب شيئًا. وأي حقل لا
ترجمة له بتلك اللغة قيمته null، ولا يُملأ أبدًا من اللغة الأخرى. راجع اختيار
اللغة.
لجلب ميزة واحدة عبر رمزها:
GET api/v1/features/readPaidArticle
Accept-Language: ar
تكون الاستجابة ميزة واحدة بالبنية نفسها:
{
"data": {
"code": "readPaidArticle",
"name": "قراءة المقالات المدفوعة",
"description": "الوصول إلى المقالات المدفوعة."
}
}
والرمز code رمزك أنت، لا يتغيّر بتغيّر اللغة؛ ولا يتغيّر إلا النصّ المقروء.
أما الرمز الذي لا يخصّ مشروعك فيُجاب عليه بـ 404 — تمامًا كرمز غير موجود أصلًا.
فأنت لا ترى إلا ميزات مشروعك، ولا سبيل لتوسيع هذا النطاق. راجع الأخطاء وإعادة
المحاولة لكيفية التعامل مع 404.
المعاملات
| الاسم | النوع | مطلوب | القيم المقبولة | الوصف |
|---|---|---|---|---|
Accept-Language
|
header | اختياري | en (default) or ar | اللغة التي يعود بها الاسم والوصف. أما الرمز فلا يتغيّر بتغيّرها. |
الاستجابات
{
"data": [
{
"code": "aiCareerCoach",
"name": "AI career coach",
"description": "Personalised career guidance."
},
{
"code": "readPaidArticle",
"name": "Read paid articles",
"description": "Access articles behind the paywall."
}
]
}
الخطط
/api/v1/plans
🔒 يتطلّب مفتاح واجهة برمجية للمشروع.
الخطة اشتراك يقدّمه مشروعك. تحمل كل خطة سعرًا، ومدة الفترة الواحدة، ووتيرة تحصيلها، ومجموعة الميزات التي تمنحها. وكِلا المسارين أدناه قراءة فقط — تُنشأ الخطط وتُعدّل من لوحة تحكم برافو باي لا عبر الواجهة البرمجية.
وخلافًا للميزة، ليس للخطة رمز من اختيارك؛ تُعرّف برقمها id — وهو المعرّف نفسه
الذي ستستخدمه عند إنشاء طلب لها.
لعرض كل خطط مشروعك، الأقدم أولًا:
GET api/v1/plans
لجلب خطة واحدة عبر رقمها:
GET api/v1/plans/42
قراءة الحقول:
priceكائن، أوnullللتجربة المجانية بلا سعر. ولا يستخدم رقمًا عشريًا أبدًا. فـamountعدد صحيح من أصغر وحدة للعملة —5000معminor_unit: 2تعني50.00. أماdecimalفهو القيمة نفسها مكتوبةً بدقّة للعرض؛ أجرِ حساباتك علىamountلا عليه.durationوduration_typeتحدّدان طول الفترة التي تمنح الوصول —durationبقيمة3معduration_type: "month"هو ربع سنة. أماbilling_cycleفهو وتيرة التحصيل، وهي مستقلة: قد تمنح الخطة سنةً كاملة وتحصّلmonthly.duration_typeواحد منdayأوweekأوmonthأوyear. وbilling_cycleواحد منmonthlyأوquarterlyأوyearlyأوone_time.statusإمّاactive(متاحة لاشتراكات جديدة) أوinactive(مسحوبة — تبقى الاشتراكات القائمة، ولا يمكن اشتراك جديد).is_trialوis_featuredكلٌّ منهما صحيح لخطة واحدة على الأكثر في مشروعك.featuresتسرد كل ميزة ممنوحة برمزهاcode، معusage_limitالذي تحدّده الخطة (-1يعني بلا حد)، وحالة المنحactiveأوdisabled.
أما رقم خطة لا يخصّ مشروعك فيُجاب عليه بـ 404 — تمامًا كرقم غير موجود. فأنت لا
ترى إلا خطط مشروعك.
الاستجابات
{
"data": {
"id": 42,
"name": "Pro",
"description": "Everything, monthly.",
"price": {
"amount": 5000,
"currency": "USD",
"minor_unit": 2,
"decimal": "50.00"
},
"duration": 1,
"duration_type": "month",
"billing_cycle": "monthly",
"is_trial": false,
"is_featured": true,
"status": "active",
"features": [
{
"code": "readPaidArticle",
"usage_limit": -1,
"status": "active"
}
]
}
}
المنتجات
/api/v1/products
🔒 يتطلّب مفتاح واجهة برمجية للمشروع.
المنتج شراءٌ لمرة واحدة يبيعه مشروعك — شيء يُشترى مرةً لا يُشترك فيه. وكِلا المسارين أدناه قراءة فقط: تُنشأ المنتجات وتُعدّل من لوحة تحكم برافو باي لا عبر الواجهة البرمجية.
وكالخطة، ليس للمنتج رمز من اختيارك؛ يُعرّف برقمه id — وهو المعرّف نفسه الذي
تضعه في سطر الطلب باسم product_id.
وشراء المنتج لا يمنح شيئًا داخل برافو باي. هذا هو الفرق كله بينه وبين الخطة،
وعنه يتفرّع كل ما عداه. فدفع ثمن الخطة يُنشئ اشتراكًا ويفتح الميزات التي تمنحها،
أما دفع ثمن المنتج فلا يُنشئ شيئًا: يسجّل برافو باي وقوع البيع، ويحاسب عليه،
ويحصّل المال. وتسليم الشيء من شأن تطبيقك أنت. فراقِب حالة الطلب status —
وfulfilled تعني أن البيع تمّ — ثم تصرّف بنفسك.
ولهذا السبب نفسه لا وجود في هذه الواجهة لمخزون ولا عنوان شحن ولا تنزيل ولا مفتاح ترخيص. فبرافو باي لا يعلم أعندك الصنف أم لا، ولا كيف يصل إلى المشتري.
لعرض كل منتجات مشروعك، الأقدم أولًا:
GET api/v1/products
ولعرض المعروض للبيع منها فقط:
GET api/v1/products?status=active
ولجلب منتج واحد عبر رقمه:
GET api/v1/products/12
قراءة الحقول:
priceكائن، ولا يستخدم رقمًا عشريًا أبدًا، وخلافًا لسعر الخطة لا يكونnullقطّ — فللمنتج عملة دائمًا. وamountعدد صحيح من أصغر وحدة للعملة، فـ7000معminor_unit: 2تعني70.00. أماdecimalفهو القيمة نفسها مكتوبةً بدقّة للعرض؛ أجرِ حساباتك علىamountلا عليه.compare_at_priceهو السعر المشطوب «قبل الخصم»، أوnullإن لم يكن المنتج على تخفيض. وهو للعرض لا غير. فلا يُحاسَب به أبدًا، ولا يُضاف إلى مجموع، ولا يُحوَّل إلى عملة أخرى، ولا يُكتب في سطر طلب. إنما وُجد لتشطبه في صفحتك، ولا شيء سواه.is_discountedوdiscount_percentageيُحسبان من السعرين لا يُخزَّنان، فلا يمكن أن يخالفاهما. وإن لم يكن ثمّة تخفيض كاناfalseوnull.statusإمّاactive(قابل للشراء) أوinactive(مسحوب). والطلب الجديد الذي يذكر منتجًا مسحوبًا يُرفض بـ422، أما الطلبات القائمة فتبقى سطورها وفواتيرها مستحقة، ويظلّ الاسترداد ممكنًا.image_urlهو صورة المنتج الرئيسية، أوnull. وmediaكل ما عداها — صور إضافية وفيديو، بالترتيب الذي رتّبه المشغّل. وكلاهما روابط مطلقة على مضيف عام، لأنك تعرضها في موقعك أنت.media[].urlهو العنوان النهائي سواء رُفع الملف إلى برافو باي أو استُضيف في مكان آخر، فلا تحتاج أبدًا إلى التفريق بينهما. وmediaتكون[]عند خلوّها، ولا تكونnullقطّ — فيمكنك المرور عليها بلا احتراز.media[].sort_orderترتيب العرض، الأصغر أولًا. وهي تصلك مرتّبةً أصلًا.
وما يتحرّك مع اللغة: name وdescription وalt كل عنصر في media تُكتب
باللغة التي طلبتها بترويسة Accept-Language. أما ما عداها — id وstatus
وis_discounted وdiscount_percentage وكل رقم داخل كائنَي السعر وكل رابط — فهو
واحد بأي لغة سألت. والحقل الذي لا ترجمة له يعود null، لا بنصّ اللغة الأخرى.
راجع «اختيار اللغة».
وصفحة واحدة في كل مرة. تحصل على ٢٥ منتجًا ما لم تطلب limit مختلفًا، ولا
تتجاوز ١٠٠ بحال. والمنتج يحمل معرضه كاملًا، فالصفحة هنا أثقل قراءةً مما يوحي به
عدد سطورها. راجع «تصفّح القوائم».
أما رقم منتج لا يخصّ مشروعك فيُجاب عليه بـ 404 — تمامًا كرقم غير موجود، وكذلك
منتج حذفه المشغّل. فأنت لا ترى إلا منتجات مشروعك.
المعاملات
| الاسم | النوع | مطلوب | القيم المقبولة | الوصف |
|---|---|---|---|---|
status
|
string | اختياري | active, inactive | يحصر القائمة في المعروض للبيع أو المسحوب. وأي قيمة أخرى تُجاب بـ 422 تذكر القيم الحقيقية، بدل صفحة فارغة تُقرأ وكأن «لا منتجات لديك». ويبقى المرشِّح في links.next. |
page
|
integer | اختياري | 1 or more, default 1 | أي صفحة تقرأ. والصفحة بعد الأخيرة تُجاب بـ 200 ومصفوفة data فارغة، فيمكنك المضيّ حتى تفرغ. |
limit
|
integer | اختياري | 1 to 100, default 25 | كم منتجًا تحمل الصفحة. وهو سقف صارم لا اقتراح: فطلب ما فوقه يُجاب بـ 422 يذكر الحدّ، بدل تقديم ١٠٠ بصمت، حتى لا تُحسب صفحة مبتورة جوابًا كاملًا. |
الاستجابات
{
"data": {
"id": 12,
"name": "Advanced Report Pack",
"description": "A one-off bundle of twelve printable reports.",
"image_url": "https://pay.example.com/storage/products/report-pack.webp",
"price": {
"amount": 7000,
"currency": "SAR",
"minor_unit": 2,
"decimal": "70.00"
},
"compare_at_price": {
"amount": 10000,
"currency": "SAR",
"minor_unit": 2,
"decimal": "100.00"
},
"is_discounted": true,
"discount_percentage": 30,
"status": "active",
"media": [
{
"id": 4,
"type": "image",
"url": "https://pay.example.com/storage/products/report-pack-detail.webp",
"alt": "A sample page",
"sort_order": 1
},
{
"id": 5,
"type": "video",
"url": "https://videos.example.com/report-pack.mp4",
"alt": null,
"sort_order": 2
}
]
}
}
{
"message": "The selected status is invalid.",
"errors": {
"status": [
"The selected status is invalid."
]
}
}
العضويات
/api/v1/memberships
🔒 يتطلّب مفتاح واجهة برمجية للمشروع.
العضوية اشتراك يملكه أحد الأشخاص في مشروعك. وهي مرتبطة بالشخص عبر external_sso_id
الذي تعرفه به — لا يحتفظ برافو باي بنسخة عن الشخص، بل بالمعرّف فقط — فالقيمة نفسها التي
ترسلها في مواضع أخرى تعرّفه هنا. وكل ما يلي قراءة فقط — أما بدء العضوية وتجديدها
وتغييرها وإنهاؤها فمكانها «دورة حياة العضوية» بعد هذا.
لعرض العضويات، الأحدث أولًا:
GET api/v1/memberships
ومرّر external_sso_id لحصر القائمة بشخص واحد — وهو سؤال «بماذا يستحقّ هذا الشخص؟»:
GET api/v1/memberships?external_sso_id=user-1043
لجلب عضوية واحدة عبر رقمها:
GET api/v1/memberships/8091
ولقراءة سجلّها — كل ما مرّت به من عمليات دورة الحياة، والأحدث أولًا:
GET api/v1/memberships/8091/history
قراءة الحقول:
statusواحد منactiveأوtrialingأوpausedأوpast_dueأوcancelledأوexpired. ولا يمنح الوصولَ إلاactiveوtrialing، وما عداهما يحجبه — فـpausedتعليق قابل للرجوع، وpast_dueمشكلة دفع قد تُحلّ، وcancelledوexpiredمنتهيتان.plan_idهو الخطة التي جاءت منها العضوية، أوnullلعضوية يدوية منحها المشغّل مباشرة. وnameوdescriptionلقطتان أُخذتا عند الإنشاء، فلا تتغيّران إن أُعيدت تسمية الخطة لاحقًا.start_dateوend_dateوtrial_ends_atوcancelled_atطوابع زمنية (أوnull). وauto_renewيبيّن هل ضُبطت على التجديد.featuresتسرد الاستحقاقات التي تمنحها العضوية حاليًا — ولا يُدرج منح مسحوب أو منح سُحبت ميزته. تحمل كل واحدة رمز الميزةcode، وusage_countالخاص بهذا الشخص، وusage_limitالمنسوخ من الخطة (-1يعني بلا حد)، وremaining(ما تبقّى، أوnullعند اللامحدود)، وreset_at(تاريخ إعادة ضبط العدّاد، أوnull).
أما رقم عضوية لا يخصّ مشروعك فيُجاب عليه بـ 404. فأنت لا ترى إلا عضويات مشروعك.
المعاملات
| الاسم | النوع | مطلوب | القيم المقبولة | الوصف |
|---|---|---|---|---|
external_sso_id
|
string | اختياري | The subject identifier you know a person by | معامل استعلام في مسار القائمة. يحصر النتيجة بعضويات هذا الشخص وحده. |
الاستجابات
{
"data": {
"id": 8091,
"external_sso_id": "user-1043",
"plan_id": 42,
"name": "Pro",
"description": "Everything, monthly.",
"status": "active",
"start_date": "2026-07-01T00:00:00+00:00",
"end_date": "2026-08-01T00:00:00+00:00",
"trial_ends_at": null,
"cancelled_at": null,
"cancellation_reason": null,
"auto_renew": true,
"features": [
{
"code": "readPaidArticle",
"usage_count": 2,
"usage_limit": 5,
"remaining": 3,
"reset_at": "2026-08-01"
}
]
}
}
دورة حياة العضوية
/api/v1/memberships
🔒 يتطلّب مفتاح واجهة برمجية للمشروع.
سبع عمليات كتابة، واحدة لكل ما قد يحدث للاشتراك. ويجيب كلٌّ منها بالعضوية كما صارت — بالبنية نفسها التي تعيدها مسارات القراءة — فيكفيك نداءٌ واحد لتحديث ما تعرضه لمستخدمك.
لبدء عضوية على إحدى خططك. يُنسخ اسم الخطة ووصفها وحدود ميزاتها إلى العضوية في هذه اللحظة، فإعادة تسمية الخطة لاحقًا لا تُعيد كتابة ما اشترك فيه الشخص:
POST api/v1/memberships
{ "external_sso_id": "user-1043", "plan_id": 42 }
ومرّر start_date لتقديم بداية المدة أو تأجيلها، وauto_renew لتسجيل رغبة الشخص في
الاستمرار. أما خطة تجريبية فتبدأ العضوية trialing لا active، ويُضبط trial_ends_at
على نهاية المدة. والجواب 201.
للتجديد — مدة أخرى من مدد الخطة، ورصيد جديد:
POST api/v1/memberships/8091/renew
وتُضاف المدة الجديدة إلى نهاية الحالية ما دامت في المستقبل، فالتجديد المبكر لا يكلّف
الشخص ما تبقّى له؛ أما عضوية منقضية فتُجدَّد من اليوم. وتعود عدادات الاستخدام إلى الصفر،
وتبقى الحدود كما هي، وتعود العضوية active.
لتغيير الخطة — والاتجاه أنت من يصرّح به، لأنك وحدك تعرف أيّ خططك هي الأعلى:
POST api/v1/memberships/8091/upgrade { "plan_id": 43 }
POST api/v1/memberships/8091/downgrade { "plan_id": 41 }
ويؤدّي المساران العمل نفسه ولا يختلفان إلا فيما يُسجَّل: تُنسخ صياغة الخطة الجديدة وحدودها، وتُسحب الميزات التي لا تحملها الخطة الجديدة، وتُستأنف المدة بمقدار مدة واحدة من الخطة الجديدة. ويُحتفظ بما أُنفق من الاستخدام — فمن استخدم ثلاثة من خمسة تصديرات فقد استخدم ثلاثة مهما صار الحد الجديد. ولا تُحتسب أيّ تسوية تناسبية ولا ينتقل أيّ مال.
للإلغاء والتعليق ورفع التعليق:
POST api/v1/memberships/8091/cancel { "reason": "Switching providers" }
POST api/v1/memberships/8091/pause
POST api/v1/memberships/8091/reactivate
ويسري الإلغاء فورًا ويُلغي auto_renew؛ وreason اختياري ويُخزَّن كما هو بكلماتك أنت.
أما التعليق فحجزٌ قابل للرجوع يوقف الوصول ولا يغيّر شيئًا سواه، فرفعه يعيد ما عُلّق تمامًا
— إلى trialing إن كانت نافذة التجربة ما تزال مفتوحة، وإلى active فيما عدا ذلك.
لقراءة السجل. كل عملية مما سبق تترك قيدًا، والأحدث أولًا:
GET api/v1/memberships/8091/history
ويحمل كل قيد action (وهو created أو renewed أو upgraded أو downgraded أو
paused أو reactivated أو cancelled)، وcreated_at، و— عند تغيير الخطة وحده —
old_plan_id وnew_plan_id.
حين يُرفض النداء. بخلاف التحقق من استحقاق، فالنداء في دورة الحياة إن تعذّر تنفيذه
فهو خطأ: لو أُجبت بـ 200 لصحّ لك أن تظنّ العضوية قد تحرّكت. فأيّ عملية لا تسمح بها حالة
العضوية الراهنة — تعليق عضوية ملغاة، أو رفع تعليق عن عضوية لم تُعلَّق قط، أو تجديد عضوية
يدوية لا خطة لها تُؤخذ منها مدة — يُجاب عنها بـ 422 مع message يبيّن الحالات المسموح
بالعملية منها. ولا يُطبَّق شيء جزئيًّا ولا يُكتب أيّ سجل.
أما رقم عضوية أو خطة يخصّ مشروعًا آخر فيُجاب عنه بـ 404. فأنت لا تتصرّف إلا في سجلات
مشروعك.
وتطلب السبعة كلها ترويسة Idempotency-Key — انظر «إعادة المحاولة بأمان». فأعد
المحاولة بالمفتاح نفسه يصلك الجواب الأول، لا تجديدٌ ثانٍ ولا مدةٌ ثانية.
المعاملات
| الاسم | النوع | مطلوب | القيم المقبولة | الوصف |
|---|---|---|---|---|
external_sso_id
|
string | مطلوب | The subject identifier you know a person by | صاحب العضوية. مطلوب عند الإنشاء؛ أما بقية المسارات فتأخذ رقم العضوية في المسار بدلًا منه. |
plan_id
|
integer | مطلوب | One of your own plan ids | الخطة التي تبدأ عليها العضوية أو تنتقل إليها. مطلوبة عند الإنشاء والترقية والتخفيض. وخطة تخصّ مشروعًا آخر يُجاب عنها بـ 404. |
start_date
|
string | اختياري | Any parseable date or timestamp | عند الإنشاء فقط. متى تبدأ المدة إن لم تكن الآن — فتاريخ الانتهاء يُقاس من هنا. |
auto_renew
|
boolean | اختياري | true / false | عند الإنشاء فقط. يسجّل رغبة الشخص في استمرار الاشتراك. ولا يتجدّد شيء تلقائيًّا بعد — فاستدعِ مسار التجديد. |
reason
|
string | اختياري | Free text, up to 1000 characters | عند الإلغاء فقط. سبب انتهاء الاشتراك، يُخزَّن كما هو ويُعاد في `cancellation_reason`. |
الطلب
{
"external_sso_id": "user-1043",
"plan_id": 42,
"auto_renew": true
}
الاستجابات
{
"data": {
"id": 8091,
"external_sso_id": "user-1043",
"plan_id": 42,
"name": "Pro",
"description": "Everything, monthly.",
"status": "active",
"start_date": "2026-07-27T00:00:00+00:00",
"end_date": "2026-08-27T00:00:00+00:00",
"trial_ends_at": null,
"cancelled_at": null,
"cancellation_reason": null,
"auto_renew": true,
"features": [
{
"code": "reports.export",
"usage_count": 0,
"usage_limit": 5,
"remaining": 5,
"reset_at": "2026-08-27"
}
]
}
}
{
"message": "Membership [8091] cannot be paused: it is cancelled, and this action is only allowed from [active, trialing]."
}
الاستحقاقات
/api/v1/entitlements/{code}
🔒 يتطلّب مفتاح واجهة برمجية للمشروع.
الاستحقاق يجيب عن سؤال واحد بشأن شخص واحد: هل يجوز لهذا الشخص استخدام هذه الميزة
الآن، وكم تبقّى منها إن كانت محدودة بعدّاد. وتُسمّى الميزة بالرمز code الذي عرّفتها
به، ويُسمّى الشخص بـ external_sso_id الذي تعرفه به.
للتحقق من الوصول دون إنفاق استخدام — قراءة آمنة يمكن استدعاؤها عند كل تحميل صفحة:
GET api/v1/entitlements/reports.export?external_sso_id=user-1043
لإنفاق استخدام واحد — كتابة تسجّل أنّ استخدامًا قد وقع. أرسِل الشخص في جسم الطلب:
POST api/v1/entitlements/reports.export/consume
وهذا هو المسار الذي تلجأ إليه لحظة استخدام الشخص للميزة فعلًا. وهو ذرّي: إن تسابق طلبان من طلباتك على آخر استخدام متبقٍّ، مُنح أحدهما فقط وأُبلغ الآخر بأنّ الحد قد بُلغ — فلا يمكن إنفاق ميزة محدودة بعدّاد بما يتجاوز حدّها مهما تزامن من الطلبات.
قراءة الحكم. يجيب المساران بـ 200 وبالبنية نفسها — فالنتيجة السالبة ليست خطأً بل
جوابًا. وتفرّع على هذه الحقول:
granted— القيمة المنطقية التي تحتاجها البوّابة. تكونtrueفقط عند السماح بالوصول (وعند الإنفاق: عند تسجيل استخدام فعلًا).status— السبب، لتخبر مستخدمك بشيء محدّد. وهو واحد من:granted، وlimit_reached(يملك الميزة لكن العدّاد نفد)، وmissing_feature(عضوية فاعلة لكنها لا تمنح هذه الميزة)، وno_active_membership(لا عضوية في حالة تمنح الوصول).usage_limitوusage_countوremaining— الأرقام خلف عرضٍ مثل «٣ من ٥ مستخدمة». وusage_limitيساوي-1حين يكون المنح بلا حد، وremainingيكونnullعندئذٍ (وكلما لم يُبلغ أيّ منح أصلًا). وعند إنفاقٍ مُجاب بالمنح يكونusage_countهو العدّ بعد الاستخدام.
كما أنّ شخصًا أو رمزًا code لا وجود له في مشروعك ليس خطأً أيضًا — بل يُحَلّ ببساطة إلى
no_active_membership أو missing_feature. فأنت لا تتصرّف إلا في عضويات مشروعك
وميزاته.
ويطلب الإنفاق ترويسة Idempotency-Key — انظر «إعادة المحاولة بأمان». فأعد المحاولة
بالمفتاح نفسه يصلك الجواب الأول، ولا يُصرف استخدامٌ ثانٍ. أما صرف استخدام ثانٍ عن قصد
فمحاولة جديدة تأخذ مفتاحًا جديدًا. والتحقّق لا يأخذ الترويسة ولا يحتاجها، فهو لا يغيّر شيئًا.
المعاملات
| الاسم | النوع | مطلوب | القيم المقبولة | الوصف |
|---|---|---|---|---|
code
|
string | مطلوب | A feature code you defined | جزء من المسار في كلا المسارين — الميزة التي يُتحقّق منها أو تُنفق. |
external_sso_id
|
string | مطلوب | The subject identifier you know a person by | الشخص المعنيّ بالاستحقاق. معامل استعلام في التحقق، وحقلٌ في جسم طلب الإنفاق. |
الطلب
{
"external_sso_id": "user-1043"
}
الاستجابات
{
"data": {
"feature": "reports.export",
"external_sso_id": "user-1043",
"status": "granted",
"granted": true,
"usage_limit": 5,
"usage_count": 3,
"remaining": 2
}
}
الطلبات
/api/v1/orders
🔒 يتطلّب مفتاح واجهة برمجية للمشروع.
الطلب عملية شراء قام بها أحد الأشخاص في مشروعك. وبرافو باي لا يحتفظ بسلّة
شراء — فالسلّة تخصّ تطبيقك، وأنت ترسل الطلب مكتملًا هنا في نداء واحد. فيُسعَّر
ويُكتب وتُصدر فاتورته معًا، ثم يعود بحالة pending.
POST api/v1/orders
Idempotency-Key: 7f3c1e94-2b6a-4d51-9f0e-8c2d5a1b3e77
{
"external_sso_id": "user-1043",
"items": [{ "plan_id": 42, "quantity": 2 }]
}
وترويسة Idempotency-Key مطلوبة — انظر «إعادة المحاولة بأمان». ويجيب النداء
بـ 201 ومعه الطلب وسطوره والفاتورة التي صدرت له.
والمال دائمًا عدد صحيح بالوحدة الصغرى. فـ 5000 في عملة ذات منزلتين هي ٥٠٫٠٠.
لا ترسل رقمًا عشريًا ولا تقرأه: فكل مبلغ يعود كائنًا يحمل amount وcurrency
وminor_unit ونصًّا عشريًّا في decimal.
العملة. إن تركت currency استُخدمت عملة مشروعك الافتراضية. ومرّرها لتسعّر
الطلب بغيرها — على أن تكون عملة يُسمح لمشروعك باستعمالها، وإلا كان الجواب 422.
الخطط والمنتجات. يذكر السطر واحدًا فقط من plan_id أو product_id، وللطلب
الواحد أن يجمع بينهما كيف شاء. فالسطر الجامع بينهما مرفوض، وكذلك الخالي منهما.
وكلاهما يُسعَّر ويُحوَّل ويُجمَّع ويُفوتَر ويُدفَع ويُستردّ على السواء — ولا
يختلفان إلا في موضع واحد، وبعد الدفع وحده: فسطر الخطة يُنشئ اشتراكًا ويفتح
ميزاته، وسطر المنتج لا يُنشئ شيئًا البتّة. فراقِب حالة الطلب status وسلّم المنتج
بنفسك.
كيف يُسعَّر السطر. يُسعَّر كل سطر بعملة الخطة أو المنتج ثم يُحوَّل إلى عملة الطلب، ويحمل السطر الاثنتين معًا:
unit_priceوtotal— بعملة العنصر نفسه، وهما ما تعرضه بجانبه؛converted_unit_priceوconverted_total— المال نفسه بعملة الطلب، وهو ما تجمعه إجماليات الطلب وما سيُحصَّل فعلًا؛exchange_rate— السعر المستعمل، نصًّا عشريًا.
وهذا السعر لقطة محفوظة. أعد قراءة الطلب بعد سنة تجد الأرقام التي حُصّلت منك، وإن صُحّح السعر بعدها. ويُضرب سعر الوحدة المحوَّل في الكمية، فثلاثةٌ بسعر وحدةٍ معلن تساوي دائمًا إجمالي السطر المعلن.
أما الخطة بلا سعر — التجربة المجانية — فتأتي سطرًا بصفر بعملة الطلب. وأما خطة
مسعّرة بعملة لا يوجد سعر صرف منها إلى عملة الطلب فلا يمكن تحويلها، ويُرفض النداء
كله بـ 422 بدل التخمين، ولا يُكتب شيء.
وdiscount_amount يُخصم من المجموع الفرعي، بالوحدة الصغرى لعملة الطلب. وما
زاد على المجموع الفرعي جوابه 422.
لقراءة الطلبات:
GET api/v1/orders
GET api/v1/orders?external_sso_id=user-1043
GET api/v1/orders?status=pending
GET api/v1/orders/5501
القائمة بالأحدث أولًا، والمرشِّحان يجتمعان. وstatus واحد من pending أو
confirmed أو processing أو fulfilled أو cancelled أو refunded — وما عداها
جوابه 422 لا قائمةٌ فارغة في صمت.
ويحمل كل طلب allowed_transitions — وهي الخطوات التي يجوز لك أن تخطوها،
أي cancelled أو refunded أو لا شيء — و**is_terminal**. اقرأهما بدل تثبيت دورة
حياتنا في شيفرتك، يبقَ تكاملك عاملًا حين تضيف نسخةٌ لاحقة حالةً جديدة. أما الحالات
المتقدّمة فليست في تلك القائمة عن قصد: فالدفع هو ما يقدّم الطلب، لا نداءٌ منك.
أما رقم خطة أو طلب لا يخصّ مشروعك فجوابه 404، دون أن يكشف أموجود هو أم لا.
المعاملات
| الاسم | النوع | مطلوب | القيم المقبولة | الوصف |
|---|---|---|---|---|
external_sso_id
|
string | مطلوب | The subject identifier you know a person by | لمن هذا الطلب. لا يحتفظ برافو باي بنسخة عن الشخص، بل بهذا المعرّف فقط. |
items
|
array | مطلوب | At least one line | سطور الطلب. والطلب بلا سطور ليس طلبًا. |
items.*.plan_id
|
integer | اختياري | The id of one of your project's plans | اشتراك يُشترى. وفي كل سطر واحد منهما فقط، plan_id أو product_id: فالسطر الجامع بينهما مرفوض، وكذلك الخالي منهما. وخطةٌ تخصّ مشروعًا آخر جوابها 404. |
items.*.product_id
|
integer | اختياري | The id of one of your project's products | شراءٌ لمرة واحدة يُشترى بدل الخطة. ودفع ثمنه لا يمنح شيئًا داخل برافو باي — فاقرأ حالة الطلب وسلّمه بنفسك. ومنتجٌ يخصّ مشروعًا آخر جوابه 404، وكذلك المحذوف. |
items.*.quantity
|
integer | اختياري | 1 to 1000, defaults to 1 | الكمية. وفي سطر الخطة، انتبه أن التنفيذ ينشئ عضوية واحدة لكل سطر لا لكل وحدة. |
items.*.metadata
|
object | اختياري | Any object | لك أنت. يُخزَّن كما هو ويعود كما هو، ولا يفسّره برافو باي أبدًا. |
currency
|
string | اختياري | A currency your project may use, e.g. SAR | رمز العملة حسب ISO 4217. وافتراضه عملة مشروعك الافتراضية، وما لا يُسمح لمشروعك به جوابه 422. |
discount_amount
|
integer | اختياري | Minor units, 0 or more, at most the subtotal | يُخصم من المجموع الفرعي، بالوحدة الصغرى لعملة الطلب. وما زاد عليه جوابه 422. |
notes
|
string | اختياري | Up to 2000 characters | ملاحظة حرّة تُحفظ مع الطلب. |
status
|
string | اختياري | One order status | معامل استعلام في مسار القائمة وحده. يحصر القائمة بنقطة واحدة من دورة الحياة. |
الطلب
{
"external_sso_id": "user-1043",
"currency": "SAR",
"discount_amount": 500,
"notes": "Renewal for the design team",
"items": [
{
"plan_id": 42,
"quantity": 2,
"metadata": {
"seat_of": "team-7"
}
},
{
"product_id": 12,
"quantity": 1
}
]
}
الاستجابات
{
"data": {
"id": 5501,
"external_sso_id": "user-1043",
"status": "pending",
"is_terminal": false,
"allowed_transitions": [
"cancelled",
"refunded"
],
"subtotal": {
"amount": 7500,
"currency": "SAR",
"minor_unit": 2,
"decimal": "75.00"
},
"discount": {
"amount": 500,
"currency": "SAR",
"minor_unit": 2,
"decimal": "5.00"
},
"total": {
"amount": 7000,
"currency": "SAR",
"minor_unit": 2,
"decimal": "70.00"
},
"exchange_rate": "0.266666666667",
"base_currency": "USD",
"notes": "Renewal for the design team",
"cancelled_at": null,
"cancellation_reason": null,
"refunded_at": null,
"refund_reason": null,
"created_at": "2026-07-27T10:15:00+00:00",
"items": [
{
"id": 9001,
"orderable_type": "plan",
"orderable_id": 42,
"quantity": 2,
"unit_price": {
"amount": 1000,
"currency": "USD",
"minor_unit": 2,
"decimal": "10.00"
},
"total": {
"amount": 2000,
"currency": "USD",
"minor_unit": 2,
"decimal": "20.00"
},
"exchange_rate": "3.750000000000",
"converted_unit_price": {
"amount": 3750,
"currency": "SAR",
"minor_unit": 2,
"decimal": "37.50"
},
"converted_total": {
"amount": 7500,
"currency": "SAR",
"minor_unit": 2,
"decimal": "75.00"
},
"metadata": {
"seat_of": "team-7"
}
}
],
"invoice": {
"id": 3301,
"external_sso_id": "user-1043",
"order_id": 5501,
"membership_id": null,
"type": "order",
"status": "pending",
"is_paid": false,
"is_unpaid": true,
"total": {
"amount": 7000,
"currency": "SAR",
"minor_unit": 2,
"decimal": "70.00"
},
"exchange_rate": "0.266666666667",
"base_currency": "USD",
"description": "Annual seats",
"due_date": null,
"paid_at": null,
"created_at": "2026-07-27T10:15:00+00:00"
}
}
}
{
"message": "This endpoint changes state, so it requires an Idempotency-Key header. Send a value unique to this attempt and reuse it if you retry."
}
{
"message": "Plan [42] is priced in USD, and there is no exchange rate to SAR to convert it."
}
إنهاء الطلب
/api/v1/orders/{id}/cancel
🔒 يتطلّب مفتاح واجهة برمجية للمشروع.
عمليتا كتابة، للنهايتين اللتين لا يعرفهما إلا أنت: انصرف عميلك، أو طلب عميلك ماله.
ويجيب كلٌّ منهما بالطلب كما صار — بالبنية نفسها التي تعيدها القراءات — فيكفيك نداءٌ
واحد لتحديث ما تعرضه لمستخدمك. وكلتاهما تطلب ترويسة Idempotency-Key؛ انظر
«إعادة المحاولة بأمان».
ويتقدّم الطلب هكذا:
pending → confirmed → processing → fulfilled
ولستَ أنت من يسوق هذا التقدّم، بل الدفع. فحين تغطّي دفعةٌ الفاتورة، يؤكّد برافو باي الطلب ويعالجه وينفّذه من تلقاء نفسه، والتنفيذ هو ما يُنشئ العضوية لكل سطر خطة، مع نسخ اسم الخطة ووصفها وحدود ميزاتها إليها. عضويةٌ واحدة لكل سطر لا لكل وحدة: فالكمية خمسة تعني عضوية واحدة، إذ إنّ خمسة استحقاقات متداخلة لشخص واحد تجعل فحص الاستحقاق يقرأ أوسعها حظًّا لا غير.
فليس ثَمّ ما تؤكّده أو تنفّذه بيدك. واستعلم عن الطلب (أو اقرأ invoice.status)
لترى ذلك يحدث.
وللإلغاء — صرف النظر عنه، مع سبب اختياري بكلماتك أنت:
POST api/v1/orders/5501/cancel { "reason": "Customer changed their mind" }
وتتبعه الفاتورة تلقائيًا: تُلغى إن لم تُدفع، وتُردّ إن كانت قد دُفعت. أما طلبٌ أُلغيت فاتورته أو رُدّت من قبل فيُلغى دون المساس بها ثانيةً.
وللاسترداد — إعادة المال:
POST api/v1/orders/5501/refund { "reason": "Duplicate charge" }
فيَرُدّ كل دفعةٍ ما زالت محفوظة على الطلب، ويُعلّم الفاتورة refunded، وينهي الطلب
عند refunded. فالطلب يقول refunded لأنّ المال عاد، لا بدلًا من عودته.
ولا تسترد إلا مالًا موجودًا حقًّا. فطلبٌ لم يُدفع قطّ، أو رُدّت دفعاته كلها من
قبل، جوابه 422 ولا يغيّر شيئًا — واستعمل الإلغاء في حالة عدم الدفع. فالاثنان
يبلغان الموضع نفسه في الطلب غير المدفوع (إذ تُلغى الفاتورة في الحالين)، والفرق أنّ
الطلب يقول حينئذٍ cancelled وهو صدق، لا refunded وهو ادّعاءٌ بعودة مالٍ لم يدخل
أصلًا.
والاسترداد لا يُلغي العضوية. فبرافو باي يفكّ المال ويترك الاستحقاق قائمًا، لأنّ كون مستخدمك أحقَّ بالوصول أو غير أحقّ قرارُك أنت لا قرارنا — فلعلّه في وسط أمرٍ ما. فألغِ العضوية بنفسك إن أردتَ زوالها؛ انظر «دورة حياة العضوية».
وحين يُرفض النداء. فخطوةٌ لا تسمح بها حال الطلب — إلغاء ما استُرد — جوابها 422
مع message يسمّي الحالات التي يجوز له الانتقال إليها. ولا يُطبَّق شيء جزئيًا.
واقرأ allowed_transitions في الطلب لتعرف ذلك سلفًا؛ فهو لا يذكر إلا ما تستطيع
نداءه فعلًا.
أما رقم طلب يخصّ مشروعًا آخر فجوابه 404.
المعاملات
| الاسم | النوع | مطلوب | القيم المقبولة | الوصف |
|---|---|---|---|---|
Idempotency-Key
|
header | مطلوب | Any value unique to one attempt | مطلوبة في المسارين كليهما. وتكرار المفتاح يعيد الجواب الأول بدل تنفيذ العملية ثانيةً. |
reason
|
string | اختياري | Up to 1000 characters | يُحفظ كما هو بكلماتك أنت — فلا يترجمه برافو باي ولا يحصره في رموز معيّنة. |
الطلب
{
"reason": "Customer changed their mind"
}
الاستجابات
{
"data": {
"id": 5501,
"external_sso_id": "user-1043",
"status": "cancelled",
"is_terminal": true,
"allowed_transitions": [],
"cancelled_at": "2026-07-27T11:02:00+00:00",
"cancellation_reason": "Customer changed their mind",
"refunded_at": null,
"refund_reason": null,
"invoice": {
"id": 3301,
"status": "void",
"is_paid": false,
"is_unpaid": false
}
}
}
{
"message": "Order [5501] has no payment left to give back — nothing was collected against it, or all of it has been refunded already. Cancel the order instead if it was never paid."
}
الفواتير
/api/v1/invoices
🔒 يتطلّب مفتاح واجهة برمجية للمشروع.
الفاتورة سجلّ برافو باي لما حُصّل وهل سُدّد. وإنشاء الطلب يصدرها تلقائيًا، فقلّما تحتاج إلى جلب الفواتير لعرض صفحة دفع — لكنها الأنسب لقراءة سجلّ الحساب، ولسؤال «بماذا لا يزال هذا الشخص مدينًا؟».
وكل ما هنا قراءة فقط، عن قصد. فليس ثمّة مسار يعلّم الفاتورة مدفوعةً أو ملغاةً أو مستردةً. فالفاتورة تسجّل أنّ مالًا انتقل، وتمكين المستهلك من ادّعاء ذلك دون انتقال مال يفرغها من معناها. وإنما تتغيّر الحالات نتيجةً لأمر واقع: فإنشاء الطلب يصدر الفاتورة، وإلغاؤه أو استرداده يبطلها أو يردّها.
GET api/v1/invoices
GET api/v1/invoices/3301
والقائمة بالأحدث أولًا. والمرشّحات تجتمع:
GET api/v1/invoices?external_sso_id=user-1043
GET api/v1/invoices?status=paid
GET api/v1/invoices?type=subscription_renewal
GET api/v1/invoices?unpaid=1
و**unpaid=1** هو ما تلجأ إليه عند تحصيل المستحقّات. فـ«ما زال مستحقًّا» يشمل ثلاث
حالات، وهذا المرشّح يعفيك من معرفتها — ومن متابعة تغيّرها.
قراءة الحقول:
statusواحد منdraft(مهيّأة ولم تُصدر)، وpending(صدرت وتنتظر السداد)، وpaid، وoverdue(تجاوزت تاريخ استحقاقها وما زالت مستحقّة — متأخّرة لا ضائعة، فما زال سدادها ممكنًا)، وrefunded، وvoid. وrefundedوvoidنهائيتان.- و
is_paidوis_unpaidكلاهما مذكور، وليسا نقيضين: فالفاتورةrefundedليست واحدًا منهما — إذ دُفعت ثم رُدّت. وكذلكvoid، إذ لم يكن عليها شيء أصلًا. - و
typeيبيّن ما يُحاسَب عليه ومن ثمّ أيّ رقم مضبوط: فـorderيحملorder_id، وsubscriptionوsubscription_renewalوupgrade_prorationتحملmembership_id، وrefundلا يحمل واحدًا منهما. - و
totalكائن فيهamount(عدد صحيح بالوحدة الصغرى) وcurrencyوminor_unitونصّ عشري فيdecimal. وamountهو ما تُجري عليه حسابك. - و
exchange_rateوbase_currencyلقطةٌ أُخذت عند إصدار الفاتورة. ولا تتحرّكان إن صُحّح السعر لاحقًا، وهذا هو سبب حفظهما أصلًا. - و
due_dateتاريخ أوnull، وpaid_atطابع زمني أوnull.
وdue_date قيمته null دائمًا حاليًّا: إذ لا يحدّد برافو باي شروط السداد بعد،
لأن انتماءها إلى المشروع أو إلى الخطة ما زال قيد التقرير. فلا تبنِ عليه إجراءات
مطالبة قبل أن يحمل قيمة. وحين يحملها، فإن الفاتورة التي تتجاوزه دون سداد تنتقل إلى
overdue وحدها — وoverdue تنبيهٌ لا نهاية، وتبقى الفاتورة قابلة للسداد.
أما رقم فاتورة لا يخصّ مشروعك فجوابه 404.
المعاملات
| الاسم | النوع | مطلوب | القيم المقبولة | الوصف |
|---|---|---|---|---|
external_sso_id
|
string | اختياري | The subject identifier you know a person by | يحصر القائمة بالفواتير الصادرة لهذا الشخص وحده. |
status
|
string | اختياري | One invoice status | يحصر القائمة بحالة واحدة. وما خرج عنها جوابه 422 لا نتيجةٌ فارغة. |
type
|
string | اختياري | One invoice type | يحصر القائمة بما يُحاسَب عليه. |
unpaid
|
boolean | اختياري | true or false | يعيد كل ما زال مستحقًّا — `draft` و`pending` و`overdue` — في نداء واحد. |
الاستجابات
{
"data": {
"id": 3301,
"external_sso_id": "user-1043",
"order_id": 5501,
"membership_id": null,
"type": "order",
"status": "pending",
"is_paid": false,
"is_unpaid": true,
"total": {
"amount": 7000,
"currency": "SAR",
"minor_unit": 2,
"decimal": "70.00"
},
"exchange_rate": "0.266666666667",
"base_currency": "USD",
"description": "Annual seats",
"due_date": null,
"paid_at": null,
"created_at": "2026-07-27T10:15:00+00:00"
}
}