مرجع الواجهة البرمجية

كل ما يحتاجه المشروع للتعامل مع برافو باي: طريقة المصادقة، وما تُرسله، وما يعود إليك.

مقدمة

برافو باي خدمة مدفوعات تستهلكها المشاريع الأخرى عبر 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>`.

الاستجابات

401 المفتاح مفقود أو غير معروف أو معطّل أو مُبطل، أو أن مشروعه معطّل.
{
    "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 تُقبل في كل طلب يغيّر البيانات. وتكرار المفتاح يعيد نتيجة العملية الأولى بدل تنفيذها مرتين.

الاستجابات

404 استجابة واحدة مهما كان السبب: معرّف غير موجود، أو معرّف يخصّ مشروعًا آخر، أو سجل محذوف — تُجاب كلها بالنص نفسه، فلا يؤكّد `404` أبدًا وجود سجل لا تراه، ولا يُعاد إليك المعرّف الذي طلبته. أما رابط لا يطابق أي مسار فيُجاب بـ `No such endpoint.`
{
    "message": "No such record."
}
422 فشل التحقق من أحد الحقول. ويسرد `errors` كل المشكلات مرتبةً حسب الحقل الذي أرسلته.
{
    "message": "The given data was invalid.",
    "errors": {
        "amount": [
            "The amount must be at least 1."
        ]
    }
}
429 طلبات كثيرة جدًا. تمهّل ثم أعد المحاولة.
{
    "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 أعد استخدام القيمة نفسها عند إعادة المحاولة. فالتكرار يعيد الجواب الأول، أما التكرار بمحتوى مختلف فيُرفض.

الاستجابات

409 ما زال نداءٌ بهذا المفتاح جاريًا. انتظر ثم أعد المحاولة بالمفتاح نفسه.
{
    "message": "A request with this Idempotency-Key is already in progress."
}
422 استُخدم هذا المفتاح لطلب مختلف. استخدم مفتاحًا جديدًا.
{
    "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 في الاستجابة لا في الطلب: اللغة التي استعملها برافو باي فعلًا. اقرأها بدل أن تفترض أن ترويستك اعتُمدت.

الاستجابات

200 الباقة نفسها مطلوبةً بالإنجليزية ثم بالعربية. لا يختلف إلا الاسم والوصف — أما المعرّف والسعر ونوع المدة ودورة الفوترة والحالة ورمز الميزة فمتطابقة.
{
    "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 يذكر الحدّ، ولا يُخدم مئةً أبدًا.

الاستجابات

200 صفحة واحدة. معروضة هنا على الميزات، وهي أقصر قوائم الواجهة — وكل قائمة أخرى تحمل meta وlinks نفسها إلى جانب بياناتها. مأخوذة بـ Accept-Language: en.
{
    "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
    }
}
422 الحدّ المطلوب فوق السقف، أو رقم الصفحة أقل من واحد. ولا يعود شيء — صحّح القيمة وأعد الإرسال.
{
    "message": "The given data was invalid.",
    "errors": {
        "limit": [
            "The limit field must not be greater than 100."
        ]
    }
}

الميزات

GET /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 اللغة التي يعود بها الاسم والوصف. أما الرمز فلا يتغيّر بتغيّرها.

الاستجابات

200 ميزات مشروعك مرتّبةً حسب الرمز مع Accept-Language: en. والنداء نفسه بـ ar يعيد الرموز نفسها بالترتيب نفسه والنصّ بالعربية. وأي حقل غير مترجَم قيمته null.
{
    "data": [
        {
            "code": "aiCareerCoach",
            "name": "AI career coach",
            "description": "Personalised career guidance."
        },
        {
            "code": "readPaidArticle",
            "name": "Read paid articles",
            "description": "Access articles behind the paywall."
        }
    ]
}

الخطط

GET /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 — تمامًا كرقم غير موجود. فأنت لا ترى إلا خطط مشروعك.

الاستجابات

200 خطة واحدة مع Accept-Language: en. يعيد مسار القائمة مصفوفةً منها ضمن `data`، والتجربة المجانية سعرها null. واطلبها بالعربية فلا يتغيّر إلا الاسم والوصف — راجع اختيار اللغة.
{
    "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"
            }
        ]
    }
}

المنتجات

GET /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 يذكر الحدّ، بدل تقديم ١٠٠ بصمت، حتى لا تُحسب صفحة مبتورة جوابًا كاملًا.

الاستجابات

200 منتج واحد مع Accept-Language: en. ويعيد مسار القائمة مصفوفةً منها ضمن `data` ومعها `meta` و`links`. وهذا على تخفيض، فـ compare_at_price مضبوط وأعلى من price؛ أما المنتج بلا تخفيض فيكون compare_at_price و discount_percentage فيه null و is_discounted بقيمة false. واطلبه بالعربية فلا يتغيّر إلا الاسم والوصف ونصّ alt لكل عنصر — راجع اختيار اللغة.
{
    "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
            }
        ]
    }
}
422 خرج أحد معاملات الاستعلام عن قيمه المسموحة — حالة ليست إحدى الحالتين، أو limit فوق السقف.
{
    "message": "The selected status is invalid.",
    "errors": {
        "status": [
            "The selected status is invalid."
        ]
    }
}

العضويات

GET /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 معامل استعلام في مسار القائمة. يحصر النتيجة بعضويات هذا الشخص وحده.

الاستجابات

200 عضوية واحدة مع Accept-Language: en. يعيد مسار القائمة مصفوفةً منها ضمن `data`. ولا يتغيّر بتغيّر اللغة إلا الاسم والوصف.
{
    "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"
            }
        ]
    }
}

دورة حياة العضوية

POST /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
}

الاستجابات

201 عضوية أُنشئت للتوّ. وتجيب بقية المسارات بـ `200` وبالبنية نفسها.
{
    "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"
            }
        ]
    }
}
422 حالة العضوية لا تسمح بهذه العملية. ولم يتغيّر شيء.
{
    "message": "Membership [8091] cannot be paused: it is cancelled, and this action is only allowed from [active, trialing]."
}

الاستحقاقات

GET /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"
}

الاستجابات

200 الحكم. هذا المثال ميزة محدودة مُجابة بالمنح؛ ويتغيّر `status` وأرقام الاستخدام بحسب النتيجة.
{
    "data": {
        "feature": "reports.export",
        "external_sso_id": "user-1043",
        "status": "granted",
        "granted": true,
        "usage_limit": 5,
        "usage_count": 3,
        "remaining": 2
    }
}

الطلبات

POST /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
        }
    ]
}

الاستجابات

201 أُنشئ الطلب ومعه سطوره والفاتورة التي صدرت له. ويعيد مسار القائمة مصفوفةً من الطلبات ضمن `data`.
{
    "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"
        }
    }
}
400 ترويسة Idempotency-Key مفقودة. انظر «إعادة المحاولة بأمان».
{
    "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."
}
422 فشل التحقق من حقل، أو تعذّر تسعير الطلب — عملة غير متاحة، أو خصم أكبر من المجموع الفرعي، أو سطر بلا سعر صرف إلى عملة الطلب.
{
    "message": "Plan [42] is priced in USD, and there is no exchange rate to SAR to convert it."
}

إنهاء الطلب

POST /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"
}

الاستجابات

200 الطلب كما صار، ومعه سطوره وفاتورته. والمعروض هنا بعد إلغاء أدّى إلى إبطال فاتورة غير مدفوعة.
{
    "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
        }
    }
}
422 مرفوض: إمّا أنّ حال الطلب لا تسمح بالخطوة، وإمّا أنه لا دفعة باقية لتُردّ. ولم يتغيّر شيء.
{
    "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."
}

الفواتير

GET /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` — في نداء واحد.

الاستجابات

200 فاتورة واحدة. ويعيد مسار القائمة مصفوفةً منها ضمن `data`.
{
    "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"
    }
}