Construyendo una Plataforma de Atención al Cliente en Redes Sociales
Los clientes modernos esperan soporte donde sea que estén. Una plataforma robusta de atención al cliente en redes sociales debe agregar conversaciones de Facebook, Instagram, Twitter, LinkedIn y más en una única interfaz donde los equipos de soporte puedan responder rápida y consistentemente. Esta guía te lleva a través de la construcción de dicha plataforma desde cero, cubriendo decisiones de arquitectura, infraestructura en tiempo real, colaboración de equipos y asistencia impulsada por IA.
Al final de este artículo, tendrás código TypeScript funcional para una plataforma de soporte social al cliente que maneja agregación de mensajes multicanal, enrutamiento inteligente, seguimiento de SLA e integración perfecta con CRM.
La Necesidad de Atención al Cliente en Redes Sociales
Los clientes ya no esperan en espera ni envían correos electrónicos. Tuitean quejas, envían DM a marcas en Instagram y dejan comentarios en publicaciones de Facebook esperando respuestas rápidas. Las investigaciones muestran que el 40% de los consumidores espera una respuesta dentro de una hora en redes sociales, y el 79% espera una respuesta dentro de 24 horas.
Construir un helpdesk de redes sociales efectivo requiere resolver varios desafíos técnicos:
- Fragmentación de canales: Cada plataforma tiene diferentes APIs, límites de tasa y formatos de datos
- Capacidad de respuesta en tiempo real: Los clientes esperan reconocimiento inmediato
- Coordinación de equipos: Múltiples agentes necesitan trabajar sin interferir entre sí
- Preservación del contexto: Los agentes necesitan el historial del cliente en todos los canales
- Seguimiento del rendimiento: La gerencia necesita visibilidad de los tiempos de respuesta y tasas de resolución
Una API de soporte omnicanal bien arquitecturada abstrae estas complejidades, dando a tu equipo una interfaz unificada independientemente de dónde se originen las conversaciones.
Visión General de la Arquitectura de la Plataforma
La arquitectura sigue un diseño modular que separa responsabilidades y escala de forma independiente:

