إرسال قالب من نظامك الخاص

استدعاء واحد للـ API يرسل قالب WhatsApp معتمدًا إلى رقم واحد — من webhook أو مهمة مجدولة أو سكربت. المفتاح، ومعرّف القالب، والطلب، وكل حالة رفض، والأمران اللذان يجب أن تبني تصميمك حولهما.

آخر تحديث

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

بعض القوالب لا يرسلها شخص أصلًا. تريد Tidewater Laundry Co. أن يخرج تأكيد الاستلام لحظة أن يؤكّد نظام الحجز لديها موعدًا — لا ينبغي أن يضطر أحد إلى فتح صندوق الوارد لذلك. لهذا يوجد endpoint واحد:

POST https://api.omniocto.com/api/wa-templates/{id}/send

يرسل قالبًا معتمدًا واحدًا إلى رقم هاتف واحد. استدعِه من معالج webhook أو من سكربت — «حين يُشحن طلب، أرسل قالب الاستلام». ولأنه قالب، لا تنطبق نافذة الـ 24 ساعة (لماذا).

احصل على مفتاح API

المفاتيح في /settings/api-keys — في قائمة الترس، تحت Admin & System ← API Keys.

  1. انقر Create key. يطلب مربع الحوار Create API key قيمةً لـ Name (optional) — شيئًا مثل “e.g. CI pipeline”؛ سمِّه باسم النظام الذي سيستخدمه.
  2. يعرض API key created المفتاح مرة واحدة، مع “Copy this key now — you won’t be able to see it again.” احفظه حيث يحفظ نظامك الأسرار.

تعرض القائمة تاريخ Last used لكل مفتاح، وRevoke key يوقفه فورًا: “Any integration using it will fail.” مفتاح لكل تكامل يجعل ذلك آمنًا.

أرسل المفتاح في كل استدعاء بصيغة Authorization: Bearer <key>.

اعثر على معرّف القالب

افتح /templates/whatsapp وانقر القالب. تنفتح اللوحة ويُضاف إلى عنوان الصفحة &template=wt-… — هذا المعرّف القصير wt- هو ما يوضع في المسار. (ومعرّف UUID للقالب يعمل أيضًا.)

تخبرك اللوحة نفسها بما يجب أن يملأه الاستدعاء:

  • Variables — متغيّرات المتن: إمّا “2 positional variables” ({{1}}، {{2}}) وإمّا Named: متبوعةً بالأسماء.
  • Header — نصّها، أو [IMAGE] أو [VIDEO] أو [DOCUMENT] لترويسة وسائطية.
  • Buttons — مسرودة بالترتيب، لكلٍّ منها نوعه ([URL]، [COPY_CODE]، [QUICK_REPLY]). الأول هو الفهرس 0.

من سكربت، يسرد GET /api/wa-templates?status=approved القوالب المعتمدة مع shortId وname وlanguage وcategory.

لا يمكن إرسال إلا قالب معتمد؛ أي حالة أخرى تُرفض.

الطلب

للمتن حقلان:

الحقلما هو
toرقم الهاتف، بصيغة E.164 — +14255550123
componentsمصفوفة template.components الخاصة بـ Meta، تُمرَّر كما هي. احذفها لقالب بلا متغيّرات

components هي المصفوفة نفسها التي ترسلها إلى Cloud API من Meta، فتنطبق عليها وثائق Meta. باختصار:

  • Header — معامل واحد: text لترويسة نصّية فيها متغيّر، أو image أو video أو document مع link يبدأ بـ https://.
  • Body — معاملات text. لقالب موضعي ({{1}}، {{2}})، أرسلها بالترتيب دون أسماء. ولقالب مسمّى ({{customer_name}})، أعطِ كل معامل parameter_name الخاص به. لا تخلط بين الطريقتين أبدًا. القالب الذي أنشأه Octo موضعي هنا، أيًّا كانت الأسماء التي أعطيتها.
  • Buttons — مكوّن لكل زر يأخذ قيمة، مع sub_type وindex (موضعه في قائمة أزرار القالب):
    • url — معامل text: الجزء المتغيّر من الرابط.
    • copy_code — الرمز، بصيغة Meta { "type": "coupon_code", "coupon_code": "FRESH20" } أو بصيغة { "type": "text" }. حتى 15 حرفًا.
    • quick_reply — اختياري؛ معامل { "type": "payload" } إن أردت أن يعود إليك payload حين يُنقر.

