Plusieurs comptes Instagram routés vers un seul webhook endpoint — architecture de routage entry.id, gestion des tokens par client, schéma d'isolation des erreurs
Dans ce guide : Comment fonctionnent les webhooks Instagram multi-comptes · Le pattern de routage entry.id · Gestion des tokens par compte · Schéma de base de données pour le multi-comptes · Isolation des données clients · Gestion des erreurs et circuit breakers · Onboarding d'un nouveau compte client · SocialHook comme couche managée

Comment fonctionnent les webhooks Instagram multi-comptes sous le capot

Quand vous enregistrez une URL de webhook dans votre App Facebook et que vous l'abonnez aux événements Instagram, cette unique URL reçoit les événements de chaque Instagram Business Account qui a accordé à votre App la permission instagram_manage_messages. C'est conçu ainsi — l'architecture webhook de Meta est fondamentalement multi-tenant. Une App, une URL de webhook, un nombre illimité de comptes.

Le mécanisme : quand un client envoie un DM à n'importe quel compte Instagram connecté, Meta déclenche un HTTP POST vers votre URL de webhook. Le corps de la requête contient un tableau entry, où chaque élément représente les événements d'un compte. Le champ entry[0].id est l'Instagram Business Account ID (IGID) qui a généré l'événement.

Voici le tableau complet de ce dont vous avez besoin pour construire un système multi-comptes :

  • Une App Facebook avec un seul enregistrement de webhook (une URL, un verify token, un App Secret)
  • Un Page Access Token par compte Instagram — chaque token est spécifique à la Page Facebook liée à ce compte Instagram
  • Une table de routage qui mappe chaque Instagram Business Account ID à son Page Access Token, ses données client et son contexte de base de données
  • L'isolation des clients pour que les erreurs, les rate limits ou les données d'un client n'affectent jamais ceux d'un autre

Le champ entry.id : la clé de routage de tout

Chaque événement webhook Instagram que vous recevez — qu'il s'agisse d'un DM, d'une réponse à une story, d'une mention de story ou d'une réaction — contient l'Instagram Business Account ID dans entry[0].id. C'est le champ qui route l'événement vers le bon client. Stockez-le comme clé primaire dans votre table accounts. Faites une recherche dessus pour chaque événement entrant. Ne traitez jamais un événement sans avoir d'abord résolu à quel client il appartient.