// types/customer-service.ts
export interface SupportPlatformConfig {
platforms: SupportedPlatform[];
webhookEndpoint: string;
routingStrategy: 'round-robin' | 'skill-based' | 'load-balanced';
slaDefaults: SLAConfiguration;
aiAssistEnabled: boolean;
}
export interface SupportedPlatform {
name: 'facebook' | 'instagram' | 'twitter' | 'linkedin' | 'bluesky' | 'telegram';
features: ('messages' | 'comments' | 'reviews')[];
credentials: PlatformCredentials;
webhookSecret?: string;
}
export interface Conversation {
id: string;
platform: string;
accountId: string;
customerId: string;
customerName: string;
customerHandle: string;
customerAvatarUrl?: string;
status: 'open' | 'pending' | 'resolved' | 'closed';
priority: 'low' | 'medium' | 'high' | 'urgent';
assignedTo?: string;
tags: string[];
messages: Message[];
metadata: ConversationMetadata;
createdAt: Date;
updatedAt: Date;
firstResponseAt?: Date;
resolvedAt?: Date;
}
export interface Message {
id: string;
conversationId: string;
direction: 'inbound' | 'outbound';
content: string;
contentType: 'text' | 'image' | 'video' | 'attachment';
attachments?: Attachment[];
senderId: string;
senderType: 'customer' | 'agent' | 'bot';
platform: string;
platformMessageId: string;
timestamp: Date;
deliveryStatus?: 'sent' | 'delivered' | 'read' | 'failed';
}
export interface SLAConfiguration {
firstResponseMinutes: number;
resolutionHours: number;
businessHoursOnly: boolean;
businessHours: {
timezone: string;
schedule: WeeklySchedule;
};
}
export interface ConversationMetadata {
source: 'dm' | 'comment' | 'mention' | 'review';
postId?: string;
postUrl?: string;
sentiment?: 'positive' | 'neutral' | 'negative';
language?: string;
crmContactId?: string;
previousConversations?: number;
}
Los componentes principales incluyen:
| Componente | Responsabilidad | Estrategia de Escalado |
|---|---|---|
| Receptor de Webhook | Ingerir eventos en tiempo real de las plataformas | Horizontal con balanceador de carga |
| Agregador de Mensajes | Normalizar y almacenar conversaciones | Réplicas de lectura para consultas |
| Motor de Enrutamiento | Asignar conversaciones a agentes | Sin estado, basado en eventos |
| Interfaz de Agente | Gestión de conversaciones en tiempo real | Conexiones WebSocket |
| Asistente de IA | Generar sugerencias de respuesta | Workers habilitados para GPU |
| Motor de Analíticas | Rastrear métricas y generar informes | Base de datos de series temporales |
Estrategia de Integración Multicanal
Cada plataforma social tiene características de API únicas. Una plataforma de soporte social al cliente exitosa normaliza estas diferencias mientras preserva las características específicas de cada plataforma.
// services/platform-adapter.ts
import { INBOX_PLATFORMS, isPlatformSupported } from '@/libs/inbox/platforms';
export interface PlatformAdapter {
platform: string;
fetchConversations(accountId: string, since?: Date): Promise<Conversation[]>;
fetchMessages(conversationId: string, cursor?: string): Promise<Message[]>;
sendMessage(conversationId: string, content: MessageContent): Promise<Message>;
markAsRead(conversationId: string): Promise<void>;
}
export class MultiChannelAdapter {
private adapters: Map<string, PlatformAdapter> = new Map();
constructor(private config: SupportPlatformConfig) {
this.initializeAdapters();
}
private initializeAdapters(): void {
for (const platform of this.config.platforms) {
if (!isPlatformSupported(platform.name, 'messages')) {
console.warn(`Platform ${platform.name} does not support direct messages`);
continue;
}
const adapter = this.createAdapter(platform);
this.adapters.set(platform.name, adapter);
}
}
private createAdapter(platform: SupportedPlatform): PlatformAdapter {
// Factory pattern for platform-specific implementations
switch (platform.name) {
case 'facebook':
return new FacebookAdapter(platform.credentials);
case 'instagram':
return new InstagramAdapter(platform.credentials);
case 'twitter':
return new TwitterAdapter(platform.credentials);
case 'linkedin':
return new LinkedInAdapter(platform.credentials);
case 'bluesky':
return new BlueskyAdapter(platform.credentials);
case 'telegram':
return new TelegramAdapter(platform.credentials);
default:
throw new Error(`Unsupported platform: ${platform.name}`);
}
}
async aggregateConversations(
accountIds: string[],
options: AggregationOptions
): Promise<AggregatedConversations> {
const results: Conversation[] = [];
const errors: AggregationError[] = [];
const fetchPromises = accountIds.map(async (accountId) => {
const account = await this.getAccount(accountId);
const adapter = this.adapters.get(account.platform);
if (!adapter) {
return {
accountId,
conversations: [],
error: { message: `No adapter for ${account.platform}` }
};
}
try {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Request timeout')), 10000);
});
const conversations = await Promise.race([
adapter.fetchConversations(accountId, options.since),
timeoutPromise
]);
return { accountId, conversations, error: null };
} catch (error: any) {
return {
accountId,
conversations: [],
error: {
message: error.message,
code: error.code,
retryAfter: error.retryAfter
}
};
}
});
const settledResults = await Promise.all(fetchPromises);
for (const result of settledResults) {
if (result.error) {
errors.push({
accountId: result.accountId,
platform: 'unknown',
error: result.error.message,
code: result.error.code,
retryAfter: result.error.retryAfter
});
} else {
results.push(...result.conversations);
}
}
// Deduplicate by conversation ID
const uniqueConversations = this.deduplicateConversations(results);
// Sort by most recent activity
uniqueConversations.sort((a, b) =>
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
);
return {
conversations: uniqueConversations,
meta: {
accountsQueried: accountIds.length,
accountsFailed: errors.length,
failedAccounts: errors,
lastUpdated: new Date().toISOString()
}
};
}
private deduplicateConversations(conversations: Conversation[]): Conversation[] {
const seen = new Set<string>();
return conversations.filter(conv => {
const key = `${conv.platform}_${conv.customerId}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
}
Nota: Las APIs de las plataformas tienen diferentes límites de tasa. Facebook permite 200 llamadas por hora por usuario, mientras que los límites de Twitter varían según el endpoint. Siempre implementa retroceso exponencial y respeta los encabezados
retryAfter.
Infraestructura de Webhook en Tiempo Real para tu Helpdesk de Redes Sociales
Un helpdesk de redes sociales receptivo requiere entrega de mensajes en tiempo real. Los webhooks eliminan el polling y aseguran que los agentes vean los nuevos mensajes instantáneamente.
// api/webhooks/social/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
import { EventEmitter } from 'events';
const webhookEmitter = new EventEmitter();
interface WebhookPayload {
platform: string;
eventType: string;
data: any;
timestamp: string;
signature?: string;
}
export async function POST(request: NextRequest) {
const platform = request.nextUrl.searchParams.get('platform');
if (!platform) {
return NextResponse.json(
{ error: 'Platform parameter required' },
{ status: 400 }
);
}
try {
const body = await request.text();
const signature = request.headers.get('x-webhook-signature');
// Verify webhook signature
const isValid = await verifyWebhookSignature(platform, body, signature);
if (!isValid) {
console.error(`Invalid webhook signature for ${platform}`);
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
);
}
const payload = JSON.parse(body);
const normalizedEvent = normalizeWebhookEvent(platform, payload);
// Process asynchronously to respond quickly
processWebhookEvent(normalizedEvent).catch(error => {
console.error('Webhook processing error:', error);
});
// Emit for real-time listeners
webhookEmitter.emit('message', normalizedEvent);
return NextResponse.json({ received: true });
} catch (error: any) {
console.error('Webhook error:', error);
return NextResponse.json(
{ error: 'Processing failed' },
{ status: 500 }
);
}
}
async function verifyWebhookSignature(
platform: string,
body: string,
signature: string | null
): Promise<boolean> {
if (!signature) return false;
const secret = process.env[`${platform.toUpperCase()}_WEBHOOK_SECRET`];
if (!secret) {
console.warn(`No webhook secret configured for ${platform}`);
return false;
}
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
function normalizeWebhookEvent(platform: string, payload: any): NormalizedEvent {
switch (platform) {
case 'facebook':
case 'instagram':
return normalizeFacebookEvent(payload);
case 'twitter':
return normalizeTwitterEvent(payload);
case 'linkedin':
return normalizeLinkedInEvent(payload);
default:
throw new Error(`Unknown platform: ${platform}`);
}
}
function normalizeFacebookEvent(payload: any): NormalizedEvent {
const entry = payload.entry?.[0];
const messaging = entry?.messaging?.[0];
if (!messaging) {
return { type: 'unknown', data: payload };
}
return {
type: 'new_message',
platform: payload.object === 'instagram' ? 'instagram' : 'facebook',
conversationId: messaging.sender.id,
message: {
id: messaging.message.mid,
content: messaging.message.text,
senderId: messaging.sender.id,
timestamp: new Date(messaging.timestamp)
},
accountId: entry.id
};
}
async function processWebhookEvent(event: NormalizedEvent): Promise<void> {
if (event.type !== 'new_message') return;
// Find or create conversation
const conversation = await findOrCreateConversation(event);
// Add message to conversation
await addMessageToConversation(conversation.id, event.message);
// Trigger routing if new conversation
if (conversation.isNew) {
await routeConversation(conversation);
}
// Update SLA tracking
await updateSLATracking(conversation.id);
// Generate AI suggestions if enabled
if (process.env.AI_ASSIST_ENABLED === 'true') {
await generateResponseSuggestions(conversation.id, event.message);
}
// Notify assigned agent via WebSocket
await notifyAgent(conversation.assignedTo, {
type: 'new_message',
conversationId: conversation.id,
message: event.message
});
}
export function subscribeToWebhooks(
callback: (event: NormalizedEvent) => void
): () => void {
webhookEmitter.on('message', callback);
return () => webhookEmitter.off('message', callback);
}
Enrutamiento y Asignación de Conversaciones
El enrutamiento inteligente