Building a Social Media Customer Service Platform
Modern customers expect support wherever they are. A robust social media customer service platform must aggregate conversations from Facebook, Instagram, Twitter, LinkedIn, and more into a single interface where support teams can respond quickly and consistently. This guide walks through building such a platform from the ground up, covering architecture decisions, real-time infrastructure, team collaboration, and AI-powered assistance.
By the end of this article, you'll have working TypeScript code for a social customer support platform that handles multi-channel message aggregation, intelligent routing, SLA tracking, and seamless CRM integration.
The Need for Social Customer Service
Customers no longer wait on hold or send emails. They tweet complaints, DM brands on Instagram, and leave comments on Facebook posts expecting rapid responses. Research shows that 40% of consumers expect a response within an hour on social media, and 79% expect a response within 24 hours.
Building an effective social media helpdesk requires solving several technical challenges:
- Channel fragmentation: Each platform has different APIs, rate limits, and data formats
- Real-time responsiveness: Customers expect immediate acknowledgment
- Team coordination: Multiple agents need to work without stepping on each other
- Context preservation: Agents need customer history across all channels
- Performance tracking: Management needs visibility into response times and resolution rates
A well-architected omnichannel support API abstracts these complexities, giving your team a unified interface regardless of where conversations originate.
Platform Architecture Overview
The architecture follows a modular design that separates concerns and scales independently:
// 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;
}
The core components include:
| Component | Responsibility | Scaling Strategy |
|---|---|---|
| Webhook Receiver | Ingest real-time events from platforms | Horizontal with load balancer |
| Message Aggregator | Normalize and store conversations | Read replicas for queries |
| Routing Engine | Assign conversations to agents | Stateless, event-driven |
| Agent Interface | Real-time conversation management | WebSocket connections |
| AI Assistant | Generate response suggestions | GPU-enabled workers |
| Analytics Engine | Track metrics and generate reports | Time-series database |
Multi-Channel Integration Strategy
Each social platform has unique API characteristics. A successful social customer support platform normalizes these differences while preserving platform-specific features.
// 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;
});
}
}
Note: Platform APIs have different rate limits. Facebook allows 200 calls per hour per user, while Twitter's limits vary by endpoint. Always implement exponential backoff and respect
retryAfterheaders.
Real-time Webhook Infrastructure for Your Social Media Helpdesk
A responsive social media helpdesk requires real-time message delivery. Webhooks eliminate polling and ensure agents see new messages instantly.
// 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);
}
Conversation Routing and Assignment
Intelligent routing ensures conversations reach the right agent quickly. This is critical for maintaining SLAs in a social media customer service operation.
// services/routing-engine.ts
export interface RoutingRule {
id: string;
name: string;
priority: number;
conditions: RoutingCondition[];
action: RoutingAction;
isActive: boolean;
}
export interface RoutingCondition {
field: 'platform' | 'sentiment' | 'language' | 'keywords' | 'customerTier';
operator: 'equals' | 'contains' | 'in' | 'matches';
value: string | string[];
}
export interface RoutingAction {
type: 'assign_agent' | 'assign_team' | 'round_robin' | 'skill_based';
targetId?: string;
skillRequirements?: string[];
fallbackAction?: RoutingAction;
}
export class RoutingEngine {
private rules: RoutingRule[] = [];
private agentStates: Map<string, AgentState> = new Map();
constructor(private config: RoutingConfig) {
this.loadRules();
this.initializeAgentTracking();
}
async routeConversation(conversation: Conversation): Promise<RoutingResult> {
// Find matching rule
const matchingRule = this.findMatchingRule(conversation);
if (matchingRule) {
return this.executeRoutingAction(matchingRule.action, conversation);
}
// Default to round-robin if no rules match
return this.executeRoundRobin(conversation);
}
private findMatchingRule(conversation: Conversation): RoutingRule | null {
// Sort by priority (higher first)
const sortedRules = [...this.rules]
.filter(r => r.isActive)
.sort((a, b) => b.priority - a.priority);
for (const rule of sortedRules) {
if (this.evaluateConditions(rule.conditions, conversation)) {
return rule;
}
}
return null;
}
private evaluateConditions(
conditions: RoutingCondition[],
conversation: Conversation
): boolean {
return conditions.every(condition => {
const fieldValue = this.getFieldValue(conversation, condition.field);
return this.evaluateCondition(condition, fieldValue);
});
}
private getFieldValue(conversation: Conversation, field: string): any {
switch (field) {
case 'platform':
return conversation.platform;
case 'sentiment':
return conversation.metadata.sentiment;
case 'language':
return conversation.metadata.language;
case 'keywords':
return conversation.messages.map(m => m.content).join(' ');
case 'customerTier':
return conversation.metadata.customerTier;
default:
return null;
}
}
private evaluateCondition(condition: RoutingCondition, value: any): boolean {
switch (condition.operator) {
case 'equals':
return value === condition.value;
case 'contains':
return String(value).toLowerCase().includes(
String(condition.value).toLowerCase()
);
case 'in':
return Array.isArray(condition.value) &&
condition.value.includes(value);
case 'matches':
return new RegExp(String(condition.value), 'i').test(String(value));
default:
return false;
}
}
private async executeRoutingAction(
action: RoutingAction,
conversation: Conversation
): Promise<RoutingResult> {
switch (action.type) {
case 'assign_agent':
return this.assignToAgent(action.targetId!, conversation);
case 'assign_team':
return this.assignToTeam(action.targetId!, conversation);
case 'skill_based':
return this.assignBySkills(action.skillRequirements!, conversation);
case 'round_robin':
default:
return this.executeRoundRobin(conversation);
}
}
private async assignBySkills(
requiredSkills: string[],
conversation: Conversation
): Promise<RoutingResult> {
// Find available agents with required skills
const eligibleAgents = Array.from(this.agentStates.entries())
.filter(([_, state]) => {
if (state.status !== 'available') return false;
if (state.currentLoad >= state.maxCapacity) return false;
return requiredSkills.every(skill => state.skills.includes(skill));
})
.sort((a, b) => a[1].currentLoad - b[1].currentLoad);
if (eligibleAgents.length === 0) {
// Queue for later assignment
return {
success: false,
queued: true,
reason: 'No agents with required skills available'
};
}
const [agentId, agentState] = eligibleAgents[0];
return this.assignToAgent(agentId, conversation);
}
private async executeRoundRobin(
conversation: Conversation
): Promise<RoutingResult> {
const availableAgents = Array.from(this.agentStates.entries())
.filter(([_, state]) =>
state.status === 'available' &&
state.currentLoad < state.maxCapacity
)
.sort((a, b) => a[1].lastAssignedAt - b[1].lastAssignedAt);
if (availableAgents.length === 0) {
return {
success: false,
queued: true,
reason: 'No agents available'
};
}
const [agentId] = availableAgents[0];
return this.assignToAgent(agentId, conversation);
}
private async assignToAgent(
agentId: string,
conversation: Conversation
): Promise<RoutingResult> {
// Update conversation
await updateConversation(conversation.id, {
assignedTo: agentId,
status: 'open'
});
// Update agent state
const state = this.agentStates.get(agentId);
if (state) {
state.currentLoad++;
state.lastAssignedAt = Date.now();
}
// Notify agent
await notifyAgent(agentId, {
type: 'conversation_assigned',
conversation
});
return {
success: true,
assignedTo: agentId,
assignedAt: new Date()
};
}
}
Team Collaboration Features
Effective social media customer service requires seamless handoffs and collaboration between agents.
// services/collaboration.ts
export interface CollaborationEvent {
type: 'note_added' | 'mention' | 'transfer' | 'escalation' | 'tag_added';
conversationId: string;
actorId: string;
data: any;
timestamp: Date;
}
export class CollaborationService {
async addInternalNote(
conversationId: string,
agentId: string,
content: string,
mentions: string[] = []
): Promise<InternalNote> {
const note: InternalNote = {
id: generateId(),
conversationId,
authorId: agentId,
content,
mentions,
createdAt: new Date()
};
await saveInternalNote(note);
// Notify mentioned agents
for (const mentionedAgentId of mentions) {
await notifyAgent(mentionedAgentId, {
type: 'mentioned_in_note',
conversationId,
noteId: note.id,
mentionedBy: agentId
});
}
return note;
}
async transferConversation(
conversationId: string,
fromAgentId: string,
toAgentId: string,
reason: string
): Promise<TransferResult> {
const conversation = await getConversation(conversationId);
if (conversation.assignedTo !== fromAgentId) {
throw new Error('Only assigned agent can transfer conversation');
}
// Update assignment
await updateConversation(conversationId, {
assignedTo: toAgentId,
transferHistory: [
...(conversation.transferHistory || []),
{
from: fromAgentId,
to: toAgentId,
reason,
timestamp: new Date()
}
]
});
// Add system note
await this.addInternalNote(
conversationId,
fromAgentId,
`Transferred to agent. Reason: ${reason}`,
[toAgentId]
);
// Notify both agents
await notifyAgent(fromAgentId, {
type: 'conversation_transferred_out',
conversationId
});
await notifyAgent(toAgentId, {
type: 'conversation_transferred_in',
conversationId,
fromAgent: fromAgentId,
reason
});
return { success: true, newAssignee: toAgentId };
}
async escalateConversation(
conversationId: string,
agentId: string,
escalationLevel: 'supervisor' | 'manager' | 'critical',
reason: string
): Promise<EscalationResult> {
const conversation = await getConversation(conversationId);
// Update priority based on escalation level
const priorityMap = {
supervisor: 'high',
manager: 'urgent',
critical: 'urgent'
} as const;
await updateConversation(conversationId, {
priority: priorityMap[escalationLevel],
escalation: {
level: escalationLevel,
reason,
escalatedBy: agentId,
escalatedAt: new Date()
}
});
// Find appropriate escalation target
const escalationTarget = await this.findEscalationTarget(
escalationLevel,
conversation.platform
);
if (escalationTarget) {
await this.transferConversation(
conversationId,
agentId,
escalationTarget.id,
`Escalation (${escalationLevel}): ${reason}`
);
}
// Log escalation for reporting
await logEscalation({
conversationId,
level: escalationLevel,
reason,
agentId,
timestamp: new Date()
});
return {
success: true,
escalationLevel,
assignedTo: escalationTarget?.id
};
}
private async findEscalationTarget(
level: string,
platform: string
): Promise<Agent | null> {
const escalationRoles = {
supervisor: ['supervisor', 'team_lead'],
manager: ['manager', 'director'],
critical: ['manager', 'director', 'vp']
};
const targetRoles = escalationRoles[level] || ['supervisor'];
const availableEscalators = await findAgentsByRole(targetRoles, {
status: 'available',
platforms: [platform]
});
// Return least loaded escalator
return availableEscalators.sort(
(a, b) => a.currentLoad - b.currentLoad
)[0] || null;
}
}
SLA Tracking and Metrics
SLA compliance is critical for any omnichannel support API implementation. Customers expect timely responses, and management needs visibility into performance.
// services/sla-tracker.ts
export interface SLAStatus {
conversationId: string;
firstResponseSLA: {
targetMinutes: number;
elapsedMinutes: number;
breached: boolean;
breachTime?: Date;
respondedAt?: Date;
};
resolutionSLA: {
targetHours: number;
elapsedHours: number;
breached: boolean;
breachTime?: Date;
resolvedAt?: Date;
};
isWithinBusinessHours: boolean;
}
export class SLATracker {
constructor(private config: SLAConfiguration) {}
async calculateSLAStatus(conversation: Conversation): Promise<SLAStatus> {
const now = new Date();
const createdAt = new Date(conversation.createdAt);
// Calculate business hours elapsed
const businessMinutesElapsed = this.calculateBusinessMinutes(
createdAt,
conversation.firstResponseAt || now
);
const businessHoursElapsed = this.calculateBusinessHours(
createdAt,
conversation.resolvedAt || now
);
const firstResponseBreached =
!conversation.firstResponseAt &&
businessMinutesElapsed > this.config.firstResponseMinutes;
const resolutionBreached =
conversation.status !== 'resolved' &&
conversation.status !== 'closed' &&
businessHoursElapsed > this.config.resolutionHours;
return {
conversationId: conversation.id,
firstResponseSLA: {
targetMinutes: this.config.firstResponseMinutes,
elapsedMinutes: businessMinutesElapsed,
breached: firstResponseBreached,
breachTime: firstResponseBreached
? this.calculateBreachTime(createdAt, this.config.firstResponseMinutes)
: undefined,
respondedAt: conversation.firstResponseAt
},
resolutionSLA: {
targetHours: this.config.resolutionHours,
elapsedHours: businessHoursElapsed,
breached: resolutionBreached,
breachTime: resolutionBreached
? this.calculateBreachTime(createdAt, this.config.resolutionHours * 60)
: undefined,
resolvedAt: conversation.resolvedAt
},
isWithinBusinessHours: this.isWithinBusinessHours(now)
};
}
private calculateBusinessMinutes(start: Date, end: Date): number {
if (!this.config.businessHoursOnly) {
return Math.floor((end.getTime() - start.getTime()) / 60000);
}
let minutes = 0;
const current = new Date(start);
while (current < end) {
if (this.isWithinBusinessHours(current)) {
minutes++;
}
current.setMinutes(current.getMinutes() + 1);
}
return minutes;
}
private calculateBusinessHours(start: Date, end: Date): number {
return this.calculateBusinessMinutes(start, end) / 60;
}
private isWithinBusinessHours(date: Date): boolean {
const { timezone, schedule } = this.config.businessHours;
// Convert to business timezone
const localDate = new Date(
date.toLocaleString('en-US', { timeZone: timezone })
);
const dayOfWeek = localDate.getDay();
const daySchedule = schedule[dayOfWeek];
if (!daySchedule || !daySchedule.isWorkday) {
return false;
}
const currentMinutes = localDate.getHours() * 60 + localDate.getMinutes();
const startMinutes = this.parseTime(daySchedule.start);
const endMinutes = this.parseTime(daySchedule.end);
return currentMinutes >= startMinutes && currentMinutes <= endMinutes;
}
private parseTime(time: string): number {
const [hours, minutes] = time.split(':').map(Number);
return hours * 60 + minutes;
}
private calculateBreachTime(start: Date, targetMinutes: number): Date {
if (!this.config.businessHoursOnly) {
return new Date(start.getTime() + targetMinutes * 60000);
}
let minutesCounted = 0;
const current = new Date(start);
while (minutesCounted < targetMinutes) {
if (this.isWithinBusinessHours(current)) {
minutesCounted++;
}
current.setMinutes(current.getMinutes() + 1);
}
return current;
}
async getTeamSLAMetrics(
teamId: string,
dateRange: DateRange
): Promise<TeamSLAMetrics> {
const conversations = await getTeamConversations(teamId, dateRange);
let firstResponseBreaches = 0;
let resolutionBreaches = 0;
let totalFirstResponseMinutes = 0;
let totalResolutionHours = 0;
let respondedCount = 0;
let resolvedCount = 0;
for (const conversation of conversations) {
const status = await this.calculateSLAStatus(conversation);
if (status.firstResponseSLA.breached) {
firstResponseBreaches++;
}
if (status.firstResponseSLA.respondedAt) {
totalFirstResponseMinutes += status.firstResponseSLA.elapsedMinutes;
respondedCount++;
}
if (status.resolutionSLA.breached) {
resolutionBreaches++;
}
if (status.resolutionSLA.resolvedAt) {
totalResolutionHours += status.resolutionSLA.elapsedHours;
resolvedCount++;
}
}
return {
totalConversations: conversations.length,
firstResponseSLA: {
breaches: firstResponseBreaches,
complianceRate: 1 - (firstResponseBreaches / conversations.length),
averageMinutes: respondedCount > 0
? totalFirstResponseMinutes / respondedCount
: 0
},
resolutionSLA: {
breaches: resolutionBreaches,
complianceRate: 1 - (resolutionBreaches / conversations.length),
averageHours: resolvedCount > 0
? totalResolutionHours / resolvedCount
: 0
}
};
}
}
AI-Powered Response Suggestions
Modern social customer support platforms leverage AI to help agents respond faster and more consistently.
// services/ai-assistant.ts
export interface ResponseSuggestion {
id: string;
content: string;
confidence: number;
source: 'template' | 'ai' | 'previous_response';
tone: 'formal' | 'friendly' | 'empathetic';
tags: string[];
}
export class AIAssistant {
private openaiClient: OpenAI;
constructor() {
this.openaiClient = new OpenAI({
apiKey: process.env.OPENAI_API_KEY
});
}
async generateSuggestions(
conversation: Conversation,
maxSuggestions: number = 3
): Promise<ResponseSuggestion[]> {
const suggestions: ResponseSuggestion[] = [];
// Get template-based suggestions first (fast)
const templateSuggestions = await this.findTemplateSuggestions(
conversation
);
suggestions.push(...templateSuggestions);
// Get AI-generated suggestions
if (suggestions.length < maxSuggestions) {
const aiSuggestions = await this.generateAISuggestions(
conversation,
maxSuggestions - suggestions.length
);
suggestions.push(...aiSuggestions);
}
// Get similar previous responses
const similarResponses = await this.findSimilarResponses(conversation);
suggestions.push(...similarResponses);
// Deduplicate and rank
return this.rankSuggestions(suggestions).slice(0, maxSuggestions);
}
private async generateAISuggestions(
conversation: Conversation,
count: number
): Promise<ResponseSuggestion[]> {
const lastMessages = conversation.messages.slice(-5);
const customerMessage = lastMessages
.filter(m => m.direction === 'inbound')
.pop();
if (!customerMessage) {
return [];
}
const systemPrompt = `You are a helpful customer service agent for a brand on social media.
Generate ${count} different response options for the customer message.
Each response should be:
- Professional but friendly
- Concise (suitable for social media)
- Helpful and solution-oriented
- Platform-appropriate for ${conversation.platform}
Respond with a JSON array of objects with "content" and "tone" fields.
Tone options: "formal", "friendly", "empathetic"`;
const conversationContext = lastMessages
.map(m => `${m.direction === 'inbound' ? 'Customer' : 'Agent'}: ${m.content}`)
.join('\n');
try {
const response = await this.openaiClient.chat.completions.create({
model: 'gpt-4-turbo-preview',
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: conversationContext }
],
response_format: { type: 'json_object' },
temperature: 0.7
});
const parsed = JSON.parse(response.choices[0].message.content || '{}');
const suggestions = parsed.suggestions || [];
return suggestions.map((s: any, index: number) => ({
id: `ai_${Date.now()}_${index}`,
content: s.content,
confidence: 0.8 - (index * 0.1),
source: 'ai' as const,
tone: s.tone,
tags: ['ai-generated']
}));
} catch (error) {
console.error('AI suggestion generation failed:', error);
return [];
}
}
private async findTemplateSuggestions(
conversation: Conversation
): Promise<ResponseSuggestion[]> {
const lastMessage = conversation.messages
.filter(m => m.direction === 'inbound')
.pop();
if (!lastMessage) return [];
// Detect intent from message
const intent = await this.detectIntent(lastMessage.content);
// Find matching templates
const templates = await findTemplatesByIntent(intent, {
platform: conversation.platform,
language: conversation.metadata.language
});
return templates.map((template, index) => ({
id: `template_${template.id}`,
content: this.personalizeTemplate(template.content, conversation),
confidence: 0.9 - (index * 0.05),
source: 'template' as const,
tone: template.tone,
tags: template.tags
}));
}
private async detectIntent(message: string): Promise<string> {
const intents = [
{ pattern: /refund|money back|return/i, intent: 'refund_request' },
{ pattern: /track|where.*order|shipping/i, intent: 'order_tracking' },
{ pattern: /broken|not working|defective/i, intent: 'product_issue' },
{ pattern: /cancel/i, intent: 'cancellation' },
{ pattern: /thank|awesome|great/i, intent: 'positive_feedback' },
{ pattern: /angry|frustrated|terrible|worst/i, intent: 'complaint' }
];
for (const { pattern, intent } of intents) {
if (pattern.test(message)) {
return intent;
}
}
return 'general_inquiry';
}
private personalizeTemplate(
template: string,
conversation: Conversation
): string {
return template
.replace(/\{customerName\}/g, conversation.customerName)
.replace(/\{platform\}/g, conversation.platform)
.replace(/\{agentName\}/g, 'Support Team');
}
private rankSuggestions(
suggestions: ResponseSuggestion[]
): ResponseSuggestion[] {
return suggestions.sort((a, b) => b.confidence - a.confidence);
}
}
CRM Integration Patterns
A complete social media helpdesk connects customer conversations to your existing CRM for a 360-degree view.
// services/crm-integration.ts
export interface CRMContact {
id: string;
email?: string;
phone?: string;
name: string;
company?: string;
socialProfiles: SocialProfile[];
tags: string[];
customFields: Record<string, any>;
lifetimeValue?: number;
tier?: 'standard' | 'premium' | 'vip';
}
export interface SocialProfile {
platform: string;
handle: string;
profileId: string;
linkedAt: Date;
}
export class CRMIntegration {
constructor(private crmClient: CRMClient) {}
async enrichConversation(
conversation: Conversation
): Promise<EnrichedConversation> {
// Try to find existing CRM contact
let contact = await this.findContactBySocialProfile(
conversation.platform,
conversation.customerId
);
// Create contact if not found
if (!contact) {
contact = await this.createContactFromConversation(conversation);
}
// Get customer history
const history = await this.getCustomerHistory(contact.id);
return {
...conversation,
crmContact: contact,
customerHistory: history,
enrichedAt: new Date()
};
}
private async findContactBySocialProfile(
platform: string,
profileId: string
): Promise<CRMContact | null> {
try {
const contacts = await this.crmClient.searchContacts({
filter: {
socialProfiles: {
platform,
profileId
}
}
});
return contacts[0] || null;
} catch (error) {
console.error('CRM search failed:', error);
return null;
}
}
private async createContactFromConversation(
conversation: Conversation
): Promise<CRMContact> {
const contact = await this.crmClient.createContact({
name: conversation.customerName,
socialProfiles: [{
platform: conversation.platform,
handle: conversation.customerHandle,
profileId: conversation.customerId,
linkedAt: new Date()
}],
tags: ['social-media-contact'],
customFields: {
firstContactPlatform: conversation.platform,
firstContactDate: conversation.createdAt
}
});
return contact;
}
async syncConversationToCRM(
conversation: Conversation
): Promise<void> {
const contact = await this.findContactBySocialProfile(
conversation.platform,
conversation.customerId
);
if (!contact) return;
// Create activity/interaction record
await this.crmClient.createActivity({
contactId: contact.id,
type: 'social_conversation',
subject: `${conversation.platform} conversation`,
description: this.summarizeConversation(conversation),
metadata: {
conversationId: conversation.id,
platform: conversation.platform,
status: conversation.status,
resolvedAt: conversation.resolvedAt
},
timestamp: conversation.createdAt
});
// Update contact tags based on conversation
const newTags = this.extractTags(conversation);
if (newTags.length > 0) {
await this.crmClient.updateContact(contact.id, {
tags: [...new Set([...contact.tags, ...newTags])]
});
}
}
private summarizeConversation(conversation: Conversation): string {
const messageCount = conversation.messages.length;
const duration = conversation.resolvedAt
? Math.round(
(new Date(conversation.resolvedAt).getTime() -
new Date(conversation.createdAt).getTime()) / 60000
)
: null;
return `${messageCount} messages exchanged${
duration ? `, resolved in ${duration} minutes` : ''
}. Topic: ${conversation.tags.join(', ') || 'General inquiry'}`;
}
private extractTags(conversation: Conversation): string[] {
const tags: string[] = [];
if (conversation.metadata.sentiment === 'negative') {
tags.push('had-negative-experience');
}
if (conversation.priority === 'urgent') {
tags.push('escalation-history');
}
return tags;
}
}
Analytics and Reporting
Data-driven insights help optimize your social media customer service operations.
// services/analytics.ts
export interface AnalyticsDashboard {
period: DateRange;
summary: SummaryMetrics;
platformBreakdown: PlatformMetrics[];
agentPerformance: AgentMetrics[];
trendData: TrendPoint[];
}
export interface SummaryMetrics {
totalConversations: number;
resolvedConversations: number;
averageResponseTime: number;
averageResolutionTime: number;
customerSatisfactionScore: number;
slaComplianceRate: number;
}
export class AnalyticsService {
async generateDashboard(
teamId: string,
dateRange: DateRange
): Promise<AnalyticsDashboard> {
const conversations = await getConversations({
teamId,
createdAt: { $gte: dateRange.start, $lte: dateRange.end }
});
const summary = this.calculateSummaryMetrics(conversations);
const platformBreakdown = this.calculatePlatformMetrics(conversations);
const agentPerformance = await this.calculateAgentMetrics(
teamId,
conversations
);
const trendData = this.calculateTrends(conversations, dateRange);
return {
period: dateRange,
summary,
platformBreakdown,
agentPerformance,
trendData
};
}
private calculateSummaryMetrics(
conversations: Conversation[]
): SummaryMetrics {
const resolved = conversations.filter(
c => c.status === 'resolved' || c.status === 'closed'
);
const responseTimes = conversations
.filter(c => c.firstResponseAt)
.map(c =>
new Date(c.firstResponseAt!).getTime() -
new Date(c.createdAt).getTime()
);
const resolutionTimes = resolved
.filter(c => c.resolvedAt)
.map(c =>
new Date(c.resolvedAt!).getTime() -
new Date(c.createdAt).getTime()
);
return {
totalConversations: conversations.length,
resolvedConversations: resolved.length,
averageResponseTime: responseTimes.length > 0
? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length / 60000
: 0,
averageResolutionTime: resolutionTimes.length > 0
? resolutionTimes.reduce((a, b) => a + b, 0) / resolutionTimes.length / 3600000
: 0,
customerSatisfactionScore: this.calculateCSAT(conversations),
slaComplianceRate: this.calculateSLACompliance(conversations)
};
}
private calculatePlatformMetrics(
conversations: Conversation[]
): PlatformMetrics[] {
const byPlatform = new Map<string, Conversation[]>();
for (const conv of conversations) {
const existing = byPlatform.get(conv.platform) || [];
existing.push(conv);
byPlatform.set(conv.platform, existing);
}
return Array.from(byPlatform.entries()).map(([platform, convs]) => ({
platform,
conversationCount: convs.length,
percentage: (convs.length / conversations.length) * 100,
averageResponseTime: this.calculateAverageResponseTime(convs),
satisfactionScore: this.calculateCSAT(convs)
}));
}
private calculateTrends(
conversations: Conversation[],
dateRange: DateRange
): TrendPoint[] {
const points: TrendPoint[] = [];
const dayMs = 24 * 60 * 60 * 1000;
let current = new Date(dateRange.start);
while (current <= dateRange.end) {
const dayEnd = new Date(current.getTime() + dayMs);
const dayConversations = conversations.filter(c => {
const created = new Date(c.createdAt);
return created >= current && created < dayEnd;
});
points.push({
date: current.toISOString().split('T')[0],
conversationCount: dayConversations.length,
averageResponseTime: this.calculateAverageResponseTime(dayConversations),
resolvedCount: dayConversations.filter(
c => c.status === 'resolved'
).length
});
current = dayEnd;
}
return points;
}
private calculateAverageResponseTime(conversations: Conversation[]): number {
const times = conversations
.filter(c => c.firstResponseAt)
.map(c =>
new Date(c.firstResponseAt!).getTime() -
new Date(c.createdAt).getTime()
);
return times.length > 0
? times.reduce((a, b) => a + b, 0) / times.length / 60000
: 0;
}
private calculateCSAT(conversations: Conversation[]): number {
const rated = conversations.filter(c => c.satisfactionRating);
if (rated.length === 0) return 0;
const sum = rated.reduce((acc, c) => acc + (c.satisfactionRating || 0), 0);
return (sum / rated.length / 5) * 100;
}
private calculateSLACompliance(conversations: Conversation[]): number {
const withSLA = conversations.filter(c => c.slaStatus);
if (withSLA.length === 0) return 100;
const compliant = withSLA.filter(
c => !c.slaStatus?.firstResponseSLA.breached
);
return (compliant.length / withSLA.length) * 100;
}
}
Security and Access Control
Enterprise social customer support platforms require granular access controls and audit logging.
// services/access-control.ts
import { checkInboxAccessWithInheritance } from '@/libs/inbox-access-checker';
export interface Permission {
resource: 'conversations' | 'analytics' | 'settings' | 'team';
actions: ('read' | 'write' | 'delete' | 'admin')[];
}
export interface Role {
id: string;
name: string;
permissions: Permission[];
platformAccess: string[];
accountAccess: 'all' | 'assigned' | 'team';
}
export class AccessControlService {
private roleDefinitions: Map<string, Role> = new Map([
['agent', {
id: 'agent',
name: 'Support Agent',
permissions: [
{ resource: 'conversations', actions: ['read', 'write'] }
],
platformAccess: ['all'],
accountAccess: 'assigned'
}],
['supervisor', {
id: 'supervisor',
name: 'Team Supervisor',
permissions: [
{ resource: 'conversations', actions: ['read', 'write', 'delete'] },
{ resource: 'analytics', actions: ['read'] },
{ resource: 'team', actions: ['read'] }
],
platformAccess: ['all'],
accountAccess: 'team'
}],
['admin', {
id: 'admin',
name: 'Administrator',
permissions: [
{ resource: 'conversations', actions: ['read', 'write', 'delete', 'admin'] },
{ resource: 'analytics', actions: ['read', 'write'] },
{ resource: 'settings', actions: ['read', 'write', 'admin'] },
{ resource: 'team', actions: ['read', 'write', 'admin'] }
],
platformAccess: ['all'],
accountAccess: 'all'
}]
]);
async checkAccess(
userId: string,
resource: string,
action: string,
resourceId?: string
): Promise<AccessCheckResult> {
const user = await getUser(userId);
if (!user) {
return { allowed: false, reason: 'User not found' };
}
// Check inbox subscription access
const rootUser = user.invitedBy
? await getUser(user.invitedBy)
: null;
const hasInboxAccess = await checkInboxAccessWithInheritance(user, rootUser);
if (!hasInboxAccess) {
return {
allowed: false,
reason: 'Inbox addon required',
code: 'INBOX_REQUIRED'
};
}
// Check role-based permissions
const role = this.roleDefinitions.get(user.role);
if (!role) {
return { allowed: false, reason: 'Invalid role' };
}
const permission = role.permissions.find(p => p.resource === resource);
if (!permission || !permission.actions.includes(action as any)) {
return {
allowed: false,
reason: `Role ${role.name} does not have ${action} access to ${resource}`
};
}
// Check resource-specific access
if (resourceId && resource === 'conversations') {
const canAccess = await this.checkConversationAccess(
user,
role,
resourceId
);
if (!canAccess) {
return {
allowed: false,
reason: 'No access to this conversation'
};
}
}
// Log access for audit
await this.logAccess(userId, resource, action, resourceId, true);
return { allowed: true };
}
private async checkConversationAccess(
user: any,
role: Role,
conversationId: string
): Promise<boolean> {
const conversation = await getConversation(conversationId);
if (!conversation) return false;
switch (role.accountAccess) {
case 'all':
return true;
case 'assigned':
return conversation.assignedTo === user.id;
case 'team':
const teamMembers = await getTeamMembers(user.teamId);
return teamMembers.some(m => m.id === conversation.assignedTo);
default:
return false;
}
}
private async logAccess(
userId: string,
resource: string,
action: string,
resourceId: string | undefined,
allowed: boolean
): Promise<void> {
await createAuditLog({
userId,
resource,
action,
resourceId,
allowed,
timestamp: new Date(),
ipAddress: getCurrentIP(),
userAgent: getCurrentUserAgent()
});
}
}
Using Late as Your Foundation
Building a social media customer service platform from scratch requires integrating with multiple social APIs, each with its own authentication flows, rate limits, and data formats. Late provides a unified API that handles this complexity, letting you focus on building features that matter to your support team.
Late's inbox aggregation capabilities align perfectly with customer service requirements:
// Using Late's unified API for customer service
import { LateClient } from '@late/sdk';
const late = new LateClient({
apiKey: process.env.LATE_API_KEY
});
// Fetch all conversations across connected platforms
async function fetchAllConversations(userId: string) {
// Late handles the complexity of aggregating from
// Facebook, Instagram, Twitter, LinkedIn, and more
const { conversations, meta } = await late.inbox.getConversations({
userId,
status: 'open',
sortBy: 'updatedAt',
sortOrder: 'desc'
});
// Meta includes information about any failed account fetches
if (meta.accountsFailed > 0) {
console.warn('Some accounts failed:', meta.failedAccounts);
}
return conversations;
}
// Send a reply through any platform
async function sendReply(
conversationId: string,
content: string
) {
// Late routes the message to the correct platform
const message = await late.inbox.sendMessage({
conversationId,
content,
contentType: 'text'
});
return message;
}
Key advantages of using Late for your omnichannel support API:
| Challenge | DIY Approach | With Late |
|---|---|---|
| Multi-platform auth | Implement OAuth for each platform | Single API key |
| Rate limiting | Build custom rate limiters per platform | Handled automatically |
| Data normalization | Parse different response formats | Unified data model |
| Webhook management | Set up endpoints for each platform | Single webhook URL |
| Token refresh | Handle expiration for each platform | Automatic refresh |
Late's platform supports all the channels your customers use, including Facebook, Instagram, Twitter, LinkedIn, Bluesky, Telegram, and more. The unified inbox API [INTERNAL_LINK:unified-inbox-api] makes it straightforward to build the aggregation layer your support team needs.
For teams building a social media helpdesk, Late eliminates months of integration work. Instead of wrestling with platform-specific quirks, you can focus on routing logic, AI assistance, and the features that differentiate your support experience.
Check out Late's documentation to see how the unified API can accelerate your customer service platform development. The inbox features provide the foundation you need for real-time message aggregation, while the publishing API lets agents respond across all channels from a single interface.

Miquel is the founder of Late, building the most reliable social media API for developers. Previously built multiple startups and scaled APIs to millions of requests.
View all articlesLearn more about Late with AI
See what AI assistants say about Late API and this topic