# الإعادة الآمنة (Idempotency)

> كيفية إعادة محاولة طلبات الواجهة البرمجية العامة لمجيب بأمان عبر رأس Idempotency-Key — السلوك في حالات الإعادة، التنفيذ الجاري، والاختلاف.

أعطال الشبكة لا مفرّ منها. حين ترسل رسائل وقوالب واتساب، أو ترد في محادثة، تستخدم الواجهة البرمجية العامة لمجيب رأس `Idempotency-Key` لجعل الإعادات آمنة — نفس المفتاح ونفس الجسم يُعيدان الاستجابة الأصلية، ولا يُرسَل شيء مرتين.

## كيف أجعل الإعادات آمنة؟

أرسل أي نص يختاره العميل بطول حتى 255 حرفاً في رأس `Idempotency-Key` حين ترسل رسالة أو قالب واتساب (`POST /v1/whatsapp/messages` و`POST /v1/whatsapp/templates`) أو ترد في محادثة (`POST /v1/agents/{agentId}/conversations/{conversationId}/messages`):

```bash
curl -X POST https://api.mojeeb.app/v1/whatsapp/templates \
  -H "Authorization: Bearer mk_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-12345-confirmation" \
  -d '{
    "agent_id": "12345678-1234-1234-1234-123456789012",
    "from":     "+15557654321",
    "to":       "+15551234567",
    "template": {
      "name":     "order_shipped",
      "language": "en",
      "parameters": {
        "customer_name": "Sara",
        "tracking_url":  "https://example.com/track/A123"
      }
    }
  }'
```

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

## ماذا يحدث عند الإعادة؟

لا يُخزَّن إلا الجواب الناجح: الإرسال المقبول (`202`)، أو الرد في محادثة (`201`). ولا يُحجَز المفتاح إلا بعد أن يجتاز الطلب فحوص التوثيق والنطاق وحد المعدّل والصيغة، وفي الرد فحص الوكيل والمحادثة. الطلب المرفوض عندها (`401` أو `403` أو `413` أو `429`، أو `422` بسبب الصيغة مثل `invalid_request_body` أو `invalid_agent_id` أو `invalid_phone_number` أو `template_header_media_url_invalid`، أو في الرد `404` لوكيل أو محادثة لا يصل إليها المفتاح، أو `503`) لا يمسّ المفتاح أبداً: صحّحه وأعد المحاولة بالمفتاح نفسه فوراً.

وبعد حجز المفتاح، يعتمد السلوك على كيف انتهى الطلب الأصلي وما إذا كان الجسم متطابقاً:

| السيناريو | الاستجابة | HTTP |
|---|---|---|
| أول إرسال بهذا المفتاح | استجابة طبيعية | 202 (201 للرد) |
| نفس المفتاح بأي جسم، ما دام طلب به قيد التنفيذ، أو لم تمضِ 60 ثانية على بدء طلب به رُفض أو فشل بعد حجزه (مثل `agent_not_found` أو `from_phone_not_found_for_agent` أو `template_not_found` في إرسال واتساب، أو `messaging_window_closed` أو `channel_not_supported` في رد، أو `5xx`) | `idempotency_key_in_progress` (مع `Retry-After`) | 409 |
| نفس المفتاح + نفس الجسم، الأصلي مقبول | إعادة الاستجابة الأصلية، رأس `Idempotent-Replayed: true` | 202 (201 للرد) |
| نفس المفتاح + **جسم أو نقطة نهاية أو محادثة مختلفة**، الأصلي مقبول | `idempotency_key_in_use_with_different_params` | 422 |
| نفس المفتاح وقد بدأ ذلك الطلب المرفوض أو الفاشل منذ 60 ثانية أو أكثر، أو مفتاح مختلف | يُعالَج كطلب جديد | — |

## كم تُخزَّن الاستجابات؟

24 ساعة على الأقل: إعادة المحاولة بالمفتاح نفسه والجسم نفسه خلالها تُعيد الجواب المُخزَّن دائماً. ولا تعتمد على أن يصبح المفتاح جديداً بعدها. أعطِ كل عملية جديدة مفتاحاً جديداً، واختر مفاتيح ثابتة مرتبطة بعملية أعمالك، لا UUIDs عشوائية تتغيّر بين المحاولات.

## كيف أختار مفاتيح إعادة جيّدة؟

المفتاح الجيد **ثابت، فريد، ومرتبط بعملية أعمال واحدة**.

**جيّد:**
- `order-12345-confirmation`
- `signup-456-welcome`
- `appointment-reminder-2026-04-30-user-789`

**سيّئ:**
- الطوابع الزمنية (كل إعادة تأخذ واحدة مختلفة — يُفقِد المعنى)
- UUIDs عشوائية تُولَّد وقت الطلب (نفس المشكلة)
- نصوص عامة مثل `retry-1` (تصادم بين العمليات)
- مفاتيح مُعاد استخدامها مع أجسام رسائل أو محادثات مختلفة (يُحفّز اختلاف 422)