Payload webhook brut — entry.id est votre clé de routage
{ "object": "instagram", "entry": [{ "id": "987654321098765", // ← C'EST VOTRE CLÉ DE ROUTAGE // = Instagram Business Account ID du compte qui a reçu le DM // = le même ID utilisé dans POST /{this_id}/messages pour envoyer les réponses "time": 1747231892, "messaging": [{ "sender": { "id": "12345678901234" }, // IGSID du client "recipient": { "id": "987654321098765" }, // identique à entry.id "message": { "text": "Hello!" } }] }] } // entry[0].id vous dit : lequel des comptes Instagram de votre client a reçu ce DM // Utilisez-le pour retrouver : Page Access Token, client_id, shard de base de données, handler
entry peut contenir plusieurs éléments dans un seul POST. Si deux comptes de vos clients reçoivent des DMs dans le même lot de livraison, Meta peut les regrouper dans un seul POST avec deux éléments dans le tableau entry. Bouclez toujours sur toutes les entries, pas seulement entry[0]. Chaque élément a son propre champ id pointant vers un compte Instagram différent.

Architecture : une URL, plusieurs clients

Architecture de routage des webhooks multi-comptes
→→→
Une seule URL de webhook
yourserver.com/webhook
→→→
Route via
entry[0].id
Handler Client A
Token A · DB shard A
Handler Client B
Token B · DB shard B
Handler Client C
Token C · DB shard C
Handler Client D
Token D · DB shard D

Gestion des tokens : un token par compte

Chaque Instagram Business Account nécessite son propre Page Access Token — généré pour la Page Facebook liée à ce compte. C'est ce token que vous utilisez pour appeler la Send API Instagram pour ce compte spécifique. Vous ne pouvez pas utiliser le token du Client A pour envoyer des messages depuis le compte du Client B.

Règles de stockage des tokens :

  • Chiffrement au repos. Les Page Access Tokens sont des identifiants sensibles. Stockez-les chiffrés dans votre base de données (AES-256), pas en clair. Déchiffrez-les uniquement quand un appel API en a besoin.
  • Ne jamais logger les tokens. Assurez-vous que votre handler de webhook, votre logger d'erreurs et votre pipeline analytics ne logguent jamais la valeur brute du token.
  • Rotation des tokens. Les Page Access Tokens longue durée n'expirent pas par défaut, mais faites-en la rotation périodiquement (chaque trimestre) et chaque fois qu'un client se désabonne. Révoquez immédiatement les tokens des comptes qui se déconnectent de votre plateforme.
  • Un token par compte, chargé au moment de la requête. Ne mettez pas les tokens en cache mémoire indéfiniment — chargez-les depuis la base de données au moment de la requête pour que la révocation prenne effet immédiatement.
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 };

Schéma de base de données pour la gestion multi-comptes

Trois tables couvrent le modèle de données multi-comptes complet. La table ig_accounts est la table de routage — consultée à chaque événement entrant. La table igsids stocke les identifiants utilisateurs scopés à chaque compte, garantissant une isolation totale entre les clients.

Table ig_accounts — le registre de routage

ColonneTypeNotes
ig_business_id PKVARCHAR(64)entry[0].id du webhook — aussi paramètre de chemin dans la Send API. À stocker comme chaîne.
client_id FKUUIDRéférence la table clients. Chaque requête filtre sur ce champ.
encrypted_tokenTEXTPage Access Token chiffré en AES-256. À déchiffrer uniquement au moment de la requête.
ig_usernameVARCHAR(64)Label lisible. @handle du compte Instagram.
activeBOOLEANPasser à false quand un client se désabonne. Empêche le traitement d'événements provenant de comptes déconnectés.
created_atTIMESTAMPTZDate de connexion du compte.
token_rotated_atTIMESTAMPTZSuivre l'ancienneté du token pour appliquer la politique de rotation.

Table igsids — identifiants utilisateurs par compte

ColonneTypeNotes
igsid PK partVARCHAR(64)Instagram-Scoped ID de l'utilisateur. Scopé à CE compte uniquement.
ig_business_id PK part FKVARCHAR(64)PK composite avec igsid. Le même utilisateur écrivant à deux clients = deux IGSIDs différents.
client_id FKUUIDDénormalisé pour des requêtes rapides scopées au client.
first_seenTIMESTAMPTZPremier DM de cet IGSID vers ce compte.
last_messageTIMESTAMPTZPour suivre la fenêtre de 24 heures par compte.
metadataJSONBToute donnée spécifique au client (email, nom, Shopify customer ID, etc.)

Le handler de routage multi-comptes

Voici le handler POST du webhook qui traite correctement les événements d'un nombre quelconque de comptes Instagram connectés. Toutes les contraintes de production sont couvertes : plusieurs entries dans un POST, IDs de compte inconnus, comptes inactifs, et isolation propre du contexte par client.

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); // toujours acquitter immédiatement const body = req.body; if (body.object !== 'instagram') return; // Traiter TOUTES les entries — Meta peut grouper plusieurs comptes dans un seul POST for (const entry of body.entry) { const igBusinessId = entry.id; // ← LA CLÉ DE ROUTAGE // Charger la config du compte — résout token, client_id et statut actif const account = await db.query( 'SELECT * FROM ig_accounts WHERE ig_business_id = $1 AND active = true', [igBusinessId] ); if (!account.rows[0]) { // Compte inconnu ou déconnecté — logger et ignorer console.warn(`Unknown/inactive IG account: ${igBusinessId}`); continue; } const { client_id, encrypted_token, ig_username } = account.rows[0]; const pageToken = decryptToken(encrypted_token); // Construire le contexte scopé au client — injecté dans tous les handlers const ctx = { igBusinessId, // utilisé pour le chemin de la Send API clientId: client_id, pageToken, // pour envoyer les réponses accountName: ig_username, sendDM: (igsid, text) => sendInstagramDM(igBusinessId, pageToken, igsid, text), logEvent: (data) => analytics.log({ ...data, client_id, igBusinessId }), }; // Traiter chaque événement de messagerie dans l'entry de ce compte for (const event of (entry.messaging || [])) { // Traiter dans un try/catch pour qu'une erreur d'un compte n'affecte pas les autres try { await processEvent(event, ctx); } catch (err) { // Logger avec le contexte client — ne jamais re-throw (ça sauterait les comptes restants) console.error(`Error processing event for ${ig_username} [${igBusinessId}]:`, err.message); await errorTracker.capture(err, { igBusinessId, client_id }); } } } }); // Wrapper de la Send API conscient du contexte client 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(); }

Isolation des données clients : éviter la contamination croisée

Dans un système multi-tenant, le pire mode de défaillance est que les données d'un client apparaissent dans le contexte d'un autre. Chaque requête de base de données qui touche aux données utilisateur — historique de conversation, enregistrements IGSID, analytics — doit être filtrée par client_id ET ig_business_id.

Node.js — client-scoped database operations
clientDB.js
// ✗ Faux — pas de scope client, pourrait retourner les conversations de n'importe quel compte const convos = await db.query('SELECT * FROM conversations WHERE igsid = $1', [igsid]); // ✓ Correct — toujours scoper par ig_business_id ET 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 : passer ctx partout, ne jamais passer d'IDs bruts ──────────────────── // Upsert d'un enregistrement IGSID scopé à ce compte 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] ); } // Logger un message de conversation scopé à ce compte 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] ); }

Gestion des erreurs et circuit breakers

Dans un système webhook mono-compte, une erreur peut remonter et arrêter tout le traitement. Dans un système multi-comptes, une erreur pour le Client A ne doit jamais empêcher le traitement des événements pour les Clients B, C et D. Deux patterns sont essentiels :

try/catch par compte (déjà montré ci-dessus) : Encadrez le traitement des événements de chaque compte dans son propre try/catch à l'intérieur de la boucle entry. Loggez les erreurs avec le contexte client. Ne re-throwez jamais — une erreur re-throwée à l'intérieur de la boucle interromprait toutes les entries restantes.

Circuit breaker par compte : Si un compte produit des erreurs répétées (token invalide, rate limit, permission de l'App révoquée), arrêtez temporairement de traiter les événements de ce compte plutôt que de marteler un endpoint cassé à chaque événement.

Node.js — per-account circuit breaker
circuitBreaker.js
const errorCounts = new Map(); // igBusinessId → { count, firstErrorAt } const CIRCUIT_THRESHOLD = 5; // ouvre le circuit après 5 erreurs const RESET_AFTER_MS = 10 * 60 * 1000; // reset après 10 minutes function isCircuitOpen(igBusinessId) { const state = errorCounts.get(igBusinessId); if (!state) return false; // Auto-reset après 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`); // Alertez votre équipe ops via Slack/PagerDuty ici } } // Dans la boucle par compte du router : 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); }

Onboarding d'un nouveau compte client

Chaque fois qu'un nouveau client connecte son compte Instagram à votre plateforme, vous devez exécuter une séquence d'étapes précise. Construire ça comme un flow d'onboarding formel (plutôt que des inserts manuels en base) rend le passage à l'échelle vers de nombreux clients gérable :

  1. OAuth ou collecte manuelle du token : Soit vous implémentez l'OAuth Facebook (pour du self-serve), soit vous générez manuellement un Page Access Token dans le Meta Developer Portal. Le token doit inclure les permissions instagram_manage_messages, pages_messaging et pages_read_engagement.
  2. Résoudre l'Instagram Business Account ID : Appelez GET /me?fields=instagram_business_account avec le Page Access Token. L'instagram_business_account.id dans la réponse est la valeur que vous stockez comme ig_business_id dans votre table accounts.
  3. Vérifier que le token a l'accès messagerie : Testez avec un appel GET /{ig_business_id}?fields=id,name. S'il réussit, le token a les permissions nécessaires.
  4. Insérer dans votre table accounts : Stockez le token chiffré, le client_id et l'ig_business_id. Mettez active = true.
  5. Votre webhook reçoit déjà leurs événements. Aucun ré-enregistrement requis. L'abonnement webhook existant sur votre App Facebook commence automatiquement à livrer les événements de tout compte qui accorde des permissions à votre App. La recherche en base de votre router prend en charge le nouveau compte automatiquement.

SocialHook : la couche multi-comptes managée

Construire et maintenir un router webhook multi-comptes — avec chiffrement, circuit breakers, isolation des clients et flows d'onboarding — représente des semaines de travail d'infrastructure qui ne sont pas votre cœur de produit. SocialHook gère tout cela en tant qu'infrastructure managée.

Connectez tous les comptes Instagram de vos clients à un seul workspace SocialHook. Les événements de chaque compte client arrivent à votre webhook endpoint unique dans le format normalisé, avec le contexte client déjà résolu :

SocialHook — multi-account events already routed
// Chaque événement de n'importe quel compte connecté : { "platform": "instagram", "event": "message.received", "ig_business_id": "987654321098765", // ← déjà extrait "account_name": "clientA_brand", // ← label lisible "from": "12345678901234", // ← IGSID "message": { "type": "text", "body": "Hello!" }, "signature_verified": true } // Votre handler utilise ig_business_id pour le routage — aucun lookup DB nécessaire app.post('/webhook', express.json(), async (req, res) => { res.sendStatus(200); const { ig_business_id, account_name, from, message } = req.body; // Router vers le handler spécifique au client via ig_business_id await handlers[ig_business_id]?.handle(from, message); // Ou plus simplement : utilisez ig_business_id comme clé de partition client dans votre DB });

Questions fréquentes

Une seule URL de webhook Instagram peut-elle recevoir les événements de plusieurs comptes ?
Oui. L'architecture webhook de Meta est multi-tenant par conception. Un seul enregistrement d'App Facebook livre les événements de tous les comptes Instagram qui ont accordé à votre App la permission instagram_manage_messages. Les événements de différents comptes arrivent dans le même corps de POST, différenciés par entry[0].id — qui est l'Instagram Business Account ID. Votre router lit ce champ et dispatche vers le bon handler client.
Comment identifier quel compte Instagram a envoyé un événement webhook ?
Vérifiez entry[0].id dans le payload webhook. C'est l'Instagram Business Account ID (IGID) qui a reçu le DM. C'est aussi l'ID numérique que vous utilisez comme paramètre de chemin quand vous appelez la Send API : POST /{entry[0].id}/messages. Stockez un mapping de cet ID vers le Page Access Token de votre client, le client_id et le contexte de base de données. Recherchez-le à chaque événement entrant.
Faut-il ré-enregistrer mon webhook quand j'ajoute un nouveau compte Instagram ?
Non. Votre enregistrement de webhook sur votre App Facebook est statique — il ne change pas par compte. Quand un nouveau compte Instagram accorde à votre App la permission instagram_manage_messages, ses événements commencent automatiquement à arriver sur votre URL de webhook enregistrée. Il vous suffit d'insérer ses identifiants dans votre table de routage accounts. Votre router existant gère le reste via la recherche sur entry[0].id.
entry peut-il contenir plusieurs comptes dans un seul POST ?
Oui. Si plusieurs comptes connectés reçoivent des messages dans le même lot de livraison, Meta peut les regrouper dans un seul POST avec plusieurs éléments dans le tableau entry. Bouclez toujours sur toutes les entries — for (const entry of body.entry) — pas seulement sur entry[0]. Chaque élément a son propre id pointant vers un Instagram Business Account différent.
Comment empêcher les erreurs d'un client d'affecter les autres clients ?
Encadrez le traitement des événements de chaque compte dans son propre try/catch à l'intérieur de la boucle entry. Loggez l'erreur avec le contexte client. Ne re-throwez jamais — une erreur re-throwée dans la boucle interrompt toutes les entries restantes. Pour les erreurs répétées sur un compte, implémentez un circuit breaker par compte qui ignore temporairement ce compte. Les deux patterns sont montrés dans la section gestion des erreurs ci-dessus.
Quel est le schéma de base de données correct pour les données webhook Instagram multi-comptes ?
Trois tables clés : clients (une ligne par client business), ig_accounts (une ligne par compte Instagram, avec Page Access Token chiffré et ig_business_id comme clé primaire), et igsids (clé primaire composite igsid + ig_business_id — ce qui garantit l'isolation des espaces de noms IGSID entre les comptes). Chaque requête sur les données utilisateur doit filtrer à la fois par ig_business_id ET client_id.

Tous les comptes clients.
Un seul webhook. Zéro code de routage.

Connectez chaque compte Instagram de vos clients à SocialHook. Les événements arrivent sur votre endpoint pré-routés par compte, vérifiés HMAC, normalisés. Ajoutez un nouveau compte client en quelques minutes — aucun changement de code, aucun ré-enregistrement, aucune nouvelle URL de webhook. 50 $/mois quel que soit le nombre de comptes que vous gérez.

Aucune carte bancaire requise · 50 $/mois après l'essai · Instagram + Messenger + WhatsApp