مثال كامل

قالب الاستلام لدى Tidewater، pickup_confirmation، متنه Hi {{1}}, your pickup is confirmed for {{2}}. Reply here to change it. وله زر Track pickup واحد ينتهي رابطه برقم الطلب:

curl -X POST "https://api.omniocto.com/api/wa-templates/wt-a1b2c3d4e5/send" \
  -H "Authorization: Bearer $OMNI_OCTO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14255550123",
    "components": [
      { "type": "body", "parameters": [
        { "type": "text", "text": "Ana" },
        { "type": "text", "text": "Tuesday, 9–11am" }
      ] },
      { "type": "button", "sub_type": "url", "index": "0",
        "parameters": [{ "type": "text", "text": "TW-20931" }] }
    ]
  }'

لو كان القالب يستخدم متغيّرات مسمّاة، لسمّى المتن كل قيمة:

{ "type": "body", "parameters": [
  { "type": "text", "parameter_name": "customer_name", "text": "Ana" },
  { "type": "text", "parameter_name": "pickup_slot", "text": "Tuesday, 9–11am" }
] }

النجاح هو 201:

{
  "messageId": "6f1c…",
  "conversationId": "3a9e…",
  "conversationShortId": "cv-9f8e7d6c5b",
  "externalId": "wamid.HBgL…",
  "isNewConversation": false,
  "template": { "id": "…", "shortId": "wt-a1b2c3d4e5", "name": "pickup_confirmation", "language": "en_US" }
}

externalId هو المعرّف الذي ستبلّغ Meta على أساسه عن إيصالات التسليم والقراءة؛ وتصل هذه إلى المحادثة لاحقًا. 201 تعني أن Meta قبلت الإرسال، لا أن الهاتف استلمه. احتفظ بـ conversationShortId — ستحتاج إليه أدناه.

أين تظهر الرسالة

في /conversations، في المحادثة مع ذلك الرقم على رقم WhatsApp الخاص بالقالب — المحادثة نفسها التي تصل إليها رسائل العميل:

  • إن وُجدت محادثة مع ذلك الرقم، ينضمّ إليها القالب — وإن كانت مغلقة، تُعاد فتحها. isNewConversation قيمته false.
  • وإن لم توجد، تُفتح محادثة جديدة (وتُنشأ جهة اتصال لرقم غير معروف). isNewConversation قيمته true.
  • تعرض الفقاعة المتن مع قيمك، موسومًا بـ Template · واسم القالب.

حين يرد العميل، يصل ردّه إلى المحادثة نفسها — والإرسال عبر الـ API لا يسلّم المحادثة إلى شخص. المحادثة التي كانت موجودة يبقى فيها من كان يتولّاها؛ والمغلقة أو الجديدة تصبح نشطة تحت وكيل الذكاء الاصطناعي الذي يجيب على رقم WhatsApp ذلك. فذلك الوكيل، إن كان للرقم وكيل، هو من يجيب عن الرد، إلا إذا تولّى شخصٌ المحادثة في /conversations.

حين يُرفض الإرسال

