Cómo conectar varias cuentas de Instagram a un solo webhook endpoint
13 may 2026
·
15 min de lectura
En esta guía: Cómo funcionan los webhooks multi-cuenta de Instagram · Patrón de enrutamiento por entry.id · Gestión de tokens por cuenta · Esquema de base de datos para multi-cuenta · Aislamiento de datos por cliente · Manejo de errores y circuit breakers · Onboarding de una nueva cuenta de cliente · SocialHook como capa gestionada
Cómo funcionan los webhooks multi-cuenta de Instagram por dentro
Cuando registras una URL de webhook en tu Facebook App y la suscribes a eventos de Instagram, esa única URL recibe eventos de cada Instagram Business Account que haya concedido a tu App el permiso instagram_manage_messages. Es así por diseño: la arquitectura de webhooks de Meta es multi-tenant por naturaleza. Una App, una URL de webhook, cuentas ilimitadas.
El mecanismo: cuando un cliente envía un DM a cualquier cuenta de Instagram conectada, Meta dispara un HTTP POST a tu URL de webhook. El cuerpo de la petición contiene un array entry, donde cada elemento representa los eventos de una cuenta. El campo entry[0].id es el Instagram Business Account ID (IGID) que generó el evento.
Este es el panorama completo de lo que necesitas para construir un sistema multi-cuenta:
Una Facebook App con un único registro de webhook (una URL, un verify token, un App Secret)
Un Page Access Token por cuenta de Instagram — cada token es específico de la Facebook Page vinculada a esa cuenta de Instagram
Una tabla de enrutamiento que mapee cada Instagram Business Account ID a su Page Access Token, los datos del cliente y el contexto de base de datos
Aislamiento de clientes para que los errores, rate limits o datos de un cliente nunca afecten a otro
El campo entry.id: la clave de enrutamiento para todo
Todo evento de webhook de Instagram que recibas — ya sea un DM, una respuesta a una story, una mención en una story o una reacción — contiene el Instagram Business Account ID en entry[0].id. Este es el campo que enruta el evento al cliente correcto. Almacénalo como clave primaria en la tabla de cuentas. Búscalo en cada evento entrante. Nunca proceses un evento sin resolver primero a qué cliente pertenece.
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 puede contener varios elementos en un mismo POST. Si dos de las cuentas de tus clientes reciben DMs dentro del mismo batch de entrega, Meta puede agruparlos en un solo POST con dos elementos en el array entry. Itera siempre sobre todos los entries, no solo sobre entry[0]. Cada elemento tiene su propio campo id apuntando a una cuenta de Instagram distinta.
Arquitectura: una URL, muchos clientes
Arquitectura de enrutamiento de webhook multi-cuenta
IG_ID: 987654321
@ClientA Brand
IG_ID: 111222333
@ClientB Store
IG_ID: 444555666
@ClientC Fashion
IG_ID: 777888999
@ClientD Beauty
→→→
Una sola URL de webhook
yourserver.com/webhook
→→→
Enruta por entry[0].id
Handler del Cliente A
Token A · DB shard A
Handler del Cliente B
Token B · DB shard B
Handler del Cliente C
Token C · DB shard C
Handler del Cliente D
Token D · DB shard D
Gestión de tokens: un token por cuenta
Cada Instagram Business Account requiere su propio Page Access Token, generado para la Facebook Page vinculada a esa cuenta. Este token es el que usas para llamar a la Instagram Send API en esa cuenta específica. No puedes usar el token del Cliente A para enviar mensajes en la cuenta del Cliente B.
Pautas para el almacenamiento de tokens:
Cifrado en reposo. Los Page Access Tokens son credenciales sensibles. Almacénalos cifrados en tu base de datos (AES-256), no en texto plano. Descífralos solo cuando los necesites para una llamada a la API.
Nunca loguees los tokens. Asegúrate de que tu webhook handler, logger de errores y pipeline de analítica nunca registren el valor del token en crudo.
Rotación de tokens. Los Page Access Tokens de larga duración no expiran por defecto, pero rótalos periódicamente (trimestralmente) y cada vez que un cliente se dé de baja. Revoca inmediatamente los tokens de las cuentas que se desconecten de tu plataforma.
Un token por cuenta, cargado en el momento de la petición. No mantengas los tokens en memoria de forma indefinida — cárgalos desde la base de datos en el momento de la petición para que la revocación surta efecto al instante.
Esquema de base de datos para gestión multi-cuenta
Tres tablas cubren el modelo de datos multi-cuenta completo. La tabla ig_accounts es la tabla de enrutamiento: se consulta en cada evento entrante. La tabla igsids almacena identificadores de usuario acotados a cada cuenta, asegurando un aislamiento total entre clientes.
Tabla ig_accounts — el registro de enrutamiento
Columna
Tipo
Notas
ig_business_id PK
VARCHAR(64)
entry[0].id del webhook — también es el path param en la Send API. Guárdalo como string.
client_id FK
UUID
Referencia a la tabla clients. Toda consulta filtra por este campo.
encrypted_token
TEXT
Page Access Token cifrado con AES-256. Desciframiento solo en el momento de la petición.
ig_username
VARCHAR(64)
Etiqueta legible para humanos. @handle de la cuenta de Instagram.
active
BOOLEAN
Ponlo en false cuando el cliente se dé de baja. Evita procesar eventos de cuentas desconectadas.
created_at
TIMESTAMPTZ
Cuándo se conectó la cuenta.
token_rotated_at
TIMESTAMPTZ
Lleva el control de la antigüedad del token para aplicar la política de rotación.
Tabla igsids — identificadores de usuario por cuenta
Columna
Tipo
Notas
igsid PK part
VARCHAR(64)
Instagram-Scoped ID del usuario. Acotado SOLO a esta cuenta.
ig_business_id PK partFK
VARCHAR(64)
PK compuesta junto con igsid. El mismo usuario enviando DM a dos clientes = dos IGSIDs distintos.
client_id FK
UUID
Desnormalizado para consultas rápidas acotadas al cliente.
first_seen
TIMESTAMPTZ
Primer DM desde este IGSID hacia esta cuenta.
last_message
TIMESTAMPTZ
Para llevar el control de la ventana de 24 horas por cuenta.
metadata
JSONB
Cualquier dato específico del cliente (email, nombre, Shopify customer ID, etc.)
El handler de enrutamiento multi-cuenta
Este es el handler POST del webhook que procesa correctamente eventos de cualquier número de cuentas de Instagram conectadas. Cubre todas las preocupaciones de producción: múltiples entries en un solo POST, IDs de cuenta desconocidos, cuentas inactivas y aislamiento limpio del contexto por cliente.
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 immediatelyconst body = req.body;
if (body.object !== 'instagram') return;
// Process ALL entries — Meta may batch multiple accounts in one POSTfor (const entry of body.entry) {
const igBusinessId = entry.id; // ← THE ROUTING KEY// Load account config — resolves token, client_id, and active statusconst 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 handlersconst 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 entryfor (const event of (entry.messaging || [])) {
// Process in try/catch so one account's errors don't affect otherstry {
awaitprocessEvent(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 wrapperasync functionsendInstagramDM(igBusinessId, pageToken, igsid, text) {
const res = awaitfetch(
`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();
thrownewError(`DM send failed [${igBusinessId}]: ${err.error?.message}`);
}
returnawait res.json();
}
Aislamiento de datos por cliente: cómo evitar contaminación cruzada
En un sistema multi-tenant, el peor modo de fallo es que los datos de un cliente aparezcan en el contexto de otro. Toda consulta a la base de datos que toque datos de usuario — historial de conversación, registros de IGSID, analítica — debe filtrarse por client_id Y ig_business_id.
Node.js — client-scoped database operations
clientDB.js
// ✗ Wrong — no client scope, could return any account's conversationsconst convos = await db.query('SELECT * FROM conversations WHERE igsid = $1', [igsid]);
// ✓ Correct — always scope by ig_business_id AND client_idconst 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 accountasync functionupsertUser(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 accountasync functionlogMessage(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]
);
}
Manejo de errores y circuit breakers
En un sistema de webhooks de una sola cuenta, un error puede propagarse y detener todo el procesamiento. En un sistema multi-cuenta, un error del Cliente A nunca debe impedir el procesamiento de los eventos de los Clientes B, C y D. Dos patrones son imprescindibles:
try/catch por cuenta (ya mostrado arriba): Envuelve el procesamiento de eventos de cada cuenta en su propio try/catch dentro del bucle de entries. Loguea los errores con el contexto del cliente. Nunca hagas re-throw: un error relanzado dentro del bucle abortaría el resto de las entries.
Circuit breaker por cuenta: Si una cuenta produce errores de forma repetida (token inválido, rate limit, permisos de App revocados), deja de procesar temporalmente los eventos de esa cuenta en lugar de aporrear un endpoint roto en cada evento.
Node.js — per-account circuit breaker
circuitBreaker.js
const errorCounts = newMap(); // igBusinessId → { count, firstErrorAt }constCIRCUIT_THRESHOLD = 5; // open circuit after 5 errorsconstRESET_AFTER_MS = 10 * 60 * 1000; // reset after 10 minutesfunctionisCircuitOpen(igBusinessId) {
const state = errorCounts.get(igBusinessId);
if (!state) returnfalse;
// Auto-reset after RESET_AFTER_MSif (Date.now() - state.firstErrorAt > RESET_AFTER_MS) {
errorCounts.delete(igBusinessId);
returnfalse;
}
return state.count >= CIRCUIT_THRESHOLD;
}
functionrecordError(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 {
awaitprocessEvent(event, ctx);
} catch (err) {
recordError(igBusinessId);
console.error(`Error for ${igBusinessId}:`, err.message);
}
Onboarding de una nueva cuenta de cliente
Cada vez que un nuevo cliente conecta su cuenta de Instagram a tu plataforma, necesitas ejecutar una secuencia específica de pasos. Construir esto como un flujo de onboarding formal (en lugar de inserts manuales en la base de datos) hace que escalar a muchos clientes sea manejable:
OAuth o recogida manual del token: O implementas Facebook OAuth (para self-serve) o generas manualmente un Page Access Token en el Meta Developer Portal. El token debe incluir los permisos instagram_manage_messages, pages_messaging y pages_read_engagement.
Resuelve el Instagram Business Account ID: Llama a GET /me?fields=instagram_business_account con el Page Access Token. El valor de instagram_business_account.id en la respuesta es el que almacenas como ig_business_id en tu tabla de cuentas.
Verifica que el token tiene acceso de mensajería: Prueba con una llamada GET /{ig_business_id}?fields=id,name. Si funciona, el token tiene los permisos necesarios.
Inserta en tu tabla de cuentas: Almacena el token cifrado, el client_id y el ig_business_id. Pon active = true.
Tu webhook ya recibe sus eventos. No hace falta volver a registrar nada. La suscripción de webhook existente en tu Facebook App empieza automáticamente a entregar los eventos de cualquier cuenta que conceda permisos a tu App. La búsqueda en la base de datos de tu router gestiona la nueva cuenta de forma automática.
Node.js — client onboarding sequence
onboarding.js
async functiononboardInstagramAccount(clientId, pageAccessToken) {
// Step 1: Resolve Instagram Business Account ID from the tokenconst meRes = awaitfetch(
`https://graph.facebook.com/v21.0/me?fields=instagram_business_account&access_token=${pageAccessToken}`
);
const me = await meRes.json();
if (!me.instagram_business_account?.id) {
thrownewError('No Instagram Business Account linked to this Page token');
}
const igBusinessId = me.instagram_business_account.id;
// Step 2: Get the account username for human-readable labelingconst igRes = awaitfetch(
`https://graph.facebook.com/v21.0/${igBusinessId}?fields=username&access_token=${pageAccessToken}`
);
const igAccount = await igRes.json();
// Step 3: Encrypt and store the tokenconst encryptedToken = encryptToken(pageAccessToken);
await db.query(`
INSERT INTO ig_accounts (ig_business_id, client_id, encrypted_token, ig_username, active, created_at, token_rotated_at)
VALUES ($1, $2, $3, $4, true, NOW(), NOW())
ON CONFLICT (ig_business_id) DO UPDATE
SET encrypted_token = $3, client_id = $2, active = true, token_rotated_at = NOW()
RETURNING *`,
[igBusinessId, clientId, encryptedToken, igAccount.username || igBusinessId]
);
// Step 4: No webhook re-registration needed — your existing webhook subscription// on your Facebook App automatically covers this account's events.// The router's DB lookup (ig_accounts WHERE ig_business_id = $1) handles it.
console.log(`✓ Onboarded @${igAccount.username} [${igBusinessId}] for client ${clientId}`);
return { igBusinessId, username: igAccount.username };
}
SocialHook: la capa multi-cuenta gestionada
Construir y mantener un router de webhooks multi-cuenta — con cifrado, circuit breakers, aislamiento de clientes y flujos de onboarding — son semanas de trabajo de infraestructura que no son tu producto principal. SocialHook se encarga de todo ello como infraestructura gestionada.
Conecta todas las cuentas de Instagram de tus clientes a un único workspace de SocialHook. Los eventos de cada cuenta de cliente llegan a tu único webhook endpoint en formato normalizado, con el contexto del cliente ya resuelto:
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_idawait handlers[ig_business_id]?.handle(from, message);
// Or simply: use ig_business_id as the client partition key in your own DB
});
FAQ
Preguntas frecuentes
¿Puede una sola URL de webhook de Instagram recibir eventos de varias cuentas?
Sí. La arquitectura de webhooks de Meta es multi-tenant por diseño. Un único registro de Facebook App entrega eventos de todas las cuentas de Instagram que hayan concedido a tu App el permiso instagram_manage_messages. Los eventos de distintas cuentas llegan en el mismo cuerpo POST, diferenciados por entry[0].id, que es el Instagram Business Account ID. Tu router lee este campo y lo despacha al handler del cliente correcto.
¿Cómo identifico qué cuenta de Instagram envió un evento de webhook?
Consulta entry[0].id en el payload del webhook. Es el Instagram Business Account ID (IGID) que recibió el DM. También es el ID numérico que usas como path param al llamar a la Send API: POST /{entry[0].id}/messages. Guarda un mapeo de este ID al Page Access Token del cliente, su client_id y el contexto de base de datos. Búscalo en cada evento entrante.
¿Necesito volver a registrar mi webhook al añadir una nueva cuenta de Instagram?
No. El registro del webhook en tu Facebook App es estático: no cambia por cuenta. Cuando una nueva cuenta de Instagram concede a tu App el permiso instagram_manage_messages, sus eventos empiezan a llegar automáticamente a la URL de webhook registrada. Solo necesitas insertar sus credenciales en tu tabla de enrutamiento de cuentas. Tu router existente se encarga del resto mediante la búsqueda por entry[0].id.
¿Puede entry contener varias cuentas en un mismo POST?
Sí. Si varias cuentas conectadas reciben mensajes dentro del mismo batch de entrega, Meta puede agruparlos en un solo POST con varios elementos en el array entry. Itera siempre sobre todos los entries — for (const entry of body.entry) — no solo sobre entry[0]. Cada elemento tiene su propio id apuntando a un Instagram Business Account distinto.
¿Cómo evito que los errores de un cliente afecten a otros clientes?
Envuelve el procesamiento de eventos de cada cuenta en su propio try/catch dentro del bucle de entries. Loguea el error con el contexto del cliente. Nunca hagas re-throw: un error relanzado dentro del bucle aborta el resto de las entries. Para errores repetidos en una misma cuenta, implementa un circuit breaker por cuenta que la salte temporalmente. Ambos patrones se muestran en la sección de manejo de errores de más arriba.
¿Cuál es el esquema de base de datos correcto para datos de webhooks de Instagram multi-cuenta?
Tres tablas clave: clients (una fila por cliente de negocio), ig_accounts (una fila por cuenta de Instagram, con el Page Access Token cifrado y ig_business_id como clave primaria) y igsids (clave primaria compuesta de igsid + ig_business_id, lo cual garantiza el aislamiento del namespace de IGSID entre cuentas). Toda consulta sobre datos de usuario debe filtrarse tanto por ig_business_id como por client_id.
Todas las cuentas de los clientes. Un solo webhook. Cero código de enrutamiento.
Conecta la cuenta de Instagram de cada cliente a SocialHook. Los eventos llegan a tu endpoint pre-enrutados por cuenta, verificados con HMAC y normalizados. Añade la cuenta de un nuevo cliente en minutos: sin cambios de código, sin volver a registrar nada, sin nuevas URLs de webhook. $50/mes sin importar cuántas cuentas gestiones.