إن لم يكن لديك معرّف ثابت طبيعي، أنشئه مرة واحدة في بداية عمليتك وأعد استخدامه لكل إعادة محاولة لذلك الإرسال بالذات.

## كيف يبدو الاختلاف؟

عند إعادة استخدام نفس المفتاح بجسم مختلف، أو على نقطة نهاية أو محادثة أخرى، الواجهة ترفض الطلب وتُعطيك معرّف الارتباط للإرسال الأصلي حتى تجده في سجلاتك:

```json
{
  "error": {
    "type":                    "idempotency_error",
    "code":                    "idempotency_key_in_use_with_different_params",
    "message":                 "The idempotency key was used previously with a different request body. Use a different key for this operation.",
    "correlation_id":          "req_01KQE...",
    "original_correlation_id": "req_01KQE..."
  }
}
```

`original_correlation_id` هو معرّف التتبّع للطلب الذي حجز المفتاح أصلاً. ابحث في سجلات تطبيقك عن هذا المعرّف لترى ما أُرسل.

## كيف تبدو إعادة محاولة قيد التنفيذ؟

إن أعدت المحاولة، بأي جسم، بينما الإرسال الأصلي ما زال يُعالَج (نادر، عادة في غضون ثوانٍ)، أو خلال 60 ثانية من بدء طلب بالمفتاح رُفض أو فشل بعد حجزه:

```json
{
  "error": {
    "type":           "idempotency_error",
    "code":           "idempotency_key_in_progress",
    "message":        "A request with this idempotency key is currently being processed. Retry shortly.",
    "correlation_id": "req_01KQE..."
  }
}
```

تحمل الاستجابة أيضاً رأس `Retry-After` بالثواني. انتظر هذه المدة، ثم أعد بنفس المفتاح ونفس الجسم — ستحصل على إعادة الاستجابة الأصلية (`202`، أو `201` للرد) إن كان الأصلي مقبولاً، أو محاولة جديدة بعد مرور 60 ثانية على بدء الطلب المرفوض أو الفاشل، أو 409 آخر قبل ذلك أو ما دام قيد التنفيذ.

## أسئلة شائعة

### هل تُجزّئ الواجهة جسمي لاكتشاف الاختلاف؟

نعم. أول طلب بمفتاح معيّن يُخزّن hash بـSHA-256 للطلب: طريقته ومساره وجسمه المُقَنَّن. الطلبات اللاحقة تُعيد حساب الـhash وتقارن. ترتيب الحقول لا يهم؛ نُرتّب المفاتيح قبل التجزئة — `{"a":1,"b":2}` و `{"b":2,"a":1}` هما نفس الجسم منطقياً. والمسار جزء منه أيضاً: بعد قبول الأصلي، المفتاح نفسه بالجسم نفسه على نقطة نهاية أخرى، أو في رد على محادثة أخرى، اختلافٌ (`422`) لا إعادة.

### ماذا عن الإعادة الآمنة على طلبات GET؟

نقاط GET إعادة-آمنة بطبيعتها — استدعاؤها عدة مرات لا أثر له. رأس `Idempotency-Key` يُتجاهَل على GET؛ لا حاجة لإرساله.

### هل أحتاج مفاتيح إعادة لاستعلامات الحالة؟

لا. `GET /v1/whatsapp/messages/{id}` للقراءة فقط.

### ماذا لو كانت الاستجابة المُخزَّنة خطأ؟

لا تُخزَّن. لا يُخزَّن إلا الجواب الناجح. بعد طلب رُفض أو فشل بعد حجز المفتاح (`4xx` مثل `template_not_found` أو `messaging_window_closed` في رد، أو `5xx`)، يُجيب المفتاح نفسه بـ`409 idempotency_key_in_progress` أياً كان الجسم، حتى تمضي 60 ثانية على بدء ذلك الطلب، ثم يُنفَّذ الطلب من جديد. لإعادة طلب مُصحَّح فوراً، استخدم مفتاحاً مختلفاً. أما الطلب المرفوض قبل حجز المفتاح، مثل `422` بسبب الصيغة، فلا يحجز المفتاح.

### هل الإعادة هي الاستجابة الأصلية نفسها؟

نعم. تُعيد الإعادة رمز الحالة الأصلي والجسم نفسه: الحقول نفسها، ومنها الحقول التي قيمتها null، بالقيم نفسها. ولا يُضاف إلا رأس `Idempotent-Replayed: true`، لتميّز الإعادة من الاستجابة الأصلية. وقد يختلف ترتيب الحقول، فقارن JSON بعد تحليله لا البايتات الخام.