الحالةcodeالمعنى
400—to ليس بصيغة E.164، أو components ليست بشكل تقبله Meta؛ يسرد المتن المشكلات
402billing_not_eligibleلا تستطيع مساحة العمل إرسال رسائل صادرة بعد
404template_not_foundلا يوجد قالب بهذا المعرّف في مساحة العمل هذه
409blocked_dncألغى الرقم الاشتراك. تُحفظ الرسالة في المحادثة محجوبةً؛ ولا يصل شيء إلى Meta
422template_not_approvedلم تعتمد Meta القالب؛ status تقول أين هو
422template_components_mismatchcomponents لا تناسب القالب. error يسمّي المشكلة
422channel_unavailableأُزيل رقم WhatsApp الخاص بالقالب أو عُطّل
422channel_pending_verification, workspace_suspended, route_blockedرفضه فحص إرسال على الرقم أو مساحة العمل. تُحفظ الرسالة في المحادثة محجوبةً أو فاشلة
429workspace_rate_limitedتجاوزت مساحة عملك معدّلها على هذا الـ endpoint. لا يُكتب شيء
429rate_limitedامتلأت نافذة الدفعة لرقم WhatsApp — انتظر من ثوانٍ إلى دقائق
429daily_limit_reachedاستُنفد الحدّ اليومي للقوالب لدى الرقم — قد يكون الانتظار ساعات
502send_failedرفضت Meta الإرسال؛ error يحمل رسالة Meta. تبقى الرسالة في المحادثة موسومةً بالفشل

كل 429 يحمل ترويسة Retry-After وقيمة retryAfterSec مطابقة في المتن. انتظر تلك المدة وأرسل الاستدعاء نفسه مجددًا: لم تُرسَل أي رسالة أو تُحفظ أو تُحتسب في الفوترة في المرة الأولى. (الإرسال الأول إلى رقم جديد الذي يرفضه رصيد الرقم يكون قد فتح المحادثة مع ذلك، فارغةً؛ ويحمل المتن conversationShortId الخاص بها.)

عائلتا 429 حدّان مختلفان. مساحة عملك يمكنها استدعاء هذا الـ endpoint 300 مرة في الدقيقة افتراضيًا، في دفعات تصل إلى 10 في الثانية؛ ذلك المعدّل يحسب استدعاءات الـ API فقط، ويضبطه دعم Omni Octo لكل مساحة عمل. رقم WhatsApp له رصيد إرسال خاص به، مشترك مع صندوق الوارد وOcto والحملات — ما سرعة الإرسال المسموحة لرقم واحد يشرحه.

عدم التطابق لا يرسل شيئًا

يفحص الاستدعاء components مقابل القالب قبل أن يُكتب أي شيء. قيمة ناقصة، أو زائدة، أو اسم غير معروف، أو نوع ترويسة خاطئ، أو معامل على زر لا يقبله — كلٌّ منها 422 template_components_mismatch، دون حفظ رسالة ودون إرسال شيء. ويقول error بالضبط ما الخطأ:

body expects 2 parameters, got 1
body is missing variable "pickup_slot"
button 0 is a QUICK_REPLY button, got url
button 0 (a URL button) is missing its url parameter

فعدم التطابق آمن الإصلاح وإعادة الإرسال.

الاستدعاء ليس idempotent

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

قبل إعادة المحاولة بعد انتهاء المهلة، تحقّق من المحادثة —

GET /api/conversations/{conversationShortId}

بالمعرّف من إرسال سابق إلى ذلك الرقم، أو افتح المحادثة في /conversations. لا تُعِد الإرسال إلا إن لم يكن القالب هناك.

أما حالات الرفض أعلاه فمختلفة: الـ 4xx يخبرك أن الإرسال لم يحدث.

ما لا يفعله هذا الـ endpoint

  • إرسال نص حر. يرسل القوالب المعتمدة فقط — للرد داخل النافذة، استخدم صندوق الوارد.
  • الإرسال إلى قائمة. الاستدعاء الواحد رقم واحد؛ الحملات تتولّى القوائم.
  • تجاوز Meta. قد ترفض Meta قالبًا تسويقيًا حتى بعد 201 — انظر لماذا.

إلى أين بعد ذلك