انتقل للمحتوى

    عربي أو إنجليزي · ↑↓ للتنقل · Enter للفتح · Esc للقفل

    English
    في الصفحة دي

    الويب هوك

    أحداث لحظية موقّعة: قائمة الأحداث بأمثلة كاملة، والتحقق من التوقيع، والمحاولات، وإعادة الإرسال، والترتيب ومنع التكرار.

    مرسلات بتكلم عنوان HTTPS بتاعك لما حاجة تحصل: رسالة من عميل، إيصال تسليم، رد على زرار، إلغاء اشتراك، تغيير في الاتصال، إيقاف حملة. محتوى الرسايل الواردة بيوصلك من الـ API بالطريقة دي بس، لحظيًا — والـ API مش بيرجّعه بعد كده (صندوق الوارد في لوحة التحكم بيحتفظ بيه مشفّر للمدة اللي تختارها).

    الإعداد

    • ويب هوك الحساب — رابط واحد لكل الأرقام: PUT /v2/webhook. مش متاح للمفاتيح المقصورة على أرقام.
    • ويب هوك لكل رقم — PUT /v2/numbers/{numberId}/webhook، أو من لوحة التحكم (الأرقام ← المطوّر).
    • لو الاتنين موجودين، كل حدث بيوصل للاتنين بـ نفس id الحدث.
    • رابط https:// عام على بورت 443 (أو 80). العناوين الخاصة مرفوضة والـ redirects مش بتتتبع. سر التوقيع بيرجع مرة واحدة؛ وتقدر تغيّره في أي وقت.

    شكل الطلب

    http
    POST /your/webhook HTTP/1.1
    Content-Type: application/json
    User-Agent: Wasla-Webhooks/2
    X-Wasla-Event: message.status
    X-Wasla-Delivery: dlv_3c1f0a9b8e7d6c5b4a392817
    X-Wasla-Webhook-Version: 2
    X-Wasla-Signature: t=1760353203,v1=5d2f…(64 hex)
    
    { "id": "evt_…", "type": "message.status", "apiVersion": "2026-10", "createdAt": "…", "sessionId": "…", "number": "201001234567", "data": { … } }
    الحقلالمعنى
    idالمعنىفريد لكل حدث. المحاولات وإعادة الإرسال بنفس الـ id — امنع التكرار بيه.
    typeالمعنىنوع الحدث (القائمة تحت).
    apiVersionالمعنى2026-10.
    createdAtالمعنىالحدث حصل إمتى.
    sessionId / numberالمعنىid الرقم وأرقام تليفونه.
    dataالمعنىبيانات الحدث نفسه.

    قائمة الأحداث

    message.received

    عميل بعت رسالة لرقمك. النص والتعليقات وروابط الميديا الموقّعة بتوصلك هنا لحظيًا؛ ومحتوى الويب هوك مش بيتحفظ بعد فترة المحاولات.

    message.received — الطلب كامل
    {
      "id": "evt_5f0c2b7d9e1a4c3b8a6f2d10",
      "type": "message.received",
      "apiVersion": "2026-10",
      "createdAt": "2026-10-13T08:00:03.000Z",
      "sessionId": "cm1abcd2345efgh6789ijkl",
      "number": "201001234567",
      "data": {
        "messageId": "3EB0C1D2E3F4A5B6C7D8",
        "from": {
          "phone": "201099887766",
          "lid": null,
          "name": "منى"
        },
        "chat": {
          "type": "direct"
        },
        "type": "text",
        "text": "عايز أعرف السعر",
        "caption": null,
        "media": null,
        "location": null,
        "replyTo": null,
        "timestamp": "2026-10-13T08:00:02.000Z"
      }
    }

    message.status

    رسالة بعتها اتغيرت حالتها لـ sent أو delivered أو read أو failed (ومعاها كود الخطأ).

    message.status — الطلب كامل
    {
      "id": "evt_5f0c2b7d9e1a4c3b8a6f2d10",
      "type": "message.status",
      "apiVersion": "2026-10",
      "createdAt": "2026-10-13T08:00:03.000Z",
      "sessionId": "cm1abcd2345efgh6789ijkl",
      "number": "201001234567",
      "data": {
        "messageId": "3EB0A1B2C3D4E5F60718",
        "reference": "order-1042",
        "to": "201099887766",
        "status": "delivered",
        "error": null,
        "timestamp": "2026-10-13T08:00:05.000Z"
      }
    }

    interactive.reply

    عميل رد على أزرار أو قائمة — داس، أو كتب رقم/اسم الاختيار (البديل بالأرقام). أول رد صحيح بس لكل رسالة.

    interactive.reply — الطلب كامل
    {
      "id": "evt_5f0c2b7d9e1a4c3b8a6f2d10",
      "type": "interactive.reply",
      "apiVersion": "2026-10",
      "createdAt": "2026-10-13T08:00:03.000Z",
      "sessionId": "cm1abcd2345efgh6789ijkl",
      "number": "201001234567",
      "data": {
        "interactionId": "int_8f2c1d9a7b3e4f56a1b2c3d4",
        "messageId": "3EB0A1B2C3D4E5F60718",
        "reference": "order-1042",
        "from": {
          "phone": "201099887766",
          "lid": null,
          "name": "منى"
        },
        "reply": {
          "id": "confirm",
          "title": "تأكيد",
          "index": 1,
          "source": "button"
        },
        "replyMessageId": "3EB0FFEE0011223344",
        "timestamp": "2026-10-13T08:03:00.000Z"
      }
    }

    poll.vote

    حد صوّت في استطلاع بعته رقمك. selectedOptions هي اختياراته الحالية كلها ([] = شال صوته).

    poll.vote — الطلب كامل
    {
      "id": "evt_5f0c2b7d9e1a4c3b8a6f2d10",
      "type": "poll.vote",
      "apiVersion": "2026-10",
      "createdAt": "2026-10-13T08:00:03.000Z",
      "sessionId": "cm1abcd2345efgh6789ijkl",
      "number": "201001234567",
      "data": {
        "messageId": "3EB0A1B2C3D4E5F60718",
        "reference": "survey-7",
        "from": {
          "phone": "201099887766",
          "lid": null,
          "name": "منى"
        },
        "selectedOptions": [
          "الأحد"
        ],
        "timestamp": "2026-10-13T08:04:00.000Z"
      }
    }

    contact.opted_out

    عميل بعت كلمة إلغاء الاشتراك؛ بقى في قائمة الحظر ومش هتوصله حملات أو رسايل باردة.

    contact.opted_out — الطلب كامل
    {
      "id": "evt_5f0c2b7d9e1a4c3b8a6f2d10",
      "type": "contact.opted_out",
      "apiVersion": "2026-10",
      "createdAt": "2026-10-13T08:00:03.000Z",
      "sessionId": "cm1abcd2345efgh6789ijkl",
      "number": "201001234567",
      "data": {
        "phone": "201099887766",
        "lid": null,
        "keyword": "إلغاء",
        "timestamp": "2026-10-13T08:05:00.000Z"
      }
    }

    contact.opted_in

    شلت شخص من قائمة الحظر (بيتبعت لكل أرقام الحساب على v2).

    contact.opted_in — الطلب كامل
    {
      "id": "evt_5f0c2b7d9e1a4c3b8a6f2d10",
      "type": "contact.opted_in",
      "apiVersion": "2026-10",
      "createdAt": "2026-10-13T08:00:03.000Z",
      "sessionId": "cm1abcd2345efgh6789ijkl",
      "number": "201001234567",
      "data": {
        "phone": "201099887766",
        "lid": null,
        "keyword": null,
        "timestamp": "2026-10-13T09:00:00.000Z"
      }
    }

    session.status

    جلسة الجهاز المربوط للرقم اتوصلت أو فصلت أو اتعمل لها خروج أو اتحظرت.

    session.status — الطلب كامل
    {
      "id": "evt_5f0c2b7d9e1a4c3b8a6f2d10",
      "type": "session.status",
      "apiVersion": "2026-10",
      "createdAt": "2026-10-13T08:00:03.000Z",
      "sessionId": "cm1abcd2345efgh6789ijkl",
      "number": "201001234567",
      "data": {
        "status": "disconnected",
        "reason": null,
        "timestamp": "2026-10-13T10:00:00.000Z"
      }
    }

    number.status

    حالة اتصال الرقم اتغيرت: connected أو disconnected أو pairing أو banned أو logged_out أو conflict.

    number.status — الطلب كامل
    {
      "id": "evt_5f0c2b7d9e1a4c3b8a6f2d10",
      "type": "number.status",
      "apiVersion": "2026-10",
      "createdAt": "2026-10-13T08:00:03.000Z",
      "sessionId": "cm1abcd2345efgh6789ijkl",
      "number": "201001234567",
      "data": {
        "numberId": "cm1abcd2345efgh6789ijkl",
        "status": "connected",
        "reason": null,
        "timestamp": "2026-10-13T08:00:00.000Z"
      }
    }

    number.restricted

    واتساب قفل (أو فتح) إرسال الرقم لجهات اتصال جديدة. until بيقول إمتى بيخلص.

    number.restricted — الطلب كامل
    {
      "id": "evt_5f0c2b7d9e1a4c3b8a6f2d10",
      "type": "number.restricted",
      "apiVersion": "2026-10",
      "createdAt": "2026-10-13T08:00:03.000Z",
      "sessionId": "cm1abcd2345efgh6789ijkl",
      "number": "201001234567",
      "data": {
        "numberId": "cm1abcd2345efgh6789ijkl",
        "restricted": true,
        "until": "2026-10-14T08:00:00.000Z",
        "type": "reachout_timelock",
        "timestamp": "2026-10-13T08:00:00.000Z"
      }
    }

    campaign.status

    حملة بدأت أو وقفت (ومعاها السبب) أو كمّلت أو خلصت أو فشلت أو اتلغت.

    campaign.status — الطلب كامل
    {
      "id": "evt_5f0c2b7d9e1a4c3b8a6f2d10",
      "type": "campaign.status",
      "apiVersion": "2026-10",
      "createdAt": "2026-10-13T08:00:03.000Z",
      "sessionId": "cm1abcd2345efgh6789ijkl",
      "number": "201001234567",
      "data": {
        "campaignId": "cm2camp0001xyz",
        "name": "عرض الجمعة",
        "status": "paused",
        "reason": "AUTO_LOW_DELIVERY",
        "pausedUntil": "2026-10-13T10:00:00.000Z",
        "timestamp": "2026-10-13T09:00:00.000Z"
      }
    }

    otp.verified

    كود OTP اتبعت بـ POST /v2/otp اتأكد. الكود نفسه عمره ما بيتبعت.

    otp.verified — الطلب كامل
    {
      "id": "evt_5f0c2b7d9e1a4c3b8a6f2d10",
      "type": "otp.verified",
      "apiVersion": "2026-10",
      "createdAt": "2026-10-13T08:00:03.000Z",
      "sessionId": "cm1abcd2345efgh6789ijkl",
      "number": "201001234567",
      "data": {
        "otpId": "otp_8f2c1d9a7b3e4f56a1b2c3d4",
        "to": "2010****4567",
        "verifiedAt": "2026-10-13T08:01:00.000Z"
      }
    }

    conversation.attributed

    A chat started from your website button or a tracked WhatsApp link (page, utm_*, widget / link). Metadata only.

    conversation.attributed — الطلب كامل
    {
      "id": "evt_5f0c2b7d9e1a4c3b8a6f2d10",
      "type": "conversation.attributed",
      "apiVersion": "2026-10",
      "createdAt": "2026-10-13T08:00:03.000Z",
      "sessionId": "cm1abcd2345efgh6789ijkl",
      "number": "201001234567",
      "data": {
        "conversationId": "cm5conv0001",
        "contact": {
          "phone": "201099887766",
          "lid": null,
          "name": "منى"
        },
        "messageId": "3EB0C1D2E3F4A5B6C7D8",
        "source": {
          "type": "widget",
          "widgetId": "wgt_k2m7q4x9a3b5c8d2",
          "linkId": null,
          "name": "Main website",
          "agent": "sales",
          "pagePath": "/products/bag",
          "referrerDomain": "facebook.com",
          "utm": {
            "source": "facebook",
            "medium": "cpc",
            "campaign": "summer",
            "content": null,
            "term": null
          }
        },
        "clickedAt": "2026-10-13T07:58:10.000Z",
        "timestamp": "2026-10-13T08:00:02.000Z"
      }
    }

    webhook.test

    بيتبعت من «ابعت حدث تجريبي» ومن endpoints الاختبار. بيوصل دايمًا حتى لو مش في قائمة أحداثك.

    webhook.test — الطلب كامل
    {
      "id": "evt_5f0c2b7d9e1a4c3b8a6f2d10",
      "type": "webhook.test",
      "apiVersion": "2026-10",
      "createdAt": "2026-10-13T08:00:03.000Z",
      "sessionId": "cm1abcd2345efgh6789ijkl",
      "number": "201001234567",
      "data": {
        "message": "This is a test event from Mursilat",
        "timestamp": "2026-10-13T08:00:00.000Z"
      }
    }

    التحقق من التوقيع

    X-Wasla-Signature: t=<ثواني unix>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>. استخدم الـ body الخام (قبل تحويل الـ JSON)، وقارن بوقت ثابت، وارفض أي طابع زمني بعيد عن دلوقتي بأكتر من 5 دقايق.

    // Node.js + Express. Use the RAW body: JSON.parse → JSON.stringify changes bytes and breaks the signature.
    import crypto from 'node:crypto';
    import express from 'express';
    
    const app = express();
    const SECRET = process.env.WASLA_WEBHOOK_SECRET;
    const seen = new Set(); // use Redis/DB in production
    
    function verifyWaslaSignature(rawBody, header, secret, toleranceSec = 300) {
      if (!header || !secret) return false;
      const parts = {};
      for (const pair of header.split(',')) {
        const i = pair.indexOf('=');
        if (i > 0) parts[pair.slice(0, i).trim()] = pair.slice(i + 1).trim();
      }
      const t = parts.t;
      const v1 = parts.v1;
      if (!/^\d+$/.test(t || '') || !/^[0-9a-f]{64}$/i.test(v1 || '')) return false;
      // Reject old (replayed) or far-future timestamps.
      if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > toleranceSec) return false;
      const expected = crypto
        .createHmac('sha256', secret)
        .update(t + '.')
        .update(rawBody) // the RAW bytes exactly as received, not re-serialized JSON
        .digest();
      const received = Buffer.from(v1, 'hex');
      return received.length === expected.length && crypto.timingSafeEqual(received, expected);
    }
    
    app.post('/webhooks/mursilat', express.raw({ type: 'application/json' }), (req, res) => {
      if (!verifyWaslaSignature(req.body, req.get('X-Wasla-Signature'), SECRET)) {
        return res.status(401).send('invalid signature');
      }
      const event = JSON.parse(req.body.toString('utf8'));
      res.sendStatus(200); // answer fast (within 10s); do the work after
    
      if (seen.has(event.id)) return; // retries reuse the same event id
      seen.add(event.id);
      if (event.type === 'interactive.reply') {
        console.log(event.data.reference, '→', event.data.reply.id);
      }
    });
    
    app.listen(3000);

    مش متأكد إن كودك صح؟ الصق طلب حقيقي في فاحص الويب هوك — بيتحقق من التوقيع والغلاف جوه المتصفح.

    المحاولات

    • رد بـ 2xx خلال 10 ثواني، وبعدين اعمل الشغل في الخلفية.
    • أي حاجة تانية (timeout، خطأ شبكة، 3xx، 4xx، 5xx) بتتكرر: فورًا، وبعد 10 ثواني، ودقيقة، و5، و15، و30 دقيقة — 6 محاولات في حوالي 51 دقيقة. بعدها التوصيل بيبقى FAILED.
    • 410 Gone معناها «بطّل»: الويب هوك بيتقفل فورًا والتوصيلات المستنية بتتلغي. احفظ الرابط تاني عشان يرجع.
    • بالكتير 4 طلبات في نفس الوقت لكل رابط. تغيير الرابط أو مسحه بيلغي المحاولات المستنية للقديم.

    فترة إعادة الإرسال

    مرسلات بتحتفظ بمحتوى الحدث في الذاكرة بس طول فترة المحاولات: حوالي ساعة بعد الحدث، و10 دقايق بعد التوصيل الناجح. جوه الفترة دي POST /v2/webhooks/deliveries/{deliveryId}/redeliver بيبعت نفس الحدث (بنفس الـ id) تاني؛ بعدها بيرد 410 DELIVERY_PAYLOAD_EXPIRED — ابني الحالة من الـ API (مثلًا GET /v2/messages/{id}).

    الترتيب ومنع التكرار

    • الترتيب مش مضمون: المحاولات والتوصيلات المتوازية ممكن توصل مش بالترتيب. استخدم createdAt / data.timestamp — مثلًا تجاهل delivered بعد ما شفت read.
    • التكرار متوقع (محاولات، إعادة إرسال، ويب هوك الحساب والرقم مع بعض). خزّن الـ id وتخطى اللي اتعامل معاه قبل كده.
    • سجل التوصيل بيحتفظ ببيانات وصفية 7 أيام (الحدث، الحالة، كود HTTP، المدة، الخطأ) — عمره ما بيحتفظ بالمحتوى أو برد سيرفرك.