حجزك
العربية
ابحث عن موقف

API المشغّلين · v1

ادفع مواقفك. اسحب حجوزاتك.

المرجع الموجَّه إلى مشغّل يربط نظامه هو بـ Parkena. وكل ما يلي يصف برمجيات منشورة وتستجيب فعلاً — بما في ذلك الأجزاء التي تقول «لا».

اقرأ هذا قبل أن تخطّط لمشروع يقوم عليها.

هذا مرجع كامل ودقيق. يُصدر مالك أو مدير في حساب مشغّلك المفاتيح ويبطلها من لوحة التحكم، تحت Account ثم API keys — ويُعرض السرّ مرة واحدة فقط. أما ما ليس مفتوحاً فهو الباقي: لا توجد بيئة اختبار، ونحن نربط مشغّلاً واحداً في كل مرة، فاكتب إلينا قبل أن تخطّط لتطوير يقوم عليها. وما يرفض الإصدار v1 فعله مكتوب بالكامل في القسم 12، بدل أن نتركك تكتشفه في الأسبوع الثالث.

1. ما هي هذه الواجهة، وما ليست هي

هذا هو مرجع الإصدار v1 من API المشغّلين لدى Parkena. وهو البديل عن إدارة معروضك داخل لوحة تحكم Parkena: ادفع إليها مواقف سياراتك وأسعارك، واسحب منها حجوزات Parkena الخاصة بك. كلا الطريقين يكتب في الجداول نفسها عبر آليات الأمان نفسها، ولا يستطيع أيٌّ منهما بلوغ بيانات مشغّل آخر. ويمكنك استخدام الاثنين معاً — لوحة التحكم لما يقرّره إنسان، والواجهة للمزامنة الليلية — ولن يتصادما.

وما ليست هي: منتَج تندمج معه دون أن تكلّمنا. فالمالك أو المدير، بعد تسجيل دخوله، هو من يُنشئ المفتاح في لوحة التحكم؛ ولا توجد بيئة اختبار تتمرّن عليها، وأول تكامل يُبنى بمشاركة شخص من طرفنا. وهذا وصف للمرحلة التي تمرّ بها Parkena، لا طابور يمكنك تخطّيه.

خمسة أشياء لا تفعلها هذه الواجهة، نقولها منذ البداية

الحجوزات تُسحب عبر مؤشّر، وهذا المؤشّر هو مصدر الحقيقة. ويمكن لنقطة نهاية مسجَّلة أن تتلقّى تلميحاً موقَّعاً booking.changed يقول «اسحب الآن» — القسم 13 — لكن بيانات الحجز لا تسافر في أي webhook أبداً، عن قصد، والقسم 12 يقول ما الذي لا يزال غير موجود.

لا يوجد تقويم إتاحة. الفترات المحجوبة — القسم 7 — تسحب من البيع مديات تواريخ كاملة، لكنك لا تستطيع دفع «الأماكن الشاغرة الليلة» رقماً، والحقل الذي يبدو كذلك — capacity — هو إجمالي الأماكن. ووضع الإتاحة فيه يُبلّغ عن موقفك بشكل خاطئ ويُبطل مراجعة إدراجه كل ليلة.

لا تتحرّك أي أموال عبر هذه الواجهة. لا رسوم، ولا مبالغ مستردّة، ولا بيانات تحويلات، ولا أرقام عمولات.

لا شيء هنا يُنشئ حجزاً أو يعدّله أو يلغيه أو يسجّل وصوله أو يستردّ قيمته. لا يوجد نطاق لذلك ولا تفويض خلفه.

عنوان الأساس هو https://api.parkena.com/v1 — انظر القسم 3. ولا ينبغي توجيه أي شيء آخر إليه.

2. قاعدتان تضبطهما قبل أن تكتب سطراً واحداً

هاتان هما المسألتان اللتان يخطئ فيهما حتى التكامل المتقن، لأن الخطأ فيهما معاً يبدو وكأنه نجح. ولهذا رُفعتا إلى أعلى هذه الصفحة؛ وكل ما بعدهما مادة مرجعية عادية.

القاعدة الأولى: قارن الفروق قبل أن تُحدِّث

حين يوافق مراجعٌ من Parkena على إدراجك فإنه يوافق على محتوى بعينه. فإذا تغيّر ذلك المحتوى لم تعد الموافقة تصف ما هو منشور، فتُبطَل ويعود موقف السيارات إلى المراجعة — ويتوقّف عن البيع على parkena.com إلى أن يوافق عليه إنسان من جديد.

هذا سلوك صحيح، لكنه أمام آلة تعيد دفع مجموعة مواقفها كاملةً كل ليلة يصبح أيضاً طريقاً إلى السقوط من القائمة عند الساعة 03:00 كل ليلة إلى الأبد. لذلك لا تُصدر هذه الواجهة تحديثاً لا يغيّر شيئاً. فكلا مساري الكتابة يقرأ الصف الحالي ويقارنه حقلاً حقلاً، فإذا لم يختلف شيء لم تصدر أي عبارة على الإطلاق — لا UPDATE بلا أثر؛ بل لا عبارة أصلاً. وتحصل على "result": "unchanged" و"changed": [].

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

# an identical re-push of an approved car park
{ "result": "unchanged", "changed": [], "listing_review_superseded": false }
#
# the same car park, with one word of the name changed
{ "result": "updated",   "changed": ["name"], "listing_review_superseded": true }
استجابات مختصرة من الدالة المنشورة. والشكل الكامل في القسم 7.

والاستجابة الثانية ليست تحذيراً يمكن تجاهله: فذلك الموقف قد غادر واجهة الموقع، ويبقى خارجها إلى أن يُوافَق عليه مجدداً. والقسم 8 يسرد كل حقل يكلّفك إعادة مراجعة، وكل حقل لا يكلّفك.

القاعدة الثانية: GET يُعيد حقلاً يرفضه PUT

GET /lots يُعيد كل موقف سيارات ومعه مغلّف rates الخاص به، لأن ذلك هو المفيد عند القراءة. أما PUT /lots/{external_id} فلا يقبل مغلّفاً — إذ تُضبط الأسعار عبر PUT /lots/{external_id}/rates. ولذلك فإن الحلقة البديهية — اقرأ موقفاً وغيّر فيه حقلاً واحداً ثم أعده — تفشل إلى أن تحذف rates. واحذف معه external_id: فمكانه في المسار.

curl "$BASE/lots" -H "Authorization: Bearer $KEY.$SECRET" \
  | jq '.lots[0] | del(.rates, .external_id)' > lot.json
# edit lot.json
curl -X PUT "$BASE/lots/edge-main" -H "Authorization: Bearer $KEY.$SECRET" \
  -H 'Content-Type: application/json' --data @lot.json

أعِده ومعه rates فتحصل على 422 {"error":"field_belongs_to_another_route","field":"rates"}. نحن نرفضه بدل أن نتجاهله، والفرق بين الأمرين هو بيت القصيد: فلو وضعت سعراً جديداً في جسم الموقف وأجبناك بـ 200، لاعتقدتَ عن حقٍّ أن السعر قد تغيّر. ولم يكن ليتغيّر.

3. عنوان الأساس

كل مسار في هذه الصفحة منسوب إلى https://api.parkena.com/v1. وكل شيء بصيغة JSON، دخولاً وخروجاً.

api.parkena.com اسم مضيف تملكه Parkena

وهو يقف أمام الدالة التي تستجيب؛ والمسارات أدناه لن تتغيّر إذا تغيّر ما خلفه. ومع ذلك احتفظ بعنوان الأساس في الإعدادات لا في الشيفرة.

والتطوير المحلي على حزمة المستودع نفسه يستعمل المسارات ذاتها تحت /functions/v1/operator-api/v1 على أصل Supabase المحلي — المسارات نفسها، وعنوان أساس مختلف.

لا توجد بيئة اختبار ولا عنوان أساس للتجربة. فالعنوان أعلاه هو العنوان الحيّ، والمشغّل الذي يعمل الطلب باسمه يُستنتج من المفتاح الذي تقدّمه — لا من أي شيء في عنوان أو في جسم طلب. انظر القسم 4، والقسم 12 بشأن اختيار الجهة المستأجرة.

4. المصادقة

كل طلب يحمل ترويسة واحدة:

Authorization: Bearer <key_id>.<secret>

