الحملات
ترسل الحملة قالبًا معتمدًا واحدًا إلى عدد كبير من جهات الاتصال. تُنشأ كمسودة، ويُقاس حجم جمهورها قبل إرسال أي شيء، ثم تُسلَّم إلى عامل خلفي — فالإطلاق تعليمة وليس هو الإرسال نفسه.
الحملة
تسمّي الحملة قناة واحدة في channel_id وقالبًا واحدًا في template_id، والقالب هو ما يحمل الرسالة: لا توجد حملة بنص حر.
| الحقل | الوصف |
|---|---|
| id | المعرّف العام للحملة. |
| status | إحدى الحالات التسع أدناه. |
| channel_id, template_id | القناة التي تُرسل منها والقالب الذي تُرسله. |
| template_slug, template_language | معرّف القالب النصي ولغته، منسوخان على الحملة للاطلاع. |
| audience | المحدِّدات كما حُفظت تمامًا. تُحلّ عند الإطلاق لا عند الحفظ. |
| variables, variable_mapping | قيم ثابتة، مع ربط بين متغيّر القالب وحقل جهة الاتصال. |
| schedule_at | موعد البدء، أو null للبدء في أقرب وقت ممكن. |
| total_recipients | عدد المستلمين بعد التجهيز. يبقى 0 حتى ينتهي العامل. |
| excluded | من استُبعد ولماذا: opted_out و no_consent و invalid_number. |
| materialised_at | وقت تجميد الجمهور. القيمة null تعني أنه لم يُجمَّد بعد. |
| failure_reason | سبب فشل الحملة، إن فشلت. |
المصادقة والنطاقات
كلا بيانَي الاعتماد يعملان هنا: مفتاح API، أو رمز وصول مستخدم يحمل X-Workspace-Id. تشرح صفحة المصادقة الفرق.
| الصلاحية | تُستخدم في |
|---|---|
| campaigns.read | العرض والقراءة والمستلمون والإحصاءات ومعاينة الجمهور وفحص ما قبل الإطلاق. |
| campaigns.manage | الإنشاء والتعديل والحذف والإطلاق والإيقاف المؤقت والاستئناف والإلغاء وإعادة المحاولة. |
نقاط النهاية
curl -X POST https://whats.azzamkh.sa/api/v1/campaigns \
-H "Authorization: Bearer $WA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "August reorder reminder",
"channel_id": "ch_...",
"template_id": "tpl_...",
"audience": { "group_ids": ["grp_..."] },
"variable_mapping": { "1": "contact.first_name" }
}'
# 201 Created — a DRAFT. Nothing is sent until it is launched.
# {
# "id": "cmp_...",
# "status": "draft",
# "template_slug": "reorder_reminder",
# "template_language": "ar",
# "total_recipients": 0,
# "excluded": { "opted_out": 0, "no_consent": 0, "invalid_number": 0 },
# "materialised_at": null,
# ...
# }الجمهور
المحدِّدات تراكمية: تدخل جهة الاتصال إن طابقها أي منها. ويُحفظ audience كما أُرسل ولا يُحلّ إلا عند الإطلاق، فمن يُضاف إلى مجموعة بعد ذلك لا تصله حملة تعمل بالفعل.
| الحقل | الوصف |
|---|---|
| group_ids | معرّفات مجموعات جهات الاتصال. |
| tag_ids | معرّفات وسوم جهات الاتصال. |
| contact_ids | معرّفات جهات اتصال محددة. |
| phone_numbers | أرقام E.164 مباشرة. وما لا يطابق جهة اتصال يعود في unknown_numbers. |
| all | كل جهات الاتصال في مساحة العمل. تحقّق من حجم ذلك قبل إرساله. |
الموافقة صريحة وصارمة
لا تصل الحملة إلا إلى جهة اتصال قيمة opt_in_status لديها opted_in. والقيمة unknown ليست موافقة. والفارق بين matched وeligible هو عدد الأشخاص الذين لن تراسلهم الحملة، ومكانه أمام من يضغط زر الإطلاق لا في التقرير بعده.
المعاينة وفحص ما قبل الإطلاق
يقيس POST /v1/campaigns/audience-preview حجم الجمهور قبل وجود الحملة، ويفعل …/validate الشيء نفسه لحملة محفوظة ويضيف ready وissues. ويقبل كلاهما validate_numbers الذي يفحص عيّنة من الأرقام لدى واتساب — انظر التحقق من الأرقام. وكلاهما محدود بـ 60 طلبًا في الدقيقة لكل مساحة عمل.
curl -X POST https://whats.azzamkh.sa/api/v1/campaigns/cmp_.../validate \
-H "Authorization: Bearer $WA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "sample_size": 5, "validate_numbers": false }'
# 200 OK
# {
# "campaign_id": "cmp_...",
# "matched": 1240,
# "eligible": 861,
# "ineligible": {
# "opted_out": 12, "no_consent": 361, "contact_deleted": 0,
# "invalid_number": 4, "duplicate": 2
# },
# "ready": true,
# "issues": [],
# "unknown_numbers": [],
# "unknown_selectors": [],
# "sample": [
# { "contact_id": "cnt_...", "phone_e164": "+9665XXXXXXXX", "display_name": "..." }
# ]
# }
# matched - eligible is 379 people this campaign will NOT message.دورة الحياة
| الحالة | الوصف |
|---|---|
| draft | أُنشئت وقابلة للتعديل. لا يُرسل شيء. |
| validating | أُطلقت، وعامل خلفي يحلّ الجمهور. |
| ready | جُهّز الجمهور والإرسال على وشك البدء. |
| scheduled | جُهّز الجمهور وينتظر schedule_at. |
| running | تسليم المستلمين إلى الطابور. |
| paused | توقفت عن تسليم مزيد من المستلمين. |
| completed | عُولج كل مستلم. |
| cancelled | أُوقفت نهائيًا، وأُلغي المستلمون المعلّقون. |
| failed | تعذّر تشغيل الحملة، وسبب ذلك في failure_reason. |
الإطلاق يجيب بـ validating لا running
يعود POST …/launch فورًا بـ status: "validating" وtotal_recipients: 0 وmaterialised_at: null، ثم يجهّز عامل خلفي الجمهور. وأي تقدّم يُرسم قبل انتهاء ذلك يقرأ صفرًا من صفر ويبدو كإرسال معطّل. استعلم GET /v1/campaigns/cmp_... حتى تُضبط materialised_at.
- يُجمّد الجمهور مرة واحدة عند الإطلاق. وتعديل مجموعة بعد ذلك لا يغيّر شيئًا في حملة أُطلقت بالفعل.
- يوقف
…/pauseتسليم مزيد من المستلمين، ولا يستطيع استرجاع دفعة سُلّمت للطابور، فقد تُرسَل دفعة واحدة بعد الإيقاف — اذكر ذلك في التأكيد. - يُكمل
…/resumeمن حيث توقف، ولا يُرسَل إلى أحد مرتين. - يعيد
…/retry-failedإدراج المستلمين الفاشلين ويجيب بالحملة مع عددretried.
التقدّم
يُشتقّ GET …/stats وقت القراءة من الرسائل نفسها، ويُحصى في مجموعات منفصلة بحسب الحالة الحالية. أما القمع التراكمي في التحليلات فيجيب عن سؤال مختلف، ولذلك لا يتطابق الرقمان — وهذا مقصود لا انحراف.
curl -X POST https://whats.azzamkh.sa/api/v1/campaigns/cmp_.../launch \
-H "Authorization: Bearer $WA_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
# 200 OK — "validating", NOT "running".
# { "id": "cmp_...", "status": "validating", "total_recipients": 0, ... }
# Poll until materialised_at is set, then read progress:
curl https://whats.azzamkh.sa/api/v1/campaigns/cmp_.../stats \
-H "Authorization: Bearer $WA_API_KEY"
# 200 OK — disjoint buckets by CURRENT state, derived at read time.
# {
# "campaign_id": "cmp_...",
# "status": "running",
# "total_recipients": 861,
# "recipients": {
# "pending": 402, "enqueued": 455, "failed": 3, "skipped": 1,
# "cancelled": 0
# },
# "messages": {
# "queued": 12, "sent": 431, "delivered": 388, "read": 106, "failed": 3
# },
# "excluded": { "opted_out": 12, "no_consent": 361, "invalid_number": 4 },
# "started_at": "2026-08-14T09:00:00.000Z",
# "completed_at": null
# }| الحقل | الوصف |
|---|---|
| recipients.pending | جُهّز ولم يُسلَّم إلى الطابور بعد. |
| recipients.enqueued | سُلّم إلى الطابور، وللرسالة الآن حالتها الخاصة. |
| recipients.skipped | أُسقط قبل الإرسال، وسبب ذلك في skip_reason على المستلم. |
| recipients.failed | جُرّب الإرسال وفشل، ويحمل error_code الرمز. |
| messages.* | أعداد حالات الرسائل: queued و sent و delivered و read و failed. |
| excluded.* | من لم يصبح مستلمًا أصلًا، مصنّفًا بالسبب. |
لا يحمل GET /v1/campaigns أي عدّادات. فالتقدّم موجود في …/stats وحده — بطلب واحد لكل حملة، ولا يوجد حقل له في القائمة.
الأخطاء المهمة هنا
| الرمز | HTTP | متى يحدث |
|---|---|---|
| CAMPAIGN_NOT_FOUND | 404 | لا توجد حملة بهذا المعرّف في مساحة العمل هذه. |
| CAMPAIGN_INVALID_STATE | 409 | الإجراء غير متاح من الحالة الحالية للحملة — كإيقاف مسودة أو إطلاق حملة مكتملة. |
| CAMPAIGN_CHANNEL_NOT_ELIGIBLE | 409 | القناة لا تصلح لتشغيل حملة: غير متصلة أو غير مؤهلة. |
| CAMPAIGN_PAUSED | 409 | حاولت الإرسال والحملة موقوفة مؤقتًا. |
| CAMPAIGN_AUDIENCE_EMPTY | 422 | لم يُسفر الجمهور عن أحد يجوز أن تصله رسالة تسويقية. |
| ENTITLEMENT_LIMIT_REACHED | 409 | الخطة لا تشمل الحملات أو بلغت حدًّا. هذه حالة ترقية لا إشعار خطأ. |
كل رمز وحالته وحالة HTTP الخاصة به ورسالته الافتراضية في صفحة الأخطاء.