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 }والاستجابة الثانية ليست تحذيراً يمكن تجاهله: فذلك الموقف قد غادر واجهة الموقع، ويبقى خارجها إلى أن يُوافَق عليه مجدداً. والقسم 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_id—pk_api_متبوعاً بـ 32 خانة ست عشرية. هذا هو النصف العلني. وهو يظهر في سجلّاتنا عن قصد، ويمكنك بأمان أن تضعه في ملف إعدادات أو تذكره في طلب دعم. ومعرفته لا تُبلغ صاحبها أبعد من معرفة اسم مستخدم.secret—pk_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 ولن يوجد. فانتهاء الصلاحية عددُ أيام يحوّله الخادم مقابل ساعته هو؛ وهذه الواجهة لا تقبل طابعاً زمنياً يقدّمه المستدعي، في أي موضع، على أي مسار.
التدوير
لا يوجد استدعاء للتدوير، لأن التدوير دون انقطاع ليس إلا استدعاءين بالترتيب الصحيح:
- أصدر مفتاحاً ثانياً بالنطاقات نفسها.
- انشره في نظامك وتأكّد من أن الطلبات تتدفّق —
GET /pingبالمفتاح الجديد، ثم راقبlast_used_atعليه. - أبطل المفتاح القديم.
والمفتاحان حيّان بين الخطوة الأولى والثالثة. ولا يوجد حدّ يمنعك من حمل اثنين.
الإبطال
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 | /lots | read_supply |
PUT | /lots/{external_id} | write_supply |
PUT | /lots/{external_id}/rates | write_supply |
GET | /lots/{external_id}/blocked-periods | read_supply |
PUT | /lots/{external_id}/blocked-periods | write_supply |
GET | /bookings | read_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–200 | 50 | طلب أكثر من ذلك يُرفض، ولا يُخفَّض بصمت. |
والترقيم يجري على 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 فيما عدا ذلك.| الحقل | المعنى |
|---|---|
result | created أو updated أو unchanged. |
changed | أسماء الحقول التي اختلفت فعلاً. وهي فارغة عند unchanged. |
listing_review_superseded | true إن أبطلت هذه الكتابة مراجعة إدراجك لدى 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_from | YYYY-MM-DD أو null | لا | تاريخ تقويمي. والطابع الزمني يُرفض ولا يُقتطَع. |
valid_to | YYYY-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_on | YYYY-MM-DD | نعم | أول يوم محجوب، شمولاً. تاريخ تقويمي — والطابع الزمني يُرفض ولا يُقتطع. |
ends_on | YYYY-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..2032 | starts_on / ends_on المدخلة |
ends_on قبل starts_on | ends_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 فيما عدا ذلك.| الحقل | المعنى |
|---|---|
result | created (كان صفر فترات فصار فيه شيء)، أو 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–500 | 200 | طلب أكثر من ذلك يُرفض، ولا يُخفَّض بصمت. |
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 | نصّ عشري، لا عدد عائم. |
status | pending أو confirmed أو checked_in أو completed أو cancelled. |
payment_status | pending أو 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)ثلاث قواعد، وكلٌّ منها ركن لا يُستغنى عنه:
- أزل التكرار اعتماداً على
reference. فالتسليم يجري مرة واحدة على الأقل، لا مرة واحدة بالضبط أبداً. وسترى الحجز نفسه أكثر من مرة، وذلك سلوك صحيح لا خلل. - احفظ
nextبعد أن تكون قد خزّنت الدفعة تخزيناً دائماً، لا قبل ذلك. فإن ماتت عمليتك في منتصف دفعة، أعاد المؤشّرُ غير المحفوظ تسليمها — وهو ما تجعله القاعدة الأولى غير ضارّ. - لا تصنع مؤشّراً بنفسك. فهو رمز أصدرناه نحن. ولا يوجد
?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.
المفتاح
| الحالة | الرمز | المعنى |
|---|---|---|
401 | unauthorized | انظر أدناه — فهو يغطّي ست حالات مختلفة ويرفض أن يقول أيّها. |
شكل الطلب
| الحالة | الرمز | المعنى |
|---|---|---|
404 | not_found | لا وجود لهذا المسار. ولا يُستعمل أبداً لمورد. |
405 | method_not_allowed | المسار صحيح والفعل خاطئ. وترويسة Allow تسمّي الفعل الذي أردناه. |
413 | payload_too_large | جسم يتجاوز 64 KiB. |
415 | unsupported_media_type | لم تكن Content-Type هي application/json. |
422 | invalid_json | لم يكن الجسم JSON قابلاً للتحليل. |
422 | invalid_body | JSON سليم التكوين، بشكل خاطئ أو بقيمة غير صالحة للاستعمال. |
422 | missing_field | حقل مطلوب كان غائباً. |
422 | unknown_field | حقل لا نعرفه. مرفوض لا متجاهَل. |
422 | field_belongs_to_another_route | حقل حقيقي، لكنه يُضبط في مكان آخر. والوحيد اليوم هو rates — انظر القسم 2. |
422 | invalid_query | معامل استعلام سيّئ أو غير معروف. |
422 | invalid_cursor | رمز since لم يكن من إصدارنا. |
422 | too_many_lots | جسم على هيئة مصفوفة. طلب واحد، موقف سيارات واحد. |
المحتوى
| الحالة | الرمز | المعنى |
|---|---|---|
422 | invalid_timezone | ليس اسم منطقة زمنية وفق IANA. |
422 | invalid_coordinates | خارج المدى، أو خط عرض بلا خط طول. |
422 | invalid_country_code | ليس ISO 3166-1 alpha-2. |
422 | invalid_stay_bounds | min_stay_days وmax_stay_days غير متّسقين. |
422 | incomplete_cancellation_policy | واحد أو اثنان من مفاتيح الإلغاء الثلاثة. |
422 | invalid_price_envelope | الحد الأدنى والسعر الأساسي والحد الأعلى ليست على ترتيبها. |
422 | invalid_first_day_price | min_first_day_price خارج المغلّف. |
422 | invalid_validity_window | valid_to يسبق valid_from. |
422 | currency_is_not_settlement_currency | ليست عملة التسوية لديك. |
422 | invalid_blocked_periods | مدخلة فترات محجوبة غير صالحة؛ وfield يسمّيها بإحداثيات جسمك أنت (blocked_periods[3].ends_on). وجدول الرفض في القسم 7. |
409 | conflict | تسابق حقيقي: دفعتان أنشأتا external_id نفسه في اللحظة ذاتها. أعد المحاولة. |
أخطاؤنا نحن
| الحالة | الرمز | المعنى |
|---|---|---|
429 | rate_limited | تجاوزتَ حدّاً. احترم Retry-After. |
503 | try_again | تعارض عابر في قاعدة البيانات. أعد المحاولة؛ وRetry-After مضبوطة. |
500 | internal_error | خطؤنا نحن. وهو في سجلّاتنا بالكامل. وإعادة المحاولة معقولة. |
لماذا لن يخبرك الرمز 401 بأكثر من ذلك
unauthorized يغطّي هذه الحالات كلّها ويرفض التمييز بينها:
- لا ترويسة
Authorization، أو ترويسة لا نستطيع تحليلها. key_idلا يسمّي أي مفتاح.key_idموجود، مع سرّ خاطئ.- مفتاح مُبطَل أو منتهي الصلاحية أو حساب مشغّله موقوف.
- مفتاح صالح ينقصه النطاق الذي يتطلّبه هذا المسار.
- مفتاح صالح يسمّي
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 /lots | 1–200، والافتراضي 50 |
صفحة GET /bookings | 1–500، والافتراضي 200 |
الفترات المحجوبة لكل PUT …/blocked-periods | 100 — قائمة واحدة، موقف سيارات واحد |
حدود المعدّل دلاء رموز (token buckets) — سعة اندفاع تُعاد تعبئتها باستمرار.
| الدلو | يُحسب على | الاندفاع | إعادة التعبئة |
|---|---|---|---|
| كل الطلبات | عنوان المصدر | 240 | 4 / ثانية |
| المصادقات الفاشلة | عنوان المصدر | 20 | 1 كل 3 ثوانٍ |
| القراءات | المفتاح | 120 | 2 / ثانية |
| الكتابات (اندفاعاً) | المفتاح | 60 | 1 / ثانية |
| الكتابات (بالساعة) | المفتاح | 1000 | 1000 / ساعة |
والرفض هو 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"}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 هو القائمة الكاملة لما لا يستطيع فعله، مكتوبةً بالكامل حتى تستطيع أن تقرّر رفضه قبل أن تبني أي شيء.