key_id وsecret هما نصفا مفتاح واحد، تصلهما نقطة. النقطة الأولى هي الفاصل بينهما؛ وأي نقطة بعدها تعود إلى السرّ.

curl "$BASE/ping" \
  -H "Authorization: Bearer pk_api_3f9c….pk_sec_a71b…"
  • key_idpk_api_ متبوعاً بـ 32 خانة ست عشرية. هذا هو النصف العلني. وهو يظهر في سجلّاتنا عن قصد، ويمكنك بأمان أن تضعه في ملف إعدادات أو تذكره في طلب دعم. ومعرفته لا تُبلغ صاحبها أبعد من معرفة اسم مستخدم.
  • secretpk_sec_ متبوعاً بـ 64 خانة ست عشرية، أي 256 بت. نحن لا نخزّنه. بل نخزّن HMAC-SHA256 مملّحاً له، في عمودين لا يستطيع أي دور في قاعدة بياناتنا قراءتهما. ولا يمكننا استرجاعه لك، أبداً. فإن فقدته، أصدر مفتاحاً جديداً وأبطل القديم.

لا يوجد بديل عبر سلسلة الاستعلام ?key= ولن يوجد: فالسرّ في عنوان URL هو سرّ في سجلّ وسيط، وفي سجلّ تصفّح، وفي ترويسة Referer.

كل إخفاق في المصادقة يُعيد الرمز 401 نفسه

فـ key_id مجهول، أو سرّ خاطئ، أو مفتاح مُبطَل، أو مفتاح منتهي الصلاحية، أو حساب مشغّل موقوف، أو مفتاح ينقصه النطاق الذي يتطلّبه المسار، أو external_id ليس من مواقفك — كلّها 401 {"error":"unauthorized"}. وهي غير قابلة للتمييز، عن قصد — والقسم 9 يقول ماذا يشتري ذلك وماذا يكلّفك.

استخدم GET /ping للتحقّق من مفتاح، بدل أن تستنتج شيئاً من رمز 401.

5. إصدار المفتاح وتدويره وإبطاله

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

والاستدعاءان اللذان خلف ذلك هما api_credential_issue وapi_credential_revoke، ويجريان عبر PostgREST برمز مستخدم مسجَّل الدخول لا بمفتاح API. ونوثّقهما هنا لأن مشغّلاً لديه فريق هندسي سيرغب في استدعائهما مباشرة؛ أما الطريق المعتاد للإصدار فهو شاشة لوحة التحكم تحت Account ثم API keys.

الإصدار

curl -X POST '<project rest url>/rpc/api_credential_issue' \
  -H "apikey: <anon key>" \
  -H "Authorization: Bearer <a signed-in owner or manager’s JWT>" \
  -H 'Content-Type: application/json' \
  -d '{
        "p_label": "nightly sync",
        "p_scopes": ["read_supply", "write_supply", "read_bookings"],
        "p_expires_in_days": 365
      }'
المعاملالنوعملاحظات
p_labelنص، 1–80 حرفاًما ستسمّي به هذا المفتاح بعد ستة أشهر. مطلوب.
p_scopesمصفوفة نطاقاتمجموعة غير فارغة من نطاقات متمايزة. مطلوب.
p_expires_in_daysعدد صحيح 1–3650، أو nullالقيمة null تعني بلا انتهاء صلاحية. اختياري.
[{
  "credential_id": "01a01a73-a650-710e-9be1-08ffc77b4696",
  "key_id":        "pk_api_3f9c…",
  "secret":        "pk_sec_a71b…",
  "label":         "nightly sync",
  "scopes":        ["read_supply", "write_supply", "read_bookings"],
  "created_at":    "2026-08-19T14:22:07.113904Z",
  "expires_at":    "2027-08-19T14:22:07.113904Z"
}]
الاستجابة — والمرة الوحيدة التي يُعاد فيها السرّ على الإطلاق.

لا يوجد p_expires_at ولن يوجد. فانتهاء الصلاحية عددُ أيام يحوّله الخادم مقابل ساعته هو؛ وهذه الواجهة لا تقبل طابعاً زمنياً يقدّمه المستدعي، في أي موضع، على أي مسار.

التدوير

لا يوجد استدعاء للتدوير، لأن التدوير دون انقطاع ليس إلا استدعاءين بالترتيب الصحيح:

  1. أصدر مفتاحاً ثانياً بالنطاقات نفسها.
  2. انشره في نظامك وتأكّد من أن الطلبات تتدفّق — GET /ping بالمفتاح الجديد، ثم راقب last_used_at عليه.
  3. أبطل المفتاح القديم.

والمفتاحان حيّان بين الخطوة الأولى والثالثة. ولا يوجد حدّ يمنعك من حمل اثنين.

الإبطال

curl -X POST '<project rest url>/rpc/api_credential_revoke' \
  -H "apikey: <anon key>" \
  -H "Authorization: Bearer <a signed-in owner or manager’s JWT>" \
  -H 'Content-Type: application/json' \
  -d '{"p_credential_id": "01a01a73-a650-710e-9be1-08ffc77b4696"}'

الإبطال فوري ودائم. والمفتاح المُبطَل لا يُعاد تفعيله — بل يُستبدل. وإبطاله مرتين يُعيد الصف نفسه بقيمة revoked_at الأصلية، لأن المفتاح لا يموت مرتين. ومعرِّفٌ ليس من معرّفاتك يُعيد []، تماماً كما يفعل معرِّف لم يوجد قط.

6. النطاقات

كل مفتاح يحمل مجموعة من النطاقات. وثلاثة نطاقات موجودة:

النطاقما يسمح به
read_supplyسرد مواقف سياراتك، ومغلّفات أسعارها لدى Parkena، والفترات المحجوبة لكل موقف — بما فيها العدّاد المجمَّع overlapping_bookings على كل فترة. عدّادٌ لا حجزٌ أبداً: لا مرجع معه ولا اسم ولا لوحة سيارة.
write_supplyإنشاء مواقف السيارات وأسعارها لدى Parkena وقوائم فتراتها المحجوبة وتعديلها.
read_bookingsسحب حجوزات Parkena الخاصة بك.

امنح أقلّ ما تحتاج إليه. فالمزامنة التي تدفع المعروض فقط لا تحتاج إلى read_bookings؛ ومهمّة تقارير لا تفعل إلا سحب الحجوزات يجب ألّا تحمل write_supply.

والتحقّق من النطاق ليس استرشادياً. فهو وسيطٌ يُمرَّر إلى استدعاء قاعدة البيانات نفسه الذي يقرّر صفوف أيّ مشغّل يجوز للطلب أن يمسّها، فلا يوجد طريق إلى بياناتك يتخطّاه. والمفتاح الذي لا يحمل النطاق الذي يتطلّبه المسار يحصل على الرمز 401 الموحَّد — الجواب نفسه الذي يحصل عليه سرٌّ خاطئ.

ولا يوجد write_bookings عن قصد. فلا شيء في Parkena يتيح لآلة أن تنشئ حجزاً أو تعدّله أو تلغيه، ولذلك فإن نطاقاً يحمل اسم تلك القدرة سيبدو لك حدّاً وهو ليس بحدّ إطلاقاً.

7. المسارات

الفعلالمسارالنطاق المطلوب
GET/pingأي مفتاح صالح
GET/lotsread_supply
PUT/lots/{external_id}write_supply
PUT/lots/{external_id}/rateswrite_supply
GET/lots/{external_id}/blocked-periodsread_supply
PUT/lots/{external_id}/blocked-periodswrite_supply
GET/bookingsread_bookings

{external_id} هو معرّفك أنت لموقف السيارات — أيّاً كان ما يسمّيه به نظامك. وهو معتِم بالنسبة إلينا: لا نحلّله أبداً، ولا يلزم أن يكون UUID. رمّزه بترميز النسبة المئوية إن احتوى على / أو على مسافة. ومعرّفات Parkena الداخلية لا تُرسَل إليك أبداً ولا تُقبَل منك أبداً.

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

GET /ping

يتحقّق من مفتاح ويخبرك بما يجوز له. وهذا هو المسار الذي تستعمله حين لا يعمل تكامل — فكل رفض آخر في هذه الواجهة عاجز عن قصد عن إخبارك أيٌّ من ستة أسباب وقع. وهو لا ينشئ سياق مشغّل ولا يقرأ أي صفوف.

curl "$BASE/ping" -H "Authorization: Bearer $KEY.$SECRET"
#
{
  "ok": true,
  "key_id": "pk_api_3f9c…",
  "scopes": ["read_supply", "write_supply", "read_bookings"],
  "server_time": "2026-08-19T15:12:09.870151Z"
}

server_time هو ساعتنا نحن، بتوقيت UTC. وهو للعلم فقط — فأنت لا ترسل إلينا وقتاً أبداً.

GET /lots

مواقف سياراتك، ومع كلٍّ منها مغلّف سعره لدى Parkena.

معامل الاستعلامالنوعالافتراضيملاحظات
afterنصاستأنف بعد external_id هذا. استخدم قيمة next_after من الصفحة السابقة.
limitعدد صحيح 1–20050طلب أكثر من ذلك يُرفض، ولا يُخفَّض بصمت.

والترقيم يجري على external_id الخاص بك، تصاعدياً — لا على إزاحة، فلا ينزلق حدُّ الصفحة تحت كتابة متزامنة.

curl "$BASE/lots?limit=50" -H "Authorization: Bearer $KEY.$SECRET"
#
{
  "lots": [
    {
      "external_id": "LOT-1",
      "name": "Terminal Park",
      "timezone": "Europe/Berlin",
      "handover": "self",
      "online_bookable": true,
      "capacity": 250,
      "latitude": "52.520000",
      "longitude": "13.405000",
      "city": "Berlin",
      "country_code": "DE",
      "features": ["indoor", "valet"],
      "arrival": null,
      "media": null,
      "min_advance_days": 1,
      "min_stay_days": 1,
      "max_stay_days": 60,
      "cancellation": {
        "free_until_hours_before_check_in": 48,
        "penalty_percent_after": "50.00",
        "no_show_forfeits_full": true
      },
      "rates": {
        "channel": "parkena",
        "currency": "EUR",
        "base_price": "12.50",
        "floor_price": "9.00",
        "ceiling_price": "18.00",
        "min_first_day_price": "11.00",
        "dynamic": false,
        "valid_from": null,
        "valid_to": null
      }
    }
  ],
  "next_after": null
}

next_after لا يظهر إلا حين تكون الصفحة قد امتلأت. اتبعه حتى يصير null وتكون قد رأيت مجموعة مواقفك كاملةً مرة واحدة بالضبط. وrates تكون null لموقف سيارات لا سعر له لدى Parkena بعد.

PUT /lots/{external_id}

إنشاء أو تحديث موقف سيارات واحد. طلب واحد، موقف واحد — لا توجد نقطة نهاية للدفعات، وشكل المسار هو ما يفرض ذلك. وجسمٌ على هيئة مصفوفة يُرفض بـ too_many_lots.

PUT تمثيل كامل. فغياب مفتاح يعني null، لا «اتركه كما كان». أرسل الموقف كاملاً في كل مرة. فالبديل يجعل مسح حقلٍ مستحيلاً، ويحوّل مفتاحاً مكتوباً خطأً إلى عملية بلا أثر، دائمة وصامتة.

الحقلالنوعمطلوبملاحظات
external_idنصلاإن وُجد فيجب أن يساوي الذي في المسار. يُتحقَّق منه ولا يُستخدَم.
nameنص، ≤200نعم
timezoneنصنعماسم IANA، مثل Europe/Berlin. وكل حساب لحدود اليوم يمرّ عبره.
handoverنصنعمself أو attended.
online_bookableقيمة منطقيةنعمزرّ البيع لديك. وهو مطلوب لهذا السبب بالضبط: فوضع قيمة افتراضية له كان سيسحب موقفاً حيّاً من البيع عند أول دفعة ناقصة.
capacityعدد صحيح 0–1000000لاإجمالي الأماكن. لا الأماكن الشاغرة الليلة — انظر التحذير أدناه.
latitudeرقم أو نصلايُقرَّب إلى 6 منازل عشرية. ويجب إرساله مع longitude.
longitudeرقم أو نصلايُقرَّب إلى 6 منازل عشرية. ويجب إرساله مع latitude.
cityنص، ≤120لا
country_codeنصلاISO 3166-1 alpha-2، مثل DE.
featuresمصفوفة نصوصلاانظر القائمة أدناه. والترتيب لا يهمّ — نحن نرتّبها.
arrivalكائنلاإرشادات وصول حرّة الصياغة.
mediaكائنلامراجع وسائط حرّة الصياغة.
min_advance_daysعدد صحيح 0–365لاقيمته الافتراضية 0.
min_stay_daysعدد صحيح 1–365لا
max_stay_daysعدد صحيح 1–365لاالثلاثة تتوقّف عند 365، وهو أبعد ما تسعّر Parkena إقامةً إليه أصلاً. وأي قيمة أكبر ستُقبل ولن تُطبَّق أبداً.
cancellationكائن أو nullلاالمفاتيح الثلاثة كلّها أو لا شيء منها — انظر أدناه.

features يقبل: indoor، security، cameras، gate_automation، plate_recognition، ev_charging، disabled_access، oversize_vehicle، valet.

cancellation كائن متداخل واحد، وهو كلّه أو لا شيء منه:

"cancellation": {
  "free_until_hours_before_check_in": 48,
  "penalty_percent_after": "50.00",
  "no_show_forfeits_full": true
}

وإرسال واحد أو اثنين من الثلاثة هو 422 incomplete_cancellation_policy. فسياسةٌ لها نافذة مجانية بلا غرامة معلنة ليست سياسة معروفة جزئياً، بل سؤال استرداد بلا جواب. أرسل null أو احذف المفتاح للدلالة على «لا سياسة».

capacity هو إجمالي الأماكن، لا الإتاحة

إن كانت تغذيتك الليلية تدفع «الأماكن الشاغرة الليلة» إلى capacity، فأنت تخبر Parkena بأن موقفك قد انكمش، وتُبطل مراجعة إدراجك كل ليلة دون استثناء، لأن capacity محتوى خاضع للمراجعة.

والإتاحة الحيّة آلية مختلفة وليست جزءاً من الإصدار v1. وهذه الواجهة لا تستطيع كشف الخطأ، لأن الرقم رقم.

curl -X PUT "$BASE/lots/LOT-1" \
  -H "Authorization: Bearer $KEY.$SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
        "name": "Terminal Park",
        "timezone": "Europe/Berlin",
        "handover": "self",
        "online_bookable": true,
        "capacity": 250,
        "latitude": 52.5200,
        "longitude": 13.4050,
        "city": "Berlin",
        "country_code": "DE",
        "features": ["valet", "indoor"],
        "min_advance_days": 1,
        "min_stay_days": 1,
        "max_stay_days": 60,
        "cancellation": {
          "free_until_hours_before_check_in": 48,
          "penalty_percent_after": "50.00",
          "no_show_forfeits_full": true
        }
      }'
دفعة كاملة.
{
  "external_id": "LOT-1",
  "result": "created",
  "changed": ["name", "timezone", "handover", "…"],
  "listing_review_superseded": false,
  "lot": { "…": "the car park as it now stands" }
}
201 حين يُنشأ موقف السيارات، و200 فيما عدا ذلك.
الحقلالمعنى
resultcreated أو updated أو unchanged.
changedأسماء الحقول التي اختلفت فعلاً. وهي فارغة عند unchanged.
listing_review_supersededtrue إن أبطلت هذه الكتابة مراجعة إدراجك لدى Parkena — انظر القسم 8.
lotموقف السيارات المخزَّن، مقروءاً من جديد.

result: "unchanged" تعني أنه لم تصدر أي عبارة على الإطلاق — لا قفل صفّ، ولا كتابة، ولا مُشغِّل. وهذه هي النتيجة الطبيعية والمتوقَّعة لإعادة دفع ليلية، وهي ما يمنع هذه الواجهة من إسقاطك من القائمة كل ليلة.

PUT /lots/{external_id}/rates

اضبط مغلّف سعر Parkena لموقف سيارات واحد. فخطة الأسعار ليست سعراً — بل هي المدى الذي تقبل البيع ضمنه على قناة Parkena: حدّ أدنى، وحدّ أعلى، وسعر أساسي بينهما.

وهذا المسار يكتب في قناة parkena وحدها لا غير. أما تسعيرك المباشر الخاص بك فهو ملكك، ولا تستطيع هذه الواجهة أن تمسّه.

ويُقبل شكلان للجسم. مدى:

{
  "floor_price": "9.00",
  "base_price": "12.50",
  "ceiling_price": "18.00",
  "min_first_day_price": "11.00",
  "dynamic": false
}

أو سعر ثابت واحد، يتمدّد ليصير الحد الأدنى = الأساسي = الحد الأعلى:

{ "fixed_price": "12.50" }
الحقلالنوعمطلوبملاحظات
fixed_priceعدد عشريأحد الشكلين أو الآخرلا يمكن جمعه مع حقول المدى.
floor_priceعدد عشريمع المدىيجب أن يكون ≤ base_price.
base_priceعدد عشريمع المدىيجب أن يقع بين الحد الأدنى والحد الأعلى.
ceiling_priceعدد عشريمع المدىيجب أن يكون ≥ base_price.
min_first_day_priceعدد عشري أو nullلايجب أن يقع داخل المغلّف.
currencyنصلاISO 4217. قيمته الافتراضية هي عملة التسوية لديك، ولا يمكن أن يكون غيرها.
dynamicقيمة منطقيةلاقيمته الافتراضية false.
valid_fromYYYY-MM-DD أو nullلاتاريخ تقويمي. والطابع الزمني يُرفض ولا يُقتطَع.
valid_toYYYY-MM-DD أو nullلايجب ألّا يسبق valid_from.

المال نصّ، وهو يُرفض بدل أن يُقرَّب. أرسل "12.50"، لا 12.345. والمبالغ تحمل منزلتين عشريتين؛ والمنزلة الثالثة هي 422 invalid_body، لأن سعراً لم تكتبه ليس سعراً وافقت عليه. أما الإحداثيات فعلى العكس — فهي قياس، ولذلك تُقرَّب.

وإرسال fixed_price ومدى معاً هو 422. فتخمين أيّهما قصدت هو الطريق إلى موقف سيارات مسعَّر عند الطرف الخاطئ من مداه الذي وضعته أنت.

{
  "external_id": "LOT-1",
  "result": "updated",
  "changed": ["floor_price"],
  "envelope_widened": true,
  "rates": {
    "channel": "parkena",
    "currency": "EUR",
    "base_price": "12.50",
    "floor_price": "7.00",
    "ceiling_price": "18.00",
    "min_first_day_price": "11.00",
    "dynamic": false,
    "valid_from": null,
    "valid_to": null
  }
}
201 عند الإنشاء، و200 فيما عدا ذلك.

envelope_widened تكون true حين تدفع هذه الكتابة المغلّف إلى الخارج — خفض الحد الأدنى، أو رفع الحد الأعلى، أو تغيير العملة، أو قلب dynamic — أو حين لم يكن هناك مغلّف Parkena من قبل. والتوسيع هو ما قد يكلّفك مراجعة إدراج.

وهي إشارة متحفّظة، ونفضّل قول ذلك على المبالغة فيها: فنحن نقارن بالمغلّف الذي كان حيّاً قبل لحظة، لا بالذي وافق عليه مراجع، لأن هذه الواجهة عاجزة عن قصد عن قراءة حالة مراجعتك. ولذلك قد تُبلّغ عن true في حالة لا تُبطل شيئاً. لكنها لن تُبلّغ عن false وقد أُبطل شيء.

GET /lots/{external_id}/blocked-periods

قائمة الفترات المحجوبة لموقف السيارات: كل مدى تواريخ سُحب فيه من البيع على Parkena، أيّاً كان من كتبه — فمزامناتك ولوحة التحكم تكتبان القائمة نفسها. الفترة المحجوبة توقف مبيعات Parkena الجديدة للإقامات التي تلمسها، ولا تفعل شيئاً غير ذلك: لا تلغي شيئاً، ولا تمسّ مبيعاتك المباشرة. والتاريخان كلاهما مشمولان — فـ ends_on هو آخر يوم محجوب لا اليوم الذي يليه، والفترة التي يتساوى فيها starts_on وends_on تحجب ذلك اليوم الواحد بعينه.

curl "$BASE/lots/LOT-1/blocked-periods" -H "Authorization: Bearer $KEY.$SECRET"
#
{
  "external_id": "LOT-1",
  "blocked_periods": [
    { "starts_on": "2026-11-02",
      "ends_on":   "2026-11-08",
      "reason":    "resurfacing",
      "overlapping_bookings": 2 },
    { "starts_on": "2026-12-24",
      "ends_on":   "2026-12-26",
      "reason":    null,
      "overlapping_bookings": 0 }
  ]
}
تعود الفترات والأقدم أولاً. وreason تسميتك أنت، وnull حين لم تُعطَ تسمية.

overlapping_bookings عدّادٌ للشفافية: كم حجزاً لهذا الموقف — من أي قناة، وبأي حالة عدا cancelled — له إقامة تلمس الفترة. والمقارنتان كلتاهما مشمولتان، على الأيام التقويمية للموقف نفسه: فالحجز الذي يغادر صباح أول يوم من الفترة لا يزال يُحسب، وكذلك الحجز الذي يصل مساء آخر يوم منها. وانتبه إلى ما يشمله «عدا cancelled»: فالإقامة المكتملة تُحسب أيضاً. العدّاد يجيب عن سؤال «ما الذي بيع في هذه التواريخ» بما فيه التاريخ الماضي — وهو ليس عدد السيارات القادمة التي سيتركها الحجب عالقة، ولذلك قد يظهر أعلى من لوحة التحذير في لوحة التحكم التي تطرح ذلك السؤال الأضيق. إنه عدّاد لا غير: لا مرجع ولا اسم ولا لوحة سيارة ولا تاريخ لأي حجز في الاستجابة.

PUT /lots/{external_id}/blocked-periods

يستبدل بقائمة الفترات المحجوبة للموقف كاملةً القائمةَ التي في الجسم. لا يوجد نداء «أضف فترة واحدة» ولا سبيل إلى مخاطبة فترة بعينها — فمزامنةٌ لا تعرف إلا الدمج مزامنةٌ لا تعرف الحذف أبداً، والفترة التي رُفعت في نظامك كانت ستبقى على Parkena إلى الأبد. مئة مدخلة على الأكثر؛ وكل تاريخ يومٌ تقويمي حقيقي بين 2020-01-01 و2032-12-31؛ وends_on لا يسبق starts_on أبداً؛ ولا يجوز أن تتداخل مدخلتان — شمولاً: فالزوج الذي يتقاسم يوماً واحداً يتصادم.

القائمة التي ترسلها هي القائمة التي توجد

PUT هنا تمثيلٌ كامل، القاعدة نفسها التي في PUT /lots/{external_id} — ولهذه القاعدة على هذا المسار عاقبةٌ تستحق الحروف الكبيرة: الفترات المكتوبة في لوحة التحكم جزءٌ من القائمة نفسها. فإن أدخل إنسانٌ فترة محجوبة في لوحة التحكم يوم الثلاثاء، ودفعت مزامنتك الليلية فتراتها هي وحدها ليلة الثلاثاء، أزالت المزامنة فترة ذلك الإنسان — بصمت، وبشكل صحيح، لأنك قلت لنا إن القائمة المرسلة هي القائمة كلها.

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

curl -X PUT "$BASE/lots/LOT-1/blocked-periods" \
  -H "Authorization: Bearer $KEY.$SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
        "blocked_periods": [
          { "starts_on": "2026-11-02", "ends_on": "2026-11-08", "reason": "resurfacing" },
          { "starts_on": "2026-12-24", "ends_on": "2026-12-26" }
        ]
      }'
استبدال كامل. الفترة الثانية بلا reason، وهذا مسموح.
الحقلالنوعمطلوبملاحظات
starts_onYYYY-MM-DDنعمأول يوم محجوب، شمولاً. تاريخ تقويمي — والطابع الزمني يُرفض ولا يُقتطع.
ends_onYYYY-MM-DDنعمآخر يوم محجوب، شمولاً — لا اليوم الذي يليه. ولا يجوز أن يسبق starts_on.
reasonنص ≤200 أو nullلاتسمية تعرضها لوحة التحكم. الفارغة تُرفض؛ وnull والغياب كلاهما يعني «بلا سبب». تُخزَّن حرفياً ولا تُشذَّب أبداً — فمسافة متغيّرة فترةٌ متغيّرة.

كل رفض محتوى على هذا المسار يحمل الرمز نفسه، 422 {"error":"invalid_blocked_periods"}، مع field يسمّي المدخلة المخطئة بإحداثيات جسمك أنت — blocked_periods[3].ends_on. أما خطآ الغلاف فيحتفظان بالرمزين اللذين يمنحهما لهما سائر الواجهة: المفتاح الزائد في المستوى الأعلى unknown_field، ومفتاح blocked_periods الغائب missing_field.

ما الذي كان خاطئاً`field`
أكثر من 100 مدخلة، أو blocked_periods ليس مصفوفةblocked_periods
ليس يوماً تقويمياً حقيقياً (2026-02-30)، أو طابع زمني، أو خارج 2020..2032starts_on / ends_on المدخلة
ends_on قبل starts_onends_on المدخلة
مدخلتان تتداخلان (شمولاً — يكفي تقاسم يوم واحد)starts_on المدخلة التي تبدأ لاحقاً
reason فارغ، أو ليس نصاً، أو أطول من 200 حرفreason المدخلة

الفترات التي تتداخل مع حجوزات مبيعة لا تُرفض. فمزامنة الآلة لا يجوز أن تنحشر بسبب سيارة بيعت الأسبوع الماضي؛ والحجوزات باقية — فالحجب يوقف المبيعات الجديدة فقط. وبدل الرفض تحمل الاستجابة عدّاد overlapping_bookings لكل فترة (المعنى أعلاه، والإقامات المكتملة مشمولة)، ليرى نظامك — والإنسان خلفه — بالضبط أيُّ الفترات المحجوبة فيها سيارات مبيعة فعلاً.

{
  "external_id": "LOT-1",
  "result": "updated",
  "added": 1,
  "removed": 1,
  "blocked_periods": [
    { "starts_on": "2026-11-02",
      "ends_on":   "2026-11-08",
      "reason":    "resurfacing",
      "overlapping_bookings": 2 },
    { "starts_on": "2026-12-24",
      "ends_on":   "2026-12-26",
      "reason":    null,
      "overlapping_bookings": 0 }
  ]
}
201 إذا لم يكن للموقف أي فترة من قبل، و200 فيما عدا ذلك.
الحقلالمعنى
resultcreated (كان صفر فترات فصار فيه شيء)، أو updated (كل ما عداه ممّا كتب — بما فيه PUT بقيمة [] أفرغ القائمة)، أو unchanged.
added / removedعدد الصفوف الذي حرّكته الكتابة فعلاً. وكلاهما 0 عند unchanged.
blocked_periodsالقائمة المخزّنة، معادةً قراءتها بعد الكتابة بعدّادات overlapping_bookings طازجة — ما تحمله قاعدة البيانات الآن، لا صدى جسمك أبداً.

قاعدة «قارن قبل أن تحدّث» من القسم 2 تسري هنا في صورة قائمة: result: "unchanged" يعني أن القائمة المخزّنة وجسمك كانا يقولان الشيء نفسه، وأنه لم تصدر أي عبارة إطلاقاً. وإعادة دفع قائمة فترات لم تتغيّر كل ليلة تكلّف قراءة واحدة.

GET /bookings

حجوزات Parkena الخاصة بك، على هيئة مؤشّر سحب. هذا المؤشّر هو مصدر الحقيقة: يمكن لنقطة نهاية webhook مسجَّلة أن تتلقّى تلميحاً موقَّعاً booking.changed يقول لك اسحبه أبكر — القسم 13 — لكن التلميح لا يحمل بيانات حجز، وما تبنيه على هذا المؤشّر لا يضيع أبداً.

معامل الاستعلامالنوعالافتراضيملاحظات
sinceنصرمز next من استدعائك السابق. احذفه للدلالة على «من البداية».
limitعدد صحيح 1–500200طلب أكثر من ذلك يُرفض، ولا يُخفَّض بصمت.
curl "$BASE/bookings?since=$CURSOR" -H "Authorization: Bearer $KEY.$SECRET"
#
{
  "bookings": [
    {
      "reference": "PK0000000040",
      "external_reference": "OPS-9911",
      "lot_external_id": "LOT-1",
      "lot_name": "Terminal Park",
      "timezone": "Europe/Berlin",
      "check_in_local": "2026-09-01T08:00:00",
      "check_out_local": "2026-09-05T19:30:00",
      "parking_days": 5,
      "status": "confirmed",
      "payment_status": "paid",
      "currency": "EUR",
      "total": "56.00",
      "channel": "direct",
      "flight_number": "LH401",
      "customer": {
        "first_name": "Ada",
        "last_name": "Lovelace",
        "email": "[email protected]",
        "phone": "+49301234567"
      },
      "vehicle_plates": ["BXY4242"],
      "created_at": "2026-08-19T15:18:01.099213Z",
      "cancelled_at": null
    }
  ],
  "next": "eyJ2IjoxLCJ0IjoiMjAyNi0wOC0xOVQxNToxMzowOC44MjQxMDhaIiwi…",
  "has_more": false
}
الحقلملاحظات
referenceرقم الحجز لدى Parkena. وهو مفتاح إزالة التكرار.
external_referenceنصّك أنت، إن كان قد ضُبط. يُعاد إليك كما هو، ولا يُستعمل مفتاحاً أبداً.
check_in_local / check_out_localساعةُ الحائط في موقف السيارات نفسه، بلا أي إزاحة منطقة زمنية. اقرأهما وفق timezone.
totalنصّ عشري، لا عدد عائم.
statuspending أو confirmed أو checked_in أو completed أو cancelled.
payment_statuspending أو paid أو partial أو refunded أو expired أو not_required.

كيف تستطلع الحجوزات بشكل صحيح

cursor = load_saved_cursor()          # null on the first run
loop:
    r = GET /bookings?since=<cursor>&limit=200
    for b in r.bookings:
        upsert_into_your_system(b, key = b.reference)   # NOT external_reference
    cursor = r.next
    save_cursor(cursor)
    if not r.has_more: sleep(60)

ثلاث قواعد، وكلٌّ منها ركن لا يُستغنى عنه:

  1. أزل التكرار اعتماداً على reference. فالتسليم يجري مرة واحدة على الأقل، لا مرة واحدة بالضبط أبداً. وسترى الحجز نفسه أكثر من مرة، وذلك سلوك صحيح لا خلل.
  2. احفظ next بعد أن تكون قد خزّنت الدفعة تخزيناً دائماً، لا قبل ذلك. فإن ماتت عمليتك في منتصف دفعة، أعاد المؤشّرُ غير المحفوظ تسليمها — وهو ما تجعله القاعدة الأولى غير ضارّ.
  3. لا تصنع مؤشّراً بنفسك. فهو رمز أصدرناه نحن. ولا يوجد ?since=<an instant I chose>، وهذا الغياب مقصود: فشريكٌ يسمّي لحظةً في المستقبل سيتوقّف بصمت عن استقبال حجوزاته هو، وأول عَرَضٍ سيكون سيارةً عند الحاجز لم يسمع بها نظامه قط. والمؤشّر الذي يتجاوز علامتنا الزمنية يُعاد ضبطه إليها، فأسوأ ما يفعله رمز جرى العبث به هو أن يسلّم صفوفاً مرتين.

ولماذا قد ترى حجزاً مرة أخرى في الحال: المؤشّر الذي نعيده إليك لا يشير أبداً إلى ما بعد now() − 5 minutes. والحجوزات الأحدث من ذلك تُعاد إليك رغم ذلك — فأنت تريد حجزك الآن، لا بعد خمس دقائق — لكننا ببساطة لا نثبّت المؤشّر عندها. وهذا ليس حذراً لذاته. فمعاملة قاعدة البيانات تُختم بوقت بدئها، ولذلك قد تُثبِّت معاملةٌ بطيئةٌ صفّاً مختوماً بوقت أسبق من صفّ سبق أن سُلِّم إليك؛ ولولا هذا التأخّر لكان مؤشّرٌ قفز مباشرة إلى أحدث صفّ قد تخطّاه إلى الأبد وبصمت.

8. ما الذي يكلّفك إعادة مراجعة

القسم 2 يحمل القاعدة؛ وهذه هي قائمة الحقول التي خلفها. فتغيير أيٍّ ممّا يلي عبر PUT /lots/{external_id} يُبطل إدراجاً معلَّقاً أو معتمَداً، وتقول الاستجابة ذلك بـ "listing_review_superseded": true:

name · timezone · handover · capacity · latitude · longitude · city · country_code · features · arrival · media · min_advance_days · min_stay_days · max_stay_days · cancellation_free_until_hours_before_check_in · cancellation_penalty_percent_after · cancellation_no_show_forfeits_full

وما الذي لا يكلّفك واحدة

  • online_bookable. فهو زرّ البيع لديك، لا محتوى خاضعاً للمراجعة. وإطفاؤه يسحب موقف السيارات من البيع فوراً دون أن يُبطل شيئاً — وهذا بالضبط سبب كونه حقلاً مطلوباً في كل PUT.
  • وكل ما في مسار الأسعار ولا يوسّع المغلّف. فرفع حدّك الأدنى أو خفض حدّك الأعلى بقاءٌ داخل وعدٍ قطعته سلفاً. أما خفض الحد الأدنى أو رفع الحد الأعلى أو تغيير العملة أو قلب dynamic فهو قطع وعدٍ جديد، وقد يُبطل المراجعة — وenvelope_widened تخبرك متى.
  • valid_from / valid_to. فانتهاء الصلاحية والتجديد يُقرآن حيّاً، ولذلك فتغيير النافذة يسحب موقف السيارات من واجهة الموقع ويعيده إليها دون إنسان.

فخّان يستحقّان المعرفة

  • ترتيب المزايا لا يهمّك، لكنه كان يهمّنا. فنحن نرتّب features قبل تخزينها، ولذلك فإن ["valet","indoor"] و["indoor","valet"] هما الدفعة نفسها. ولا حاجة بك إلى الترتيب.
  • ودقّة الإحداثيات لا تهمّ. فنحن نقرّب إلى ست منازل عشرية قبل المقارنة، ولذلك فإرسال 52.5200001 كل ليلة لا يُبلّغ عن latitude بوصفه متغيّراً إلى الأبد.

ولا يوجد حذف

هذه الواجهة لا تستطيع حذف موقف سيارات ولا سعر، ولا يوجد تفويض يتيح لها ذلك. فأسلوب delete ثم insert هو أشهر أساليب الـ upsert في شيفرات الاستيراد، وهو على هذه البيانات كارثي: إذ سيأخذ موقف السيارات هوية جديدة، ويذهب معها تاريخ إدراجه وأسعاره وإتاحته — من مهمّة لا يراقبها أحد. وإزالة موقف سيارات شيء يفعله إنسان في لوحة التحكم، عن قصد، حيث يسمّي التأكيدُ كلَّ ما سيذهب معه. وموقف سيارات سبق أن استقبل حجزاً لا يمكن إزالته إطلاقاً، من أي أحد: فالحجز سجلّ مالي، وهو محفوظ، ومحفوظ معه موقف السيارات الذي أُخذ من أجله. ولإيقاف بيع موقف، اضبط "online_bookable": false.

9. الأخطاء

كل خطأ هو JSON يحمل رمز error ثابتاً تقرؤه الآلة:

{ "error": "invalid_price_envelope", "field": "floor_price" }

وfield، حين يوجد، هو اسم حقلك أنت، مُعاداً إليك. ونحن لا نُعيد أبداً رسالة قاعدة بيانات، ولا اسم قيد، ولا اسم جدول — فتلك أسماؤنا نحن ولنا أن نغيّرها، ولا ينبغي أن ينكسر تكاملك حين نفعل. فرِّع منطقك على error.

المفتاح

الحالةالرمزالمعنى
401unauthorizedانظر أدناه — فهو يغطّي ست حالات مختلفة ويرفض أن يقول أيّها.

شكل الطلب

الحالةالرمزالمعنى
404not_foundلا وجود لهذا المسار. ولا يُستعمل أبداً لمورد.
405method_not_allowedالمسار صحيح والفعل خاطئ. وترويسة Allow تسمّي الفعل الذي أردناه.
413payload_too_largeجسم يتجاوز 64 KiB.
415unsupported_media_typeلم تكن Content-Type هي application/json.
422invalid_jsonلم يكن الجسم JSON قابلاً للتحليل.
422invalid_bodyJSON سليم التكوين، بشكل خاطئ أو بقيمة غير صالحة للاستعمال.
422missing_fieldحقل مطلوب كان غائباً.
422unknown_fieldحقل لا نعرفه. مرفوض لا متجاهَل.
422field_belongs_to_another_routeحقل حقيقي، لكنه يُضبط في مكان آخر. والوحيد اليوم هو rates — انظر القسم 2.
422invalid_queryمعامل استعلام سيّئ أو غير معروف.
422invalid_cursorرمز since لم يكن من إصدارنا.
422too_many_lotsجسم على هيئة مصفوفة. طلب واحد، موقف سيارات واحد.

المحتوى

الحالةالرمزالمعنى
422invalid_timezoneليس اسم منطقة زمنية وفق IANA.
422invalid_coordinatesخارج المدى، أو خط عرض بلا خط طول.
422invalid_country_codeليس ISO 3166-1 alpha-2.
422invalid_stay_boundsmin_stay_days وmax_stay_days غير متّسقين.
422incomplete_cancellation_policyواحد أو اثنان من مفاتيح الإلغاء الثلاثة.
422invalid_price_envelopeالحد الأدنى والسعر الأساسي والحد الأعلى ليست على ترتيبها.
422invalid_first_day_pricemin_first_day_price خارج المغلّف.
422invalid_validity_windowvalid_to يسبق valid_from.
422currency_is_not_settlement_currencyليست عملة التسوية لديك.
422invalid_blocked_periodsمدخلة فترات محجوبة غير صالحة؛ وfield يسمّيها بإحداثيات جسمك أنت (blocked_periods[3].ends_on). وجدول الرفض في القسم 7.
409conflictتسابق حقيقي: دفعتان أنشأتا external_id نفسه في اللحظة ذاتها. أعد المحاولة.

أخطاؤنا نحن

الحالةالرمزالمعنى
429rate_limitedتجاوزتَ حدّاً. احترم Retry-After.
503try_againتعارض عابر في قاعدة البيانات. أعد المحاولة؛ وRetry-After مضبوطة.
500internal_errorخطؤنا نحن. وهو في سجلّاتنا بالكامل. وإعادة المحاولة معقولة.

لماذا لن يخبرك الرمز 401 بأكثر من ذلك

unauthorized يغطّي هذه الحالات كلّها ويرفض التمييز بينها:

  1. لا ترويسة Authorization، أو ترويسة لا نستطيع تحليلها.
  2. key_id لا يسمّي أي مفتاح.
  3. key_id موجود، مع سرّ خاطئ.
  4. مفتاح مُبطَل أو منتهي الصلاحية أو حساب مشغّله موقوف.
  5. مفتاح صالح ينقصه النطاق الذي يتطلّبه هذا المسار.
  6. مفتاح صالح يسمّي external_id ليس من مواقف سياراته.

والحالات 2–5 غير قابلة للتمييز حتى داخل عمليتنا نحن — فقاعدة البيانات تجيب عنها جميعاً بالطريقة ذاتها، وبالقدر نفسه من العمل المنفَّذ، حتى لا يستطيع أحد أن يعدّ قائمة المشغّلين ولا أن يرسم خريطة لما بقي عاملاً من قدرات مفتاح مسروق.

والحالة 6 تكلّفك شيئاً من الراحة، وسنقول لماذا بدل أن نكتفي بتقريرها: فمفتاح لا يحمل إلا write_supply لا يستطيع سرد مواقف السيارات. ولو أجاب external_id مجهولٌ بـ lot_not_found، لصار في يد ذلك المفتاح أداة تعداد عاملة على المجموعة نفسها التي تمنعه نطاقاته منها — أداةٌ مبنيّة من رفض. ولذلك يحصل على الرمز 401 نفسه.

GET /ping هو الجواب عن هذا كلّه. فهو يخبرك أن مفتاحك حيّ وبما يجوز له، دون أن تضطرّ إلى تخمين معنى رمز 401. فإن نجح ping ورفض مسارٌ بـ 401، فأنت أمام نطاق لا تحمله أو external_id لا تملكه — تحقّق من الاثنين مقابل GET /lots.

10. حدود المعدّل وحدود الحجم وما نسجّله

الحدّالقيمة
جسم الطلب64 KiB
مواقف السيارات لكل طلب1
صفحة GET /lots1–200، والافتراضي 50
صفحة GET /bookings1–500، والافتراضي 200
الفترات المحجوبة لكل PUT …/blocked-periods100 — قائمة واحدة، موقف سيارات واحد

حدود المعدّل دلاء رموز (token buckets) — سعة اندفاع تُعاد تعبئتها باستمرار.

الدلويُحسب علىالاندفاعإعادة التعبئة
كل الطلباتعنوان المصدر2404 / ثانية
المصادقات الفاشلةعنوان المصدر201 كل 3 ثوانٍ
القراءاتالمفتاح1202 / ثانية
الكتابات (اندفاعاً)المفتاح601 / ثانية
الكتابات (بالساعة)المفتاح10001000 / ساعة

والرفض هو 429 مع ترويسة Retry-After بثوانٍ صحيحة. والرفض لا يستهلك رمزاً، ولذلك فإعادة المحاولة لا تُبعد تعافيك عنك أكثر.

والكتابات محدودة مرّتين، والنافذة الساعية هي التي يُعوَّل عليها. فمفتاح كتابة مسرَّب يُفرّغ مجموعة مواقف يبدو تماماً كمزامنة ليلية مشروعة — المفتاح نفسه، والمسار نفسه، والشكل نفسه، وساعة الليل نفسها — ولذلك لا يستطيع حدُّ اندفاع وحده أن يفرّق بينهما، لأن المزامنة الحقيقية اندفاعٌ أيضاً. أما السقف الساعي فيحدّ مقدار ما يستطيع مفتاح مسروق واحد أن يعيد كتابته من مجموعة قبل أن يكون إنسانٌ قد نظر إليها على نحو معقول. والمشغّل الذي لديه 1000 موقف سيارات ويدفع كلّاً منها مرة في الليلة يسع داخل ذلك؛ فإن كانت مجموعتك أكبر، فاسألنا وسنرفعه بدل أن تلتفّ حوله.

ومسارا الفترات المحجوبة ينفقان من الدلاء نفسها التي ينفق منها كل شيء آخر: فـ GET يكلّف قسيمة قراءة واحدة، وPUT كتابةٌ تُحسب على دلوَي الكتابة كليهما — فالمزامنة الليلية للفترات تُحسب على السقف نفسه، 1000 في الساعة، الذي تُحسب عليه دفعات مواقفك، عن قصد، لأن مفتاحاً مسرَّباً يسحب مجموعة مواقف من البيع هو بالضبط الشكل الذي وُجد ذلك السقف ليحدّه.

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

ولاحظ أن external_id مجهولاً يستهلك هو الآخر ميزانية الإخفاق، لأنه يُعيد الرمز 401 نفسه الذي يُعيده كل شيء آخر. فمحدِّدٌ يعامل الاثنين معاملتين مختلفتين سيكون أداة استدلال على الوجود مبنيّة من 429. فإن كنت تحمل ربطاً قديماً، فوفّق بينه وبين GET /lots بدل أن تسبر.

ما الذي نسجّله

كل طلب يُنتج سطر سجلّ واحداً عندنا؛ وكل كتابة غيّرت شيئاً تُنتج سطراً ثانياً، يحمل أسماء الحقول التي تغيّرت وما إذا كان التغيير قد كلّفك مراجعة. أسماء الحقول لا قيمها — فالسجلّ يجيب عن سؤال «ماذا جرى لهذه المجموعة الليلة الماضية»، وأسعارك ليست فيه. أما key_id الخاص بك فهو فيه، وبه تستطيع أن تسألنا أيُّ تكاملاتك فعل شيئاً بعينه. وسرّك ليس فيه أبداً، بأي صورة.

11. قبل أن يستطيع موقفك أن يبيع

دفع موقف سيارات عبر هذه الواجهة يُنشئه، لكن الموقف الجديد لا يبدأ البيع على parkena.com لحظة أن تُعيد الواجهة 201. فبعض المطلوب محتوى تستطيع هذه الواجهة تقديمه، وبعضه قرار يجب أن يؤكّده إنسان في لوحة التحكم.

وPUT /lots/{external_id} كاملاً مع PUT …/rates يستوفي متطلّبات السعة والسعر والموقع الجغرافي والمدينة والدولة وسياسة الإلغاء. ويبقى ما يلي معلَّقاً، ولا سبيل إليه إلا في لوحة التحكم:

  • تأكيد المنطقة الزمنية وقواعد الحجز — فهي مشتقّة وذات قيم افتراضية، وجوابٌ خاطئ لكنه معقول يسعّر الحجوزات خطأً وبصمت، ولذلك يؤكّدها إنسان مرة واحدة.
  • إرشادات الوصول وصورة رئيسية، من أجل الإدراج العلني.
  • قبول اتفاقية الإدراج لدى Parkena، مرة واحدة للحساب كلّه.
  • تقديم موقف السيارات للمراجعة.

والأخير مقصود: فتقديم موقف سيارات إلى مراجع بشري تصريحٌ تدلي به عن عملك، وآلة تحمل مفتاحاً لا ينبغي أن تستطيع الإدلاء به نيابة عنك.

ولوحة التحكم تعرض كل متطلّب، وهل استُوفي، وما الذي يحميه. وهذا ليس قيداً ننوي رفعه في الإصدار v1.

12. ما لا يفعله الإصدار v1

نقولها صراحةً، لأن تكاملاً مبنيّاً على افتراض لم نقطعه قطّ أسوأ من تكامل مبنيّ على ثغرة موثَّقة.

  • لا أرقام إتاحة حيّة. صار بوسعك سحب مديات تواريخ كاملة من البيع عبر PUT /lots/{external_id}/blocked-periods — القسم 7 — لكن لا سبيل بعدُ إلى دفع «الأماكن الشاغرة الليلة» رقماً، وcapacity هو إجمالي الأماكن — ووضع الإتاحة الحيّة فيه سيُبلّغ عن موقفك بشكل خاطئ ويعيد مراجعته كل ليلة. وتقويم الإتاحة بالعدّادات ليس في الإصدار v1.
  • لا حمولات في الـ webhooks. فنقطة النهاية المسجَّلة تتلقّى التلميح الموقَّع booking.changed من القسم 13 — مرجع حجز ولا شيء غيره — ويبقى مؤشّر السحب مصدر الحقيقة، تماماً كما وعدت هذه القائمة قبل أن يوجد التلميح. أما ما لا يزال غير موجود: حمولات لكل حدث (بيانات الحجز لا تسافر في webhook أبداً)، وضمانات ترتيب (التلميحات تندمج وتُعاد؛ والتسلسل شأن المؤشّر)، وواجهة إعادة إرسال (لا شيء يُعاد إرساله — أعد قراءة المؤشّر). وعلى أي تكامل أن يعمل والتلميحات مطفأة، لأن تلميحاً يُخفق خمس مرات يموت بصمت وعن قصد.
  • لا كتابة للحجوزات. فلا تستطيع عبر هذه الواجهة أن تنشئ حجزاً أو تعدّله أو تلغيه أو تسجّل وصوله أو تستردّ قيمته. لا يوجد نطاق لذلك ولا تفويض خلفه.
  • لا مدفوعات. لا رسوم، ولا مبالغ مستردّة، ولا بيانات تحويلات، ولا أرقام عمولات. وتغذية الحجوزات تحمل إجمالي البيع وعملته، ولا شيء عن كيفية حركة المال.
  • لا توزيع عبر OTA ولا عبر مدير قنوات. فهذه الواجهة تكتب في قناة parkena وحدها. وهي ليست مدير قنوات، ولا تدفع إلى أي جهة أخرى.
  • لا عمليات حذف. انظر القسم 8.
  • لا نقطة نهاية للدفعات. طلب واحد، موقف سيارات واحد.
  • لا تقديم للإدراج ولا حالة مراجعة. فلا تستطيع عبر الواجهة أن تقدّم للمراجعة ولا أن تقرأ حالة مراجعتك. وlisting_review_superseded وenvelope_widened هما الإشارتان الوحيدتان القريبتان من المراجعة، والثانية متحفّظة عن قصد.
  • لا اختيار للجهة المستأجرة. فلا يوجد tenant_id في أي جسم ولا في أي سلسلة استعلام تحلّلها هذه الواجهة. والمشغّل الذي يعمل الطلب باسمه يُستنتج من المفتاح ومن لا شيء غيره — فمعرِّف مشغّل يقدّمه الطلب كان سيكون مفتاح كتابة عابراً بين المستأجرين.
  • ولا طوابع زمنية يقدّمها المستدعي، في أي موضع. لا as_of، ولا watermark، ولا updated_since. والتواريخ الوحيدة التي يجوز لك إرسالها هي valid_from وvalid_to، وهما يومان تقويميان تصرّح بهما عن سعرك أنت. وكل ما عداهما فمن ساعتنا نحن.

13. الـ webhooks: التلميح `booking.changed`

تستطيع تسجيل نقطة نهاية HTTPS واحدة لكل حساب مشغّل، وسنرسل إليها POST بتلميح موقَّع كلّما أُنشئ حجزٌ لك أو تغيّر — أي قناة، وأي حقل. اقرأ التحذير أدناه قبل أن تصمّم أي شيء حول ذلك.

التلميح ليس بيانات. المؤشّر هو البيانات.

جسم التلميح كله اسم حدث ومرجع حجز وطابع زمني. لا حالة، ولا تواريخ، ولا مسافر، ولا مبالغ — لا شيء يستطيع نظامك التصرّف بناءً عليه مباشرة، ولا شيء يَقدُم في الطريق. والاستجابة الصحيحة الوحيدة للتلميح هي ما يفعله تكاملك أصلاً: اسحب GET /bookings بمؤشّرك المحفوظ.

الشريك الذي تتعطّل نقطة نهايته يوماً كاملاً يخسر سرعةً، لا بيانات أبداً — فالمؤشّر يعيد تسليم كل شيء في القراءة التالية. وإن كان تكاملك لا يستطيع العيش والتلميحات مطفأة، فهو مبنيٌّ خطأً.

هذا التقسيم للعمل هو سبب أن القسم 12 لم يعد يقول «لا webhooks»: فالذي رفضنا إطلاقه كان webhook يحمل الحجز نفسه، لأن webhook يفشل بصمت هو حجزٌ لا تسمع به أبداً بينما تصل السيارة إلى حاجزك فعلاً. أما التلميح فيجوز له أن يفشل بصمت ولا يكلّفك شيئاً.

التسليم

POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Parkena-Webhooks/1
Parkena-Signature: t=1767139200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
#
{"event":"booking.changed","booking_reference":"PK0000000040","occurred_at":"2026-08-27T09:15:12.114532Z"}
حدث واحد في v1 — booking.changed، اسم واحد للإنشاء والتعديل والإلغاء معاً، لأن التلميح لا يقول ما الذي حدث؛ المؤشّر هو الذي يقول.
  • booking_reference هو reference نفسه الذي تحمله تغذية الحجوزات — أعطه لإزالة التكرار عندك، تماماً كما تزيل تكرار صفوف المؤشّر.
  • occurred_at هو لحظة تسجيل التغيير عندنا، لا لحظة إرسال هذه المحاولة. وإعادات المحاولة ترسله حرفياً.

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

تسجيل نقطة نهاية، والقواعد التي عليها استيفاؤها

تُسجَّل نقاط النهاية في لوحة التحكم، على صفحة Account → API keys نفسها التي تُصدَر فيها المفاتيح، بيد مالك أو مدير — لا عبر هذه الواجهة، للسبب نفسه الذي لأجله لا تُصدَر المفاتيح عبرها. سرّ التوقيع (whsec_ يليه 64 حرفاً سداسي عشرياً) يُولَّد في متصفحك ويُعرض مرة واحدة: نخزّنه لنوقّع به، لكن لا شاشة في لوحة التحكم ولا استعلام يستطيع قراءته ثانيةً أبداً. ويجوز أن تكون نقطة نهاية واحدة نشطة لكل حساب في v1. ونقطة النهاية لا تُعدَّل أبداً — فعنوان جديد أو سرّ مُدوَّر نقطةُ نهاية جديدة (عطّل القديمة أولاً)؛ والتعطيل هو المفتاح الوحيد الذي تقدّمه لوحة التحكم بعد الميلاد.

  • HTTPS فقط، والمنفذ 443 فقط. http://، وأي منفذ صريح غير 443، لا يُجرَّب أبداً.
  • اسم مضيف لا عنوان. حرفيّات IP ‏(v4 أو v6) وlocalhost وكل ما يقع على نطاقات منصّتنا نحن، كلها تُرفض.
  • إعادات التوجيه لا تُتبع أبداً. فإعادة التوجيه عنوانٌ ثانٍ لم يفحصه أحد؛ والمحاولة تفشل بدل ذلك.
  • مهلة خمس ثوانٍ، وما بعد رمز الحالة لا تُقرأ استجابتك أبداً. أجب بسرعة واعمل بعدها — والشكل الصحيح هو «ضع في الطابور وأعد 204».

عقد 2xx، وإعادات المحاولة، والموت

أي 2xx داخل المهلة يعني أنه سُلِّم. وكل ما عداه — 4xx أو 5xx أو مهلة منقضية أو اتصال مرفوض — يُعاد وفق تراجع تدريجي ثابت، وبعد المحاولة الخامسة الفاشلة يموت التلميح: يُسجَّل آخر رمز HTTP وسبب الإخفاق ويظهران في لوحة التحكم، ولا محاولة بعد ذلك. أما الحجز نفسه فكالعادة ينتظر على المؤشّر. وتعطيل نقطة النهاية يقتل تلميحاتها المعلّقة في المرور التالي، بدل أن تُسلَّم لاحقاً إلى نقطة نهاية أطفأتها أنت.

المحاولات الفاشلة حتى الآنالمحاولة التالية
1بعد دقيقة واحدة
2بعد 5 دقائق
3بعد 30 دقيقة
4بعد ساعتين
5لا محاولة — التلميح ميت، وآخر حالة له وسببها مسجّلان

التحقّق من التوقيع

كل تسليم يحمل ترويسة Parkena-Signature واحدة: t=<unix-seconds>,v1=<hex HMAC-SHA-256>. والحمولة الموقَّعة هي السلسلة الحرفية t + . + جسم الطلب الخام — وقّع البايتات التي وصلتك، لا إعادة تسلسل لِـ JSON بعد تحليله. وهذا عمداً هو مخطط Stripe نفسه لتوقيع الـ webhooks، بما فيه سابقة السر whsec_: فأي متحقّق webhooks لـ Stripe يعمل عندك أصلاً — أو المتحقّق المنشور في مستودع Parkena نفسه — يتحقّق من هذه التسليمات دون تغيير.

# header: "t=1767139200,v1=5257a869…"   secret: "whsec_…" exactly as shown once
const pairs = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = hexHmacSha256(secret, `${pairs.t}.${rawBody}`);
const fresh = Math.abs(nowSeconds() - Number(pairs.t)) <= 300;
const ok = fresh && timingSafeEqual(pairs.v1, expected);
الكل، في مسوّدة.

ثلاث تفاصيل تخطئها التنفيذات المتعجّلة: قارن بمساواة ثابتة الزمن لا بـ ===، حتى لا يكشف زمن الاستجابة كم كان صحيحاً من توقيع مزوَّر؛ وارفض t أقدم من بضع دقائق — ننصح بـ 300 ثانية — فهذا ما يجعل تسليماً ملتقَطاً بلا قيمة لإعادة تشغيله لاحقاً؛ وإن كنت تحلّل الترويسة تحليلاً صحيحاً بدل قصّها بسذاجة، فتحقّق من كل زوج v1= موجود واقبل إن طابق أيٌّ منها — فهذا ما يمنع تدويراً مستقبلياً لسرّ التوقيع من كسرك في منتصف النافذة.

التلميح الذي يفشل في تحقّقك ليس تسليماً من Parkena. أجبه بـ 401 ولا تفعل شيئاً آخر — وبخاصة، لا تقرأ المؤشّر على الإيقاع الذي يمليه عليك. قراءة المؤشّر آمنة دائماً؛ أما الرفض فغايته ألّا تدع مستدعياً غير موثَّق يقود إيقاع نظامك.

14. كيف تحصل على وصول، وكيف تحصل على مساعدة

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

اكتب إلى [email protected]. وللدعم على تكامل يعمل بالفعل: اذكر key_id الخاص بك — لا سرّك أبداً — وexternal_id والطابع الزمني للطلب الذي تسأل عنه. فكلاهما يظهر في سجلّاتنا، وهما معاً يحدّدان طلباً واحداً بعينه.

اسأل عن البرنامج التجريبي.

أخبرنا ما هو نظامك وما الذي تريد أن يدفعه. وسنقول لك بصراحة إن كان الإصدار v1 يغطّيه — فالقسم 12 هو القائمة الكاملة لما لا يستطيع فعله، مكتوبةً بالكامل حتى تستطيع أن تقرّر رفضه قبل أن تبني أي شيء.