عدة حسابات Instagram موجَّهة إلى نقطة نهاية webhook واحدة — معمارية توجيه entry.id، إدارة الرموز لكل عميل، مخطط عزل الأخطاء
في هذا الدليل: كيف تعمل Webhooks متعددة الحسابات على Instagram · نمط التوجيه عبر entry.id · إدارة الرموز لكل حساب · مخطط قاعدة البيانات لتعدد الحسابات · عزل بيانات العملاء · معالجة الأخطاء وقواطع الدائرة · إعداد حساب عميل جديد · SocialHook بوصفها الطبقة المُدارة

كيف تعمل Webhooks متعددة الحسابات على Instagram تحت الغطاء

عندما تُسجّل عنوان URL لـ webhook في تطبيق Facebook الخاص بك وتشترك في أحداث Instagram، يستقبل هذا العنوان الواحد الأحداثَ من كل حساب Instagram Business منح تطبيقك صلاحية instagram_manage_messages. هذا تصميم مقصود — معمارية webhook من Meta متعددة المستأجرين جوهرياً. تطبيق واحد، عنوان webhook واحد، عدد لا محدود من الحسابات.

الآلية: عندما يُرسل عميل DM إلى أي حساب Instagram متصل، تُطلق Meta طلب HTTP POST إلى عنوان webhook الخاص بك. يحتوي جسم الطلب على مصفوفة entry، حيث يمثّل كل عنصر فيها أحداث حساب واحد. حقل entry[0].id هو معرّف حساب Instagram Business (IGID) الذي ولّد الحدث.

هذه هي الصورة الكاملة لما تحتاجه لبناء نظام متعدد الحسابات:

  • تطبيق Facebook واحد بتسجيل webhook واحد (عنوان واحد، رمز تحقق واحد، App Secret واحد)
  • Page Access Token واحد لكل حساب Instagram — كل رمز خاص بصفحة Facebook المرتبطة بهذا الحساب
  • جدول توجيه يربط كل معرّف Instagram Business Account برمز Page Access Token الخاص به وبيانات العميل وسياق قاعدة البيانات
  • عزل العملاء بحيث لا تؤثر أبداً أخطاء أحد العملاء أو حدود معدلاته أو بياناته على غيره

حقل entry.id: مفتاح التوجيه لكل شيء

كل حدث Instagram webhook تستقبله — سواء كان DM أو رد ستوري أو إشارة في ستوري أو تفاعل — يحتوي على معرّف حساب Instagram Business عند entry[0].id. هذا هو الحقل الذي يوجّه الحدث إلى العميل الصحيح. خزّنه كمفتاح أساسي في جدول الحسابات. ابحث عنه مع كل حدث وارد. لا تعالج حدثاً أبداً قبل تحديد العميل الذي ينتمي إليه.

Raw webhook payload — entry.id is your routing key
{ "object": "instagram", "entry": [{ "id": "987654321098765", // ← THIS IS YOUR ROUTING KEY // = Instagram Business Account ID of the account that got the DM // = the same ID used in POST /{this_id}/messages to send replies "time": 1747231892, "messaging": [{ "sender": { "id": "12345678901234" }, // IGSID of the customer "recipient": { "id": "987654321098765" }, // same as entry.id "message": { "text": "Hello!" } }] }] } // entry[0].id tells you: which of your client's Instagram accounts received this DM // Use it to look up: Page Access Token, client_id, database shard, handler
قد يحتوي entry على عدة عناصر في طلب POST واحد. إذا استقبل حسابان من حسابات عملائك رسائل DM ضمن نفس دفعة التسليم، فقد تجمعها Meta في طلب POST واحد بعنصرين في مصفوفة entry. كرّر دائماً على جميع العناصر، وليس فقط entry[0]. لكل عنصر حقل id خاص به يشير إلى حساب Instagram مختلف.

المعمارية: عنوان واحد، عملاء كثيرون

معمارية توجيه webhook متعدد الحسابات
→→→
عنوان webhook واحد
yourserver.com/webhook
→→→
التوجيه بحسب
entry[0].id
معالج Client A
Token A · DB shard A
معالج Client B
Token B · DB shard B
معالج Client C
Token C · DB shard C
معالج Client D
Token D · DB shard D

إدارة الرموز: رمز واحد لكل حساب

كل حساب Instagram Business يتطلب رمز Page Access Token خاصاً به — يُولَّد لصفحة Facebook المرتبطة بذلك الحساب. هذا الرمز هو ما تستخدمه لاستدعاء Instagram Send API لذلك الحساب تحديداً. لا يمكنك استخدام رمز Client A لإرسال الرسائل من حساب Client B.

إرشادات تخزين الرموز:

  • التشفير في حالة السكون. رموز Page Access Token اعتمادات حساسة. خزّنها مشفّرة في قاعدة بياناتك (AES-256) وليس كنص صريح. فكّ التشفير فقط عند الحاجة لاستدعاء API.
  • لا تُسجّل الرموز أبداً. تأكد من أن معالج webhook ومُسجّل الأخطاء وخط تحليلاتك لا يسجّلون قيمة الرمز الخام أبداً.
  • تدوير الرموز. رموز Page Access Token طويلة الأمد لا تنتهي صلاحيتها افتراضياً، لكن دوّرها دورياً (فصلياً) وكلما غادر عميل المنصة. أبطل الرموز فوراً للحسابات التي تنفصل عن منصتك.
  • رمز واحد لكل حساب، يُحمَّل وقت الطلب. لا تُخزّن الرموز مؤقتاً في الذاكرة إلى ما لا نهاية — حمّلها من قاعدة البيانات وقت الطلب حتى يصبح الإبطال نافذاً فوراً.
Node.js — encrypted token store
tokenStore.js
const crypto = require('crypto'); const ENC_KEY = Buffer.from(process.env.TOKEN_ENCRYPTION_KEY, 'hex'); // 32 bytes function encryptToken(token) { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv('aes-256-cbc', ENC_KEY, iv); const encrypted = Buffer.concat([cipher.update(token), cipher.final()]); return iv.toString('hex') + ':' + encrypted.toString('hex'); } function decryptToken(stored) { const [ivHex, encHex] = stored.split(':'); const iv = Buffer.from(ivHex, 'hex'); const decipher = crypto.createDecipheriv('aes-256-cbc', ENC_KEY, iv); return Buffer.concat([decipher.update(Buffer.from(encHex, 'hex')), decipher.final()]).toString(); } async function getAccountToken(igBusinessId) { const account = await db.query( 'SELECT encrypted_token FROM ig_accounts WHERE ig_business_id = $1 AND active = true', [igBusinessId] ); if (!account.rows[0]) return null; return decryptToken(account.rows[0].encrypted_token); } module.exports = { encryptToken, decryptToken, getAccountToken };

مخطط قاعدة البيانات لإدارة تعدد الحسابات

ثلاثة جداول تغطي نموذج بيانات تعدد الحسابات كاملاً. جدول ig_accounts هو جدول التوجيه — يُستعلَم عنه مع كل حدث وارد. جدول igsids يخزّن معرّفات المستخدمين ضمن نطاق كل حساب، مما يضمن العزل التام بين العملاء.

جدول ig_accounts — سجل التوجيه

العمودالنوعملاحظات
ig_business_id PKVARCHAR(64)entry[0].id من webhook — وأيضاً معامل المسار في Send API. خزّنه كسلسلة نصية.
client_id FKUUIDيرجع إلى جدول العملاء. كل استعلام يصفّي به.
encrypted_tokenTEXTPage Access Token مشفّر بـ AES-256. فكّ التشفير وقت الطلب فقط.
ig_usernameVARCHAR(64)تسمية مقروءة للبشر. @handle لحساب Instagram.
activeBOOLEANاضبطه على false عند مغادرة العميل. يمنع معالجة الأحداث من حسابات منفصلة.
created_atTIMESTAMPTZوقت ربط الحساب.
token_rotated_atTIMESTAMPTZتتبّع عمر الرمز لفرض سياسة التدوير.

جدول igsids — معرّفات المستخدمين لكل حساب

العمودالنوعملاحظات
igsid PK partVARCHAR(64)Instagram-Scoped ID للمستخدم. ضمن نطاق هذا الحساب فقط.
ig_business_id PK part FKVARCHAR(64)مفتاح أساسي مركّب مع igsid. نفس المستخدم يراسل عميلين = IGSIDs مختلفان.
client_id FKUUIDمُكرّر لتسريع الاستعلامات ضمن نطاق العميل.
first_seenTIMESTAMPTZأول DM من هذا الـ IGSID إلى هذا الحساب.
last_messageTIMESTAMPTZلتتبّع نافذة الـ 24 ساعة لكل حساب.
metadataJSONBأي بيانات خاصة بالعميل (بريد إلكتروني، اسم، معرّف عميل Shopify، إلخ.)

معالج التوجيه متعدد الحسابات

هذا هو معالج طلب POST في webhook الذي يعالج الأحداث بشكل صحيح من أي عدد من حسابات Instagram المتصلة. كل اهتمامات الإنتاج مغطّاة: عناصر متعددة في طلب POST واحد، ومعرّفات حسابات غير معروفة، وحسابات غير نشطة، وعزل سياق نظيف لكل عميل.

Node.js — multi-account Instagram webhook router
multiAccountRouter.js
const { getAccountToken } = require('./tokenStore'); const { verifySignature } = require('./verifySignature'); app.post('/webhook', verifySignature, async (req, res) => { res.sendStatus(200); // always acknowledge immediately const body = req.body; if (body.object !== 'instagram') return; // Process ALL entries — Meta may batch multiple accounts in one POST for (const entry of body.entry) { const igBusinessId = entry.id; // ← THE ROUTING KEY // Load account config — resolves token, client_id, and active status const account = await db.query( 'SELECT * FROM ig_accounts WHERE ig_business_id = $1 AND active = true', [igBusinessId] ); if (!account.rows[0]) { // Unknown or disconnected account — log and skip console.warn(`Unknown/inactive IG account: ${igBusinessId}`); continue; } const { client_id, encrypted_token, ig_username } = account.rows[0]; const pageToken = decryptToken(encrypted_token); // Build client-scoped context — injected into all handlers const ctx = { igBusinessId, // used for Send API path clientId: client_id, pageToken, // for sending replies accountName: ig_username, sendDM: (igsid, text) => sendInstagramDM(igBusinessId, pageToken, igsid, text), logEvent: (data) => analytics.log({ ...data, client_id, igBusinessId }), }; // Process each messaging event within this account's entry for (const event of (entry.messaging || [])) { // Process in try/catch so one account's errors don't affect others try { await processEvent(event, ctx); } catch (err) { // Log with client context — never re-throw (would skip remaining accounts) console.error(`Error processing event for ${ig_username} [${igBusinessId}]:`, err.message); await errorTracker.capture(err, { igBusinessId, client_id }); } } } }); // The client-context-aware Send API wrapper async function sendInstagramDM(igBusinessId, pageToken, igsid, text) { const res = await fetch( `https://graph.facebook.com/v21.0/${igBusinessId}/messages?access_token=${pageToken}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ recipient: { id: igsid }, message: { text } }) } ); if (!res.ok) { const err = await res.json(); throw new Error(`DM send failed [${igBusinessId}]: ${err.error?.message}`); } return await res.json(); }

عزل بيانات العملاء: منع التلوّث المتبادل

في النظام متعدد المستأجرين، أسوأ نمط فشل هو أن تظهر بيانات عميل في سياق عميل آخر. كل استعلام في قاعدة البيانات يمسّ بيانات المستخدمين — تاريخ المحادثات، وسجلات IGSID، والتحليلات — يجب أن يُصفَّى بـ client_id وig_business_id معاً.

Node.js — client-scoped database operations
clientDB.js
// ✗ Wrong — no client scope, could return any account's conversations const convos = await db.query('SELECT * FROM conversations WHERE igsid = $1', [igsid]); // ✓ Correct — always scope by ig_business_id AND client_id const convos = await db.query( 'SELECT * FROM conversations WHERE igsid = $1 AND ig_business_id = $2 AND client_id = $3', [igsid, ctx.igBusinessId, ctx.clientId] ); // ─── Pattern: pass ctx everywhere, never pass raw IDs ──────────────────── // Upsert IGSID record scoped to this account async function upsertUser(igsid, ctx) { return db.query(` INSERT INTO igsids (igsid, ig_business_id, client_id, first_seen, last_message) VALUES ($1, $2, $3, NOW(), NOW()) ON CONFLICT (igsid, ig_business_id) DO UPDATE SET last_message = NOW() RETURNING *`, [igsid, ctx.igBusinessId, ctx.clientId] ); } // Log a conversation message scoped to this account async function logMessage(igsid, text, direction, ctx) { return db.query( 'INSERT INTO messages (igsid, ig_business_id, client_id, text, direction, created_at) VALUES ($1,$2,$3,$4,$5,NOW())', [igsid, ctx.igBusinessId, ctx.clientId, text, direction] ); }

معالجة الأخطاء وقواطع الدائرة

في نظام webhook ذي حساب واحد، يمكن لخطأ أن يتصاعد ويوقف كل المعالجة. في النظام متعدد الحسابات، لا يجب أبداً أن يمنع خطأ في Client A معالجة الأحداث للعملاء B وC وD. هناك نمطان أساسيان:

try/catch لكل حساب (وقد عُرض أعلاه): غلّف معالجة أحداث كل حساب داخل try/catch خاص به داخل حلقة entry. سجّل الأخطاء مع سياق العميل. لا تُعِد رمي الاستثناء أبداً — استثناء معاد رميه داخل الحلقة سيُجهض كل العناصر المتبقية.

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

Node.js — per-account circuit breaker
circuitBreaker.js
const errorCounts = new Map(); // igBusinessId → { count, firstErrorAt } const CIRCUIT_THRESHOLD = 5; // open circuit after 5 errors const RESET_AFTER_MS = 10 * 60 * 1000; // reset after 10 minutes function isCircuitOpen(igBusinessId) { const state = errorCounts.get(igBusinessId); if (!state) return false; // Auto-reset after RESET_AFTER_MS if (Date.now() - state.firstErrorAt > RESET_AFTER_MS) { errorCounts.delete(igBusinessId); return false; } return state.count >= CIRCUIT_THRESHOLD; } function recordError(igBusinessId) { const state = errorCounts.get(igBusinessId) || { count: 0, firstErrorAt: Date.now() }; state.count++; errorCounts.set(igBusinessId, state); if (state.count === CIRCUIT_THRESHOLD) { console.error(`⚡ Circuit opened for ${igBusinessId} after ${CIRCUIT_THRESHOLD} errors`); // Alert your ops team via Slack/PagerDuty here } } // In the router's per-account loop: if (isCircuitOpen(igBusinessId)) { console.warn(`Circuit open for ${igBusinessId} — skipping`); continue; } try { await processEvent(event, ctx); } catch (err) { recordError(igBusinessId); console.error(`Error for ${igBusinessId}:`, err.message); }

إعداد حساب عميل جديد

كلما ربط عميل جديد حسابه على Instagram بمنصتك، تحتاج إلى تنفيذ تسلسل محدد من الخطوات. بناء هذا كتدفّق إعداد رسمي (بدلاً من إدخالات قاعدة بيانات يدوية) يجعل التوسّع إلى عملاء كثيرين ممكناً:

  1. OAuth أو جمع الرموز يدوياً: إما أن تنفّذ Facebook OAuth (للخدمة الذاتية) أو تُولّد Page Access Token يدوياً في بوابة مطوري Meta. يجب أن يتضمن الرمز صلاحيات instagram_manage_messages وpages_messaging وpages_read_engagement.
  2. استخرج معرّف حساب Instagram Business: استدعِ GET /me?fields=instagram_business_account مع Page Access Token. القيمة instagram_business_account.id في الاستجابة هي القيمة التي تخزّنها كـ ig_business_id في جدول حساباتك.
  3. تحقق من أن الرمز يملك صلاحية الرسائل: اختبر باستدعاء GET /{ig_business_id}?fields=id,name. إذا نجح، فإن الرمز يملك الصلاحيات المطلوبة.
  4. أدخِله في جدول حساباتك: خزّن الرمز المشفّر وclient_id وig_business_id. اضبط active = true.
  5. webhook الخاص بك يستقبل أحداثهم بالفعل. لا حاجة لإعادة التسجيل. اشتراك webhook القائم على تطبيق Facebook الخاص بك يبدأ تلقائياً بتسليم الأحداث لأي حساب يمنح تطبيقك الصلاحيات. ويتولّى البحث في قاعدة البيانات من جانب المُوجِّه التعامل مع الحساب الجديد تلقائياً.

SocialHook: الطبقة المُدارة لتعدد الحسابات

بناء وصيانة مُوجِّه webhook متعدد الحسابات — مع التشفير وقواطع الدائرة وعزل العملاء وتدفقات الإعداد — يستغرق أسابيع من العمل على البنية التحتية التي ليست منتجك الأساسي. SocialHook يتولّى كل هذا كبنية تحتية مُدارة.

اربط جميع حسابات عملائك على Instagram بمساحة عمل SocialHook واحدة. تصل الأحداث من كل حساب عميل إلى نقطة نهاية webhook الوحيدة لديك بالصيغة المُوحَّدة، وسياق العميل محلول بالفعل:

SocialHook — multi-account events already routed
// Every event from any connected account: { "platform": "instagram", "event": "message.received", "ig_business_id": "987654321098765", // ← already extracted "account_name": "clientA_brand", // ← human-readable label "from": "12345678901234", // ← IGSID "message": { "type": "text", "body": "Hello!" }, "signature_verified": true } // Your handler uses ig_business_id for routing — no DB lookup needed app.post('/webhook', express.json(), async (req, res) => { res.sendStatus(200); const { ig_business_id, account_name, from, message } = req.body; // Route to client-specific handler by ig_business_id await handlers[ig_business_id]?.handle(from, message); // Or simply: use ig_business_id as the client partition key in your own DB });

أسئلة شائعة

هل يمكن لعنوان webhook واحد على Instagram استقبال الأحداث من عدة حسابات؟
نعم. معمارية webhook من Meta متعددة المستأجرين بحكم التصميم. تسجيل تطبيق Facebook واحد يُسلّم الأحداث من كل حسابات Instagram التي منحت تطبيقك صلاحية instagram_manage_messages. تصل الأحداث من حسابات مختلفة في جسم طلب POST نفسه، ويُميَّز بينها عبر entry[0].id — وهو معرّف حساب Instagram Business. يقرأ مُوجِّهك هذا الحقل ويُرسل الحدث إلى معالج العميل الصحيح.
كيف أعرف أي حساب Instagram أرسل حدث webhook؟
تحقق من entry[0].id في حمولة webhook. هذا هو معرّف حساب Instagram Business (IGID) الذي استقبل الـ DM. وهو أيضاً المعرّف الرقمي الذي تستخدمه كمعامل مسار عند استدعاء Send API: POST /{entry[0].id}/messages. خزّن خريطة تربط هذا المعرّف بـ Page Access Token الخاص بعميلك وclient_id وسياق قاعدة بياناته. ابحث عنه مع كل حدث وارد.
هل أحتاج إلى إعادة تسجيل webhook عند إضافة حساب Instagram جديد؟
لا. تسجيل webhook على تطبيق Facebook الخاص بك ثابت — لا يتغيّر لكل حساب. عندما يمنح حساب Instagram جديد تطبيقك صلاحية instagram_manage_messages، تبدأ أحداثهم بالوصول تلقائياً إلى عنوان webhook المسجّل لديك. كل ما عليك فعله هو إدخال اعتماداتهم في جدول توجيه الحسابات. يتولّى مُوجِّهك القائم الباقي عبر البحث في entry[0].id.
هل يمكن أن يحتوي entry على عدة حسابات في طلب POST واحد؟
نعم. إذا استقبلت عدة حسابات متصلة رسائل ضمن نفس دفعة التسليم، فقد تجمعها Meta في طلب POST واحد بعناصر متعددة في مصفوفة entry. كرّر دائماً على جميع العناصر — for (const entry of body.entry) — وليس فقط entry[0]. لكل عنصر id خاص به يشير إلى حساب Instagram Business مختلف.
كيف أمنع أخطاء عميل من التأثير على عملاء آخرين؟
غلّف معالجة أحداث كل حساب داخل try/catch خاص به داخل حلقة entry. سجّل الخطأ مع سياق العميل. لا تُعِد رمي الاستثناء أبداً — استثناء معاد رميه داخل الحلقة يُجهض كل العناصر المتبقية. للأخطاء المتكررة على حساب واحد، نفّذ قاطع دائرة لكل حساب يتخطى ذلك الحساب مؤقتاً. كلا النمطين معروضان في قسم معالجة الأخطاء أعلاه.
ما هو مخطط قاعدة البيانات الصحيح لبيانات Instagram webhook متعدد الحسابات؟
ثلاثة جداول رئيسية: clients (صف لكل عميل تجاري)، وig_accounts (صف لكل حساب Instagram، مع Page Access Token مشفّر وig_business_id كمفتاح أساسي)، وigsids (مفتاح أساسي مركّب من igsid + ig_business_id — هذا يضمن عزل نطاق أسماء IGSID بين الحسابات). كل استعلام على بيانات المستخدمين يجب أن يصفّي بـ ig_business_id وclient_id معاً.

كل حسابات العملاء.
webhook واحد. صفر كود توجيه.

اربط حساب Instagram لكل عميل بـ SocialHook. تصل الأحداث إلى نقطة نهايتك موجَّهة مسبقاً حسب الحساب، ومُتحقَّق منها بـ HMAC، ومُوحَّدة الصيغة. أضف حساب عميل جديد في دقائق — دون تغييرات في الكود، ودون إعادة تسجيل، ودون عناوين webhook جديدة. 50 دولاراً/شهر بصرف النظر عن عدد الحسابات التي تديرها.

لا حاجة لبطاقة ائتمان · 50 دولاراً/شهر بعد التجربة · Instagram + Messenger + WhatsApp