للمطورين

الويبهوكس

إشعارات موقّعة في أنظمتك عند تغيّر المنشورات أو الحسابات أو الفريق، وعند الاقتراب من حد، وكل يوم مع أرقامك.

متاحة في خطة المؤسسات

كيف تعمل الويبهوكس

يرسل روّج طلب POST عبر HTTPS إلى رابطك عند وقوع حدث في مساحة عملك: جدولة منشور أو نشره أو فشله أو إلغاؤه، أو ربط حساب أو انقطاعه أو قرب انتهائه، أو تغيّر في فريقك، أو اقتراب من حد استخدام، أو جاهزية تحليلات اليوم.

  • الويبهوكس جزء من خطة المؤسسات.
  • حتى 5 روابط لكل مساحة عمل، ويشترك كل رابط في الأحداث التي يختارها.
  • تصل الأحداث خلال 5 دقائق، وكل طلب موقّع.
  • كل ما يحدث في مساحة عملك يُحتسب، أياً كان عضو الفريق الذي قام به.

إعداد رابط

  1. افتح الإعدادات ثم الويبهوكس.
  2. أضف رابط نقطة النهاية. يجب أن يكون HTTPS ومتاحاً للعامة.
  3. اختر الأحداث. يمكنك تغييرها لاحقاً دون تغيير المفتاح السري.
  4. انسخ المفتاح السري عند ظهوره. يظهر مرة واحدة ولا يمكن استرجاعه.
  5. اضغط «إرسال تجريبي». يصلك مثال لأحد أحداثك خلال 5 دقائق ويحمل "test": true.

شكل كل طلب

طلب POST بمحتوى JSON وهذه الترويسات:

  • X-Rawwij-Event نوع الحدث، مثل post.published
  • X-Rawwij-Delivery معرّف التسليم، وهو نفس قيمة id في المحتوى
  • X-Rawwij-Timestamp وقت إرسال هذه المحاولة بالثواني (Unix)
  • X-Rawwij-Signature v1= متبوعاً بالتوقيع

المحتوى

{
  "id": "0b6f3c1e-8a2d-4f7b-9c5e-1d4a7b2c9e80",
  "type": "post.published",
  "created_at": "2026-10-01T09:01:12.000Z",
  "test": false,
  "data": {
    "post_id": "6f1c2a8e-4b1d-4c55-9a0e-2d7b3f9c1a42",
    "status": "published",
    "scheduled_at": "2026-10-01T09:00:00.000Z",
    "published_at": "2026-10-01T09:01:12.000Z",
    "error": null,
    "failed_count": 0,
    "accounts": [
      {
        "account_id": "b3e9d1f0-7a2c-4e8b-9c4d-1f6a5e2b8c73",
        "platform": "instagram",
        "username": "rawwij",
        "status": "published",
        "url": "https://www.instagram.com/p/DAbCdEfGhIj/",
        "platform_post_id": "18034567890123456",
        "error": null,
        "text": "Our autumn collection is here. Visit the store this weekend.",
        "media_type": "image",
        "media_count": 1
      }
    ]
  }
}
  • id يبقى كما هو عند إعادة المحاولة أو إعادة الإرسال. استخدمه لتجاهل التكرار.
  • created_at هو وقت وقوع الحدث، لا وقت إرسال هذه المحاولة.
  • test يكون true فقط للتسليمات المرسلة بزر الإرسال التجريبي.
  • platform قيمته واحدة من: instagram وfacebook وthreads وtiktok وtwitter (X) وlinkedin وyoutube وtelegram.

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

احسب HMAC-SHA256 للنص {timestamp}.{raw body} بمفتاح رابطك السري وبصيغة hex. يجب أن يطابق القيمة التي تلي v1= في X-Rawwij-Signature. قارن بزمن ثابت، وارفض أي طابع زمني أقدم من 5 دقائق.

Node.js

import crypto from 'node:crypto';

// rawBody: the request body exactly as received, before any JSON parsing.
export function verifyRawwij(rawBody, headers, secret) {
  const timestamp = String(headers['x-rawwij-timestamp'] ?? '');
  const signature = String(headers['x-rawwij-signature'] ?? '').replace(/^v1=/, '');

  // Refuse anything older than five minutes, so a captured request cannot be replayed.
  if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return false;
  }

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`, 'utf8')
    .digest('hex');

  const a = Buffer.from(expected, 'utf8');
  const b = Buffer.from(signature, 'utf8');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Python

import hashlib
import hmac
import time

# raw_body: the request body exactly as received, as bytes.
def verify_rawwij(raw_body: bytes, headers, secret: str) -> bool:
    timestamp = headers.get("X-Rawwij-Timestamp", "")
    signature = headers.get("X-Rawwij-Signature", "").removeprefix("v1=")

    # Refuse anything older than five minutes, so a captured request cannot be replayed.
    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
        return False

    expected = hmac.new(
        secret.encode("utf-8"),
        timestamp.encode("utf-8") + b"." + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

إعادة المحاولة والفشل وإعادة الإرسال

  • أجب بأي حالة 2xx خلال 10 ثوانٍ. أي رد آخر يُعد فشلاً، ولا تُتبع إعادات التوجيه.
  • يُعاد إرسال التسليم الفاشل 5 مرات، بعد نحو دقيقة ثم 5 دقائق ثم 30 دقيقة ثم ساعتين ثم 6 ساعات، ثم يتوقف.
  • بعد 20 فشلاً متتالياً يتوقف الرابط، ويصل مالك مساحة العمل إشعار وبريد إلكتروني. أعد تشغيله من الإعدادات، الويبهوكس.
  • تظهر كل محاولة في سجل التسليمات مع رمز الاستجابة لمدة 30 يوماً. إعادة الإرسال ترسل البيانات الأصلية بنفس المعرّف.
  • إذا خرجت مساحة العمل من خطة المؤسسات تتوقف روابطها عن استقبال الأحداث، وتبقى ظاهرة ليمكن حذفها.

جميع الأحداث

المنشورات

post.scheduledجُدول المنشور

عند جدولة منشور من صفحة الإنشاء أو الجدولة المجمعة أو تطبيق iOS. حدث واحد لكل عملية جدولة مهما كان عدد الحسابات.

data

{
  "post_id": "6f1c2a8e-4b1d-4c55-9a0e-2d7b3f9c1a42",
  "scheduled_at": "2026-10-01T09:00:00.000Z",
  "scheduled_by": "2a7e4c1b-9f3d-4b6a-8c2e-5d1f0a9b7c34",
  "accounts": [
    {
      "account_id": "b3e9d1f0-7a2c-4e8b-9c4d-1f6a5e2b8c73",
      "platform": "instagram",
      "username": "rawwij",
      "text": "Our autumn collection is here. Visit the store this weekend.",
      "media_type": "image",
      "media_count": 1
    },
    {
      "account_id": "c8a1f4e2-3d5b-4a9c-8e7f-6b2d1c9a0e51",
      "platform": "linkedin",
      "username": "Rawwij",
      "text": "Our autumn collection is here. Visit the store this weekend.",
      "media_type": "image",
      "media_count": 1
    }
  ]
}
post.publishedنُشر المنشور

عند انتهاء جميع حسابات المنشور ونجاح حساب واحد على الأقل. يحمل رابط المنشور ومعرّفه لكل حساب، وسبب الفشل لأي حساب لم يُنشر عليه.

data

{
  "post_id": "6f1c2a8e-4b1d-4c55-9a0e-2d7b3f9c1a42",
  "status": "published",
  "scheduled_at": "2026-10-01T09:00:00.000Z",
  "published_at": "2026-10-01T09:01:12.000Z",
  "error": null,
  "failed_count": 0,
  "accounts": [
    {
      "account_id": "b3e9d1f0-7a2c-4e8b-9c4d-1f6a5e2b8c73",
      "platform": "instagram",
      "username": "rawwij",
      "status": "published",
      "url": "https://www.instagram.com/p/DAbCdEfGhIj/",
      "platform_post_id": "18034567890123456",
      "error": null,
      "text": "Our autumn collection is here. Visit the store this weekend.",
      "media_type": "image",
      "media_count": 1
    }
  ]
}
post.failedفشل المنشور

عند انتهاء جميع حسابات المنشور دون نجاح أي منها. يحمل سبب الفشل لكل حساب.

data

{
  "post_id": "6f1c2a8e-4b1d-4c55-9a0e-2d7b3f9c1a42",
  "status": "failed",
  "scheduled_at": "2026-10-01T09:00:00.000Z",
  "published_at": null,
  "error": "Facebook: You have been temporarily blocked from performing this action.",
  "failed_count": 1,
  "accounts": [
    {
      "account_id": "b3e9d1f0-7a2c-4e8b-9c4d-1f6a5e2b8c73",
      "platform": "facebook",
      "username": "Rawwij",
      "status": "failed",
      "url": null,
      "platform_post_id": null,
      "error": "You have been temporarily blocked from performing this action.",
      "text": "Our autumn collection is here. Visit the store this weekend.",
      "media_type": "image",
      "media_count": 1
    }
  ]
}
post.cancelledأُلغي المنشور

عند إلغاء منشور مجدول من الموقع أو من تطبيق iOS.

data

{
  "post_id": "6f1c2a8e-4b1d-4c55-9a0e-2d7b3f9c1a42",
  "cancelled_by": "2a7e4c1b-9f3d-4b6a-8c2e-5d1f0a9b7c34",
  "post_status": "draft",
  "accounts": [
    {
      "account_id": "b3e9d1f0-7a2c-4e8b-9c4d-1f6a5e2b8c73",
      "platform": "instagram",
      "username": "rawwij"
    }
  ],
  "still_publishing": 0,
  "already_published": 0
}

الحسابات

account.connectedرُبط الحساب

عند ربط حساب جديد، أو إعادة ربط حساب موجود في مساحة العمل.

data

{
  "account_id": "b3e9d1f0-7a2c-4e8b-9c4d-1f6a5e2b8c73",
  "platform": "tiktok",
  "username": "rawwij",
  "reconnected": false,
  "connected_by": "2a7e4c1b-9f3d-4b6a-8c2e-5d1f0a9b7c34"
}
account.disconnectedانقطع الحساب

عند فصل حساب من روّج، أو من المنصة نفسها: إزالة روّج من إعدادات الحساب، أو طلب حذف البيانات، أو سحب الصلاحية.

data

{
  "account_id": "b3e9d1f0-7a2c-4e8b-9c4d-1f6a5e2b8c73",
  "platform": "facebook",
  "username": "Rawwij",
  "source": "platform",
  "reason": "app_removed_on_platform",
  "disconnected_by": null
}
account.token_expiringالحساب على وشك الانتهاء

عندما ينتهي ربط حساب خلال 7 أيام ولا يمكن تجديده تلقائياً (لينكدإن كل 60 يوماً). يُرسل مرة واحدة لكل انتهاء.

data

{
  "account_id": "b3e9d1f0-7a2c-4e8b-9c4d-1f6a5e2b8c73",
  "platform": "linkedin",
  "username": "Rawwij",
  "expires_at": "2026-10-05T11:00:00.000Z",
  "days_left": 6
}
account.expiredانتهت صلاحية الحساب

عندما يتوقف ربط حساب عن العمل ويحتاج إعادة ربط. يتوقف النشر إليه.

data

{
  "account_id": "b3e9d1f0-7a2c-4e8b-9c4d-1f6a5e2b8c73",
  "platform": "youtube",
  "username": "Rawwij",
  "reason": "Token has been expired or revoked."
}

الفريق

team.member_invitedدعوة عضو

عند دعوة شخص إلى مساحة العمل، أو إعادة إرسال دعوة.

data

{
  "team_id": "4e8c1a6d-2b9f-4d7e-b3a1-7c5e0d2f9b16",
  "invite_id": "7b2f9e4c-6a1d-4c8b-9e3f-2d5a8c1b0e67",
  "email": "sara@example.com",
  "role": "editor",
  "invited_by": "2a7e4c1b-9f3d-4b6a-8c2e-5d1f0a9b7c34",
  "expires_at": "2026-10-08T09:00:00.000Z",
  "resent": false
}
team.member_joinedانضمام عضو

عند قبول شخص للدعوة وانضمامه إلى مساحة العمل.

data

{
  "team_id": "4e8c1a6d-2b9f-4d7e-b3a1-7c5e0d2f9b16",
  "user_id": "9d4b2e7a-1c8f-4e3b-a6d5-0f2c7b1e4a98",
  "email": "sara@example.com",
  "role": "editor"
}
team.member_role_changedتغيير دور عضو

عند تغيير دور عضو. نقل الملكية يرسل حدثين: واحد للمالك الجديد وآخر للمالك السابق.

data

{
  "team_id": "4e8c1a6d-2b9f-4d7e-b3a1-7c5e0d2f9b16",
  "user_id": "9d4b2e7a-1c8f-4e3b-a6d5-0f2c7b1e4a98",
  "email": "sara@example.com",
  "old_role": "editor",
  "new_role": "admin",
  "changed_by": "2a7e4c1b-9f3d-4b6a-8c2e-5d1f0a9b7c34"
}
team.member_removedخروج عضو أو إزالته

عند إزالة عضو، أو مغادرته بنفسه، أو إزالته تلقائياً، مثلاً عند تغيير الخطة وتقليل عدد المقاعد.

data

{
  "team_id": "4e8c1a6d-2b9f-4d7e-b3a1-7c5e0d2f9b16",
  "user_id": "9d4b2e7a-1c8f-4e3b-a6d5-0f2c7b1e4a98",
  "email": "sara@example.com",
  "role": "editor",
  "reason": "removed",
  "removed_by": "2a7e4c1b-9f3d-4b6a-8c2e-5d1f0a9b7c34"
}

الاستخدام

usage.thresholdالاستخدام عند 80% و100%

عندما تصل مساحة العمل إلى 80% أو 100% من منشورات X الشهرية أو من المساحة، أو يصل عضو إلى 80% أو 100% من تعليقات الذكاء الاصطناعي الشهرية. مرة واحدة لكل مستوى في كل فترة، ويُفحص كل ساعة.

data

{
  "metric": "x_posts",
  "threshold": 80,
  "used": 320,
  "limit": 400,
  "unit": "posts",
  "user_id": null,
  "period_start": "2026-10-01T00:00:00.000Z",
  "period_end": "2026-11-01T00:00:00.000Z"
}

التحليلات

analytics.dailyالتحليلات اليومية

مرة يومياً بعد الساعة 07:00 بتوقيت UTC: المتابعون والتفاعل ومرات الظهور والوصول لأمس، لكل حساب مرتبط يوفّرها. الرقم الذي لا توفره المنصة يأتي null وليس 0. يوتيوب ولينكدإن وتيليجرام غير مشمولة.

data

{
  "date": "2026-09-30",
  "accounts": [
    {
      "account_id": "b3e9d1f0-7a2c-4e8b-9c4d-1f6a5e2b8c73",
      "platform": "instagram",
      "username": "rawwij",
      "followers": 12480,
      "engagement": 922,
      "impressions": 18230,
      "reach": 9412,
      "synced_at": "2026-10-01T00:15:40.000Z"
    },
    {
      "account_id": "c8a1f4e2-3d5b-4a9c-8e7f-6b2d1c9a0e51",
      "platform": "tiktok",
      "username": "rawwij",
      "followers": 3310,
      "engagement": 1343,
      "impressions": 40210,
      "reach": null,
      "synced_at": "2026-10-01T00:15:52.000Z"
    }
  ]
}

ممارسات جيدة

  • أجب بـ 2xx أولاً ثم نفّذ العمل بعدها، حتى لا تبدو المهمة البطيئة فشلاً.
  • تجاهل التكرار بالاعتماد على id. قد توصل إعادة المحاولة بعد انتهاء المهلة حدثاً عالجته من قبل.
  • لا تعتمد على ترتيب الوصول، واستخدم created_at لترتيب الأحداث.
  • تحقق من كل توقيع، وتعامل مع المفتاح السري ككلمة مرور.
  • تجاهل الحقول التي لا تعرفها. قد تُضاف حقول جديدة، أما الحالية فلن يتغير معناها.