ما البوت وما الذي يصلح له
البوت نوع آخر من «حسابات المستخدمين» في Korat، لا يجلس خلفه إنسان يكتب — بل يرسل برنامجك أنت الطلب، فتظهر الرسالة في غرفة الدردشة التي دُعي إليها البوت. ومن استعمالاته الحقيقية:
- تنبيه بطلب جديد في الدردشة — يصل طلب جديد إلى نظام بيع متجرك ⇒ فيكتب البوت التنبيه فورًا في غرفة الزبون أو غرفة الفريق
- تنبيه باقتراب نفاد المخزون — ينزل الرصيد تحت الحدّ المضبوط ⇒ فيكتب البوت تحذيرًا في غرفة فريق المتجر
- تقرير حالة الخادم أو النظام — ينتهي سكربت فحص سلامة نظامك ⇒ فيكتب البوت النتيجة في غرفة المجموعة التي يتابعها فريق التقنية
ما الذي لا يستطيعه البوت (معلومات حقيقية من قاعدة البيانات، لا قيود مؤقّتة):
- لا يقرأ البوت رسائل الغرفة — فهو صادر فقط، إلا أن تضبط بنفسك خطّاف ويب (webhook) لاستقبال الرسائل (انظر المرجع التقني في آخر الصفحة). وحتى مع ضبط الخطّاف، لا يُرسَل شيء فعليًا اليوم، لأن المؤقّت الذي يسحب الطابور ويرسله لم يُضبط على الإنتاج بعد.
- لا يدعو البوت نفسه إلى غرفة — بل يدعوه دائمًا شخص موجود في تلك الغرفة (أو يملك صلاحية إدارتها)
- لا يحمل البوت الواحد أكثر من رمزين صالحين في آنٍ واحد والحساب الواحد لا ينشئ أكثر من 5 بوتات
كيف تبدأ — أربع خطوات
- 1أنشئ البوت وسمِّه واختر له اسم مستخدم (ويجب أن ينتهي اسم المستخدم بكلمة
botمثلorderbot) — وإن أردت أن يكون البوت ملكًا للمتجر لا لك وحدك، فاختر المتجر لحظة الإنشاء - 2انسخ الرمز — يعرضه النظام مرّة واحدة عند انتهاء الإنشاء، فـانسخه واحفظه في مكان آمن فورًا؛ فإن أغلقت الشاشة لم تستطع استرجاع القيمة الحقيقية أبدًا (وعليك إصدار رمز جديد)
- 3ادعُ البوت إلى الغرفة — اختر غرفة الدردشة أو غرفة المجموعة أو غرفة الفريق التي تريده أن يتكلّم فيها. وعليك أن تكون فيها أصلًا (أو تملك صلاحية إدارتها) لتستطيع الدعوة
- 4أرسل أول رسالة — خذ الرمز من الخطوة 2 واستدعِ Bot API. والأمثلة القابلة للتطبيق في القسم التالي
الخطوات 1–3 تُنفَّذ في تطبيق Korat
القائمة في الإعدادات › الحساب › «بوتاتي» — ومنها تنشئ البوت، وتُصدر الرمز وتُدوّره وتُبطله، وتدعو البوت إلى غرف الدردشة والمجموعات والفرق، كل ذلك بلا سطر شيفرة واحد. وهذه القائمة تأتي مع الإصدار التالي من تطبيق Korat — فالإصدار المتاح للتنزيل اليوم لا يحتويها. فإن فتحت الإعدادات ولم ترَ «بوتاتي» فانتظر تحديث التطبيق. وفي هذه الأثناء تستطيع تنفيذ الخطوات 1–3 عبر استدعاء RPC في Supabase مباشرةً (انظر المرجع التقني في آخر الصفحة).
🔒 الرمز هو كلمة مرور البوت
كل من يحمل هذا الرمز يستطيع الإرسال باسم البوت فورًا — فلا تُرسله عبر دردشة ولا صورة ولا بريد غير مشفّر — احفظه في خادمك أو مخزن أسرارك وحده. ولن يطلب أي موظّف في Korat رمزك لا عبر الدردشة ولا البريد ولا الهاتف — فإن ادّعى أحد أنه من فريق Korat وطلب الرمز، فاعتبره احتيالًا وأبلغ عبر اتصل بنا فورًا. وإن تسرّب الرمز فأبطِله في التطبيق في الحال (والإبطال لا رجعة فيه، وعليك إصدار رمز جديد).
مثال — إرسال أول رسالة
المنفذ الوحيد هو POST https://koratland.com/api/bot/send — ضع الرمز في ترويسة الطلب، ثم بيّن ماذا تقول وفي أي غرفة:
بـcurl (لمن اعتاد الطرفية)
curl -X POST "https://koratland.com/api/bot/send" \
-H "X-Bot-Token: kbot_12_xxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "kind": "chat", "chat_id": 9931, "text": "وصل طلب جديد" }'
استبدل kbot_12_xxxxxxxxxxxxxxxxxxxx برمز البوت الحقيقي، و9931 برقم الغرفة التي دعوت البوت إليها (وتجد رقم الغرفة في رابط الدردشة داخل التطبيق، أو في قائمة الغرف في شاشة «بوتاتي»).
بلا كتابة شيفرة (Google Apps Script)
إن لم يكن لك خادم خاص، فاستعمل Google Apps Script مجانًا بحساب Google واحد — ادخل script.google.com ← مشروع جديد ← الصق هذه الشيفرة ← اضغط «تشغيل». وفي المرّة الأولى يطلب صلاحية الوصول إلى الإنترنت (اسمح بها، فهي آمنة لأن السكربت سكربتك أنت):
function sendKoratMessage() {
var url = "https://koratland.com/api/bot/send";
var payload = {
kind: "chat",
chat_id: 9931, // ضع رقم غرفتك
text: "وصل طلب جديد" // ضع النصّ الذي تريده
};
var options = {
method: "post",
contentType: "application/json",
headers: { "X-Bot-Token": "kbot_12_xxxxxxxxxxxxxxxxxxxx" }, // ضع رمز بوتك
payload: JSON.stringify(payload)
};
var res = UrlFetchApp.fetch(url, options);
Logger.log(res.getContentText()); // انظر النتيجة في «سجل التشغيل» (Execution log)
}
ضغطة واحدة على «تشغيل» تكفي للاختبار فورًا — وإن أردت إرسالًا تلقائيًا، كأن يحدث مع كل صفّ جديد في جدول Google Sheet يسجّل الطلبات، فاستعمل مؤقّتات Apps Script (Triggers) لتستدعي هذه الدالة عند تعديل الجدول (onEdit) أو كل عدد من الدقائق.
والطريقة نفسها تصلح لأي أداة أتمتة فيها إجراء «HTTP request» أو «Webhook» أصلًا، مثل n8n أو Make أو Zapier — اضبطها على POST إلى العنوان نفسه، وأضف ترويسة X-Bot-Token وجسم JSON كما في المثال أعلاه.
المشكلات الشائعة
ما تراه (reason) | ما معناه | كيف تُصلحه |
|---|---|---|
not_invited | لم يُدعَ البوت إلى هذه الغرفة (أو دُعي ثم أُخرج منها) | ادعُ البوت إلى الغرفة من جديد من شاشة «بوتاتي» أو بـbot_invite |
invite_stale | الشخص الذي دعا البوت إلى هذه الغرفة لم يعد يملك الصلاحية فيها (خرج من المجموعة، أو خُفّض دوره، أو نُزعت عنه صلاحية إدارة غرفة الفريق، أو أُغلقت الغرفة) ⇒ فصمت البوت من تلقاء نفسه بلا أن يُخرجه أحد | اطلب من شخص ما زال يملك الصلاحية فعلًا في تلك الغرفة أن يدعو البوت من جديد — فالصلاحية تُحسب دائمًا من آخر داعٍ |
conversation_not_open | هذه الغرفة صندوق وارد للمتجر، والزبون لم يراسل بعد — ولا يستطيع المتجر (ولا بوته) مراسلته ابتداءً | انتظر مراسلة الزبون، ثم دع البوت يردّ |
rate_limited | أرسل البوت أكثر من السقف المضبوط (في الدقيقة أو في اليوم) | انتظر المدّة المذكورة في retry_after_sec ثم أعد الإرسال — وإن لزمك سقف أعلى فأبلغ الفريق عبر اتصل بنا |
blocked | حظر المستخدم في تلك الغرفة هذا البوت | لا يمكن إصلاحه من جهة المستخدم — استعمل غرفة أو بوتًا آخر إن لزم |
| كان البوت يتكلّم ثم صمت فجأةً بلا سبب ظاهر | أكثر الأسباب شيوعًا هو invite_stale أعلاه — لا عطلٌ في البوت | تحقّق هل ما زال من دعا البوت موجودًا في الغرفة ويملك صلاحيته |
وجدول رموز الأخطاء كاملًا في المرجع التقني أدناه.
المرجع التقني (للمطوّرين)
الأقسام التالية مكتوبة لمن سيبرمج استدعاء Bot API بنفسه — تفاصيل كل وسيط، وجدول رموز الأخطاء كاملًا، وخطّاف الويب لاستقبال الرسائل.
تنفيذ الخطوات 1–3 عبر RPC مباشرةً (بلا التطبيق)
إن كان إصدار تطبيقك بلا شاشة «بوتاتي»، أو أردت التنفيذ آليًا، فاستدعِ RPC في Supabase مباشرةً برمز وصول المستخدم المسجَّل دخوله (لا برمز البوت) عبر PostgREST — ثلاث خطوات:
1. إنشاء البوت — bot_create
curl -X POST "https://<SUPABASE_PROJECT>.supabase.co/rest/v1/rpc/bot_create" \
-H "apikey: <SUPABASE_ANON_KEY>" \
-H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"p_name": "بوت التنبيهات",
"p_username": "notifybot",
"p_business_id": null,
"p_about": "يبلّغ بحالة الطلبات"
}'
p_username يجب أن يتكوّن من a–z 0–9 _ بطول 4–31 محرفًا، وينتهي بـbot دائمًا (بالنمط ^[a-z][a-z0-9_]{2,29}bot$) — وأرسل p_business_id حين يكون البوت ملكًا للمتجر، وعندها يجب أن يكون المستدعي عضوًا في ذلك المتجر ويملك صلاحية bot.manage — والردّ الناجح يعيد token وهو القيمة الحقيقية مرّة واحدة في عمر ذلك الرمز؛ فقاعدة البيانات لا تحفظ إلا بصمة sha256 — وإن فاتك فعليك إصدار رمز جديد (bot_rotate_token)، ولا سبيل إلى قراءة القيمة الحقيقية ثانيةً.
{ "ok": true, "bot_id": 12, "user_id": 4821, "username": "notifybot", "token": "kbot_12_…" }
2. دعوة البوت إلى غرفة دردشة — bot_invite
يجب أن تكون فعلًا في تلك الغرفة (أو من فريق المتجر إن كانت الغرفة صندوق وارد المتجر)، وأن تملك إدارة هذا البوت (can_manage_bot) — وp_target هو chats.id
curl -X POST "https://<SUPABASE_PROJECT>.supabase.co/rest/v1/rpc/bot_invite" \
-H "apikey: <SUPABASE_ANON_KEY>" \
-H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{ "p_bot": 12, "p_kind": "chat", "p_target": 9931 }'
2-أ. دعوة البوت إلى غرفة مجموعة أو غرفة فريق — صلاحيات أشدّ من غرفة الدردشة
أرسل p_kind: "community_room" أو "business_room" مع p_target بوصفه community_rooms.id/business_rooms.id — وهذا يعمل فعلًا الآن، لكن على الداعي أن يجتاز بوّابة الصلاحية المناسبة لمعنى تلك الغرفة، لا أن يكون عضوًا فحسب:
- غرفة المجموعة — على الداعي أن يستطيع الكتابة في تلك الغرفة فعلًا وفق قواعد الغرفة نفسها (
read_role/post_role) — ففي غرفة الحديث العادية يدعو الأعضاء، أما غرف الإعلانات والغرف الخلفية المقصورة على المشرفين فلا يدعو فيها إلا مالك المجموعة أو مشرفها - غرفة فريق المتجر — يجب اجتياز بوّابات الغرفة كلها (صلاحية القراءة، وصلاحية النشر، وقواعد المتلقّين) زائد القدرة
teamroom.manageدائمًا — فإدخال بوت إلى غرفة داخلية للشركة يُعدّ «إدارةً للغرفة» لا مجرّد «كتابة فيها»
وفي الحالتين يجب أن تكون مديرًا لذلك البوت (can_manage_bot) دائمًا — فالغرف المشتركة ليست مكانًا يجرّ فيه أي أحد بوت غيره ليتكلّم.
🔴 invite_stale — أهمّ ما يجب معرفته قبل الاستعمال
صلاحية الداعي لا تُفحص وقت الدعوة وحدها — فـbot_send يسأل قواعد الغرفة من جديد مع كل رسالة، باسم الداعي نفسه ⇒ فإن خرج الداعي من المجموعة، أو خُفّض دوره، أو نُزعت عنه teamroom.manage، أو أُغلقت الغرفة في أي لحظة، حصلت رسالة البوت التالية على reason: "invite_stale" فورًا — فيصمت البوت من تلقاء نفسه بلا أن يُخرجه أحد. لا مهمّة خلفية، ولا مُطلِقات يجب أن تعمل كلها؛ إنما هو إعادة حساب للصلاحية لا غير.
إرسال رسالة — POST /api/bot/send
يُرسَل الرمز بطريقتين، اختر إحداهما:
# الطريقة 1 — Authorization: Bearer
curl -X POST "https://koratland.com/api/bot/send" \
-H "Authorization: Bearer kbot_12_xxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-9931-shipped" \
-d '{ "kind": "chat", "chat_id": 9931, "text": "غادرت الشحنة المتجر" }'
# الطريقة 2 — X-Bot-Token
curl -X POST "https://koratland.com/api/bot/send" \
-H "X-Bot-Token: kbot_12_xxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "kind": "chat", "chat_id": 9931, "text": "غادرت الشحنة المتجر" }'
وإن لم تُرسل رمزًا إطلاقًا حصلت على 401:
{ "ok": false, "reason": "bad_token", "detail": "أرسل الرمز في Authorization: Bearer … أو X-Bot-Token فقط" }
حقول الجسم
| الحقل | النوع | إلزامي | المعنى |
|---|---|---|---|
kind | string | لا (والافتراضي "chat") | يحدّد نوع الوجهة — وله ثلاث قيم صالحة: "chat" · "community_room" · "business_room" وأي قيمة أخرى تعطي reason: "bad_target" من قاعدة البيانات مباشرةً (ولا رجوع صامت إلى "chat" — فالخلل القديم الذي كان يثبّت هذه القيمة أُصلح) |
chat_id | integer > 0 | نعم (أو target_id) | معرّف الغرفة الوجهة التي دُعي إليها البوت — chats.id حين kind: "chat", community_rooms.id حين "community_room", business_rooms.id حين "business_room" (ويبقى اسم الحقل في الجسم chat_id/target_id دائمًا، ولا اسم منفصل بحسب kind) |
text | string | نعم | النصّ، ولا يكون فارغًا، ولا يتجاوز السقف الذي يحدّده السجل text_max_message (والسقف قابل للتعديل وليس رقمًا ثابتًا — انظر قيمة max في الخطأ too_long) |
client_key | string | لا | مفتاح منع التكرار، بديل عن ترويسة Idempotency-Key — بطول 8–128 محرفًا، ولا يُقبل فيه إلا A–Z a–z 0–9 _ . : - |
الدردشة الخاصّة مقابل صندوق وارد المتجر — أي حقل يحسم الأمر
kind يحدّد نوع الجدول الوجهة (غرفة دردشة / غرفة مجموعة / غرفة فريق المتجر). أما الدردشة الخاصّة وصندوق وارد المتجر فلا يفرّق بينهما kind — فكلاهما صفّ في جدول chats نفسه، والفرق في chats.business_id هل له قيمة أم لا (وجود القيمة = صندوق وارد المتجر). وفحص صلاحية الدعوة، وفحص حظر المستخدم، وقاعدة «لا يراسل المتجر الزبون ابتداءً» (conversation_not_open) تُقرأ كلها من هذا العمود أيضًا — وchat_id المرسَل يجب أن يكون غرفةً صحيحة أصلًا، وليس في الواجهة وسيط منفصل تختار به بين «خاصّ» و«تجاري».
Idempotency-Key وسقوف المعدّل
أرسل الترويسة القياسية Idempotency-Key أو الحقل client_key في الجسم — اختر إحداهما. وإن أرسلتهما معًا وجب تطابق القيمتين، وإلا حصلت على 422 idempotency_key_conflict. وإعادة إرسال الطلب نفسه بالمفتاح نفسه (الغرفة والمفتاح ذاتهما) تعيد الردّ 200 السابق مع "duplicate": true ولا تُنشئ رسالة مكرّرة.
السقوف طبقتان: لكل IP (120 طلبًا في الدقيقة عند الـWorker نفسه) ولكل بوت (افتراضيًا 20 في الدقيقة و1000 في اليوم للبوت الواحد، ويعدّلها المشرف لكل بوت على حدة). وكلتاهما تردّ 429 مع ترويسة Retry-After بالثواني (لكل IP = 60 دائمًا · لكل بوت في الدقيقة = 60 · لكل بوت في اليوم = 3600).
استقبال الرسائل — خطّاف الويب
حين يكتب أحد رسالة في غرفة دُعي إليها البوت (ولم يكن المرسِل حساب البوت نفسه — منعًا لارتداد رسائله إليه) يرسل Korat طلب POST إلى العنوان المضبوط. ويُضبط ذلك عبر RPC بحساب مستخدم مسجَّل دخوله يملك إدارة ذلك البوت (can_manage_bot) كما في خطوة طلب الرمز:
curl -X POST "https://<SUPABASE_PROJECT>.supabase.co/rest/v1/rpc/bot_set_webhook" \
-H "apikey: <SUPABASE_ANON_KEY>" \
-H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{ "p_bot": 12, "p_url": "https://your-server.example.com/korat-webhook" }'
{ "ok": true, "secret": "a1b2c3…", "note": "يُعرض هذا السرّ مرّة واحدة — احفظه لتتحقّق من التوقيع بنفسك" }
يجب أن يكون العنوان https:// حصرًا وألّا يكون مضيفًا داخليًا أو loopback أو link-local (localhost, 127.0.0.1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16 ومعها منافذ بيانات التعريف لدى كل مزوّدي السحابة) — ويُرفض ذلك منذ لحظة الضبط، ويُفحص ثانيةً بحلّ DNS فعليًا قبل كل إرسال، منعًا لأن يشير النطاق إلى مكان آخر لاحقًا. وsecret لا يُعرض إلا مرّة واحدة عند الضبط أو التدوير — وbot_rotate_webhook_secret(p_bot) يدوّره متى شئت، وbot_disable_webhook(p_bot)/bot_enable_webhook(p_bot) يعطّله ويفعّله يدويًا، وbot_webhook_status(p_bot) يعرض الحالة (ولا يعيد السرّ).
لا شيء يُرسَل فعليًا اليوم
طابور الإرسال (bot_webhook_deliveries) يعمل بصورة صحيحة ومُثبَتة في قاعدة البيانات، وسرّ مسار الإرسال (BOT_WEBHOOK_DISPATCH_SECRET) ضبطه المالك، لكن الذي يسحب الطابور ويرسله فعلًا (pg_cron الذي يجب أن يستدعي الـWorker كل دقيقة) لم يُضبط على الإنتاج بعد — فتستطيع ضبط الخطّاف، وتدخل الرسائل الطابور منتظرةً، لكن وجهتك لن تستقبل شيئًا أبدًا حتى يُضبط مؤقّت pg_cron.
شكل الطلب الذي تستقبله وجهتك
POST /korat-webhook HTTP/1.1
Content-Type: application/json
X-Korat-Signature: sha256=<hex>
X-Korat-Timestamp: 1750000000000
{
"event": "message.created",
"chat_id": 9931,
"message_id": 88213,
"text": "هل لديكم اللون الأحمر؟",
"time": "14:02",
"sender": { "id": 4821, "username": "somchai_p", "name": "سمشاي ب." }
}
هذا كل شيء — لا رقم هاتف ولا بريد ولا رمز أحد إطلاقًا في الجسم. فلا يرى البوت إلا الغرفة والنصّ وهويّة المرسِل بالقدر اللازم للردّ.
تحقّق من التوقيع قبل أن تثق بالطلب
X-Korat-Signature هو HMAC-SHA256(secret, "<timestamp>." + rawBody) مُرمَّزًا بالنظام الستّ عشري — ومثال بـNode.js:
const crypto = require("crypto");
function verify(rawBody, signatureHeader, timestampHeader, secret) {
const age = Date.now() - Number(timestampHeader);
if (!(age >= 0 && age < 5 * 60_000)) return false; // منع الإعادة: ألّا يتجاوز العمر 5 دقائق
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(`${timestampHeader}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
الردّ بـ2xx خلال 8 ثوانٍ يُعدّ نجاحًا — وأي ردّ آخر (بما فيه التحويل 3xx الذي لا يتّبعه Korat) أو عدم الردّ إطلاقًا يُعدّ فشلًا ويدخل طابور إعادة المحاولة.
إعادة المحاولة حين تتعطّل الوجهة
عند الفشل ⇒ تراجع أُسّي (1، 2، 4، 8، 16، 32، 64، 128 دقيقة) بحدّ أقصى 8 محاولات لكل رسالة ثم يتوقّف إرسال تلك الرسالة. وإن فشلت الوجهة 15 مرّة متتالية (عبر عدّة رسائل) عُطّل الخطّاف تلقائيًا مع تنبيه مالك البوت في التطبيق — فلا إرسال أبديًّا إلى وجهة ميّتة. واستدعِ bot_enable_webhook لإعادة التفعيل بعد إصلاح المشكلة عند الوجهة.
جدول رموز الأخطاء كاملًا
كل ردّ فاشل يحمل {"ok": false, "reason": "…"} دائمًا — وreason هو الكلمة التي تُبنى عليها القرارات، لا حالة HTTP وحدها.
| HTTP | reason | مصدره | المعنى / الحل |
|---|---|---|---|
| 401 | bad_token | Worker / DB | لم يُرسَل رمز، أو الرمز خاطئ أو مُبطَل أو البوت معطّل — والردّ واحد في كل هذه الحالات عن قصد، منعًا للتخمين |
| 403 | not_invited | DB | لم يُدعَ البوت إلى هذه الغرفة قطّ، أو أُخرج منها — استدعِ bot_invite أولًا |
| 403 | blocked | DB | حظر المستخدم في الغرفة هذا البوت (في الغرف التي ليست صندوق وارد متجر) |
| 403 | account_suspended | DB | أحد الطرفين في الغرفة حسابه موقوف — وهذه مسألة صلاحية لا طلب خاطئ |
| 404 | no_such_chat | DB | chat_id غير موجود |
| 404 | no_such_endpoint | Worker | مسار خاطئ، ولا يوجد إلا POST /api/bot/send |
| 405 | method_not_allowed | Worker | استُعملت طريقة غير POST |
| 409 | conversation_not_open | DB | هذه الغرفة صندوق وارد للمتجر ولم يراسل الزبون بعد — ولا يراسل المتجر (ولا بوته) ابتداءً |
| 409 | chat_closed | DB | غرفة التعارف (dating) أُغلقت أمام الرسائل |
| 422 | empty_text | DB | text فارغ بعد إزالة الفراغات |
| 422 | too_long | DB | النصّ أطول من السقف — انظر قيمة max المرفقة |
| 422 | bad_target | DB | kind ليس واحدًا من "chat"/"community_room"/"business_room" |
| 404 | no_such_room | DB | chat_id لا توجد له غرفة مجموعة ولا غرفة فريق (حُذفت الغرفة) |
| 409 | room_archived | DB | أُغلقت هذه الغرفة — فلا يكتب فيها إنسان ولا بوت |
| 403 | invite_stale | DB | أُعيد حساب صلاحية الداعي فلم تعد تمرّ — خرج من المجموعة، أو خُفّض دوره، أو نُزعت عنه teamroom.manage، أو أُغلقت الغرفة. انظر detail لمعرفة السبب الفرعي (not_a_member/room_role/not_teamroom_manager) — والإصلاح بأن يدعو البوتَ إلى الغرفة من جديد شخصٌ ما زال يملك الصلاحية (bot_invite), لا بالانتظار فحسب |
| 422 | bad_json | Worker | جسم الطلب ليس JSON صحيحًا |
| 422 | bad_chat_id | Worker | chat_id/target_id يجب أن يكون عددًا صحيحًا موجبًا |
| 422 | bad_text | Worker | text يجب أن يكون نصًّا |
| 422 | idempotency_key_conflict | Worker | أُرسلت Idempotency-Key مع client_key معًا بقيمتين مختلفتين — وإرسال إحداهما يكفي |
| 422 | bad_client_key | Worker | مفتاح منع التكرار لا يطابق الصيغة (8–128 محرفًا، A–Z a–z 0–9 _ . : -) |
| 429 | rate_limited | Worker / DB | تجاوز السقف — راجع ترويسة Retry-After وحقلَي window/retry_after_sec |
| 502 | database_error | Worker | ردّت قاعدة البيانات بغير 2xx — انظر pg_code/detail المرفقة، وهي رسالة Postgres الحقيقية |
| 503 | service_key_not_configured | Worker | الخادم لم يضبط مفتاح الخدمة بعد — وهي مشكلة عند Korat لا عند المستدعي |
| 504 | database_unreachable | Worker | فشل استدعاء قاعدة البيانات أو انتهت مهلته — انظر safe_to_retry: فإعادة الإرسال آمنة فقط إن أرسلت معها client_key/Idempotency-Key |
الردّ عند النجاح
{ "ok": true, "duplicate": false, "message_id": 88213 }
duplicate: true تعني أن هذا الطلب أُرسل من قبل (بمنع التكرار عبر client_key/Idempotency-Key) — فلم تُنشأ رسالة جديدة، ومع ذلك يُعدّ نجاحًا (200) لأن النتيجة التي أرادها المستدعي قد تحقّقت فعلًا.