Skip to main content
backend21 de septiembre de 202517 min de lectura

Automatizando Publicaciones en Redes Sociales: Del Calendario de Contenido a la API

Construyendo un pipeline de automatización de redes sociales — desde la programación de contenido hasta la integración con APIs de las principales plataformas.

automationapisocial-media
Automatizando Publicaciones en Redes Sociales: Del Calendario de Contenido a la API

Gestionar redes sociales para un producto significa publicar consistentemente en múltiples plataformas, cada una con sus propias particularidades de API, flujos de autenticación, requisitos de medios y límites de tasa. Después de publicar manualmente el mismo contenido en cuatro plataformas tres veces por semana, construí un pipeline de automatización que maneja la programación, el formato, el procesamiento de medios y la publicación — con un paso de aprobación para que nada salga sin revisión humana. Este artículo cubre la arquitectura, los detalles específicos de cada plataforma y las compensaciones entre construir algo personalizado y usar herramientas existentes.

Panorama de APIs de Plataformas

Cada plataforma principal tiene su propio ecosistema de API, y la experiencia de desarrollo varía drásticamente. Aquí está el estado honesto de cada una a principios de 2026.

API de Twitter/X

La API de X ha pasado por cambios significativos desde la transición de la plataforma. La API actual (v2) ofrece endpoints de publicación, carga de medios y analítica. El nivel gratuito permite 1,500 publicaciones por mes y acceso de lectura a tus propias publicaciones, lo cual es suficiente para la mayoría de los casos de automatización.

El modelo de autenticación usa OAuth 2.0 con PKCE para acciones en contexto de usuario (publicar en nombre de un usuario) y OAuth 2.0 App-Only para operaciones de lectura. La API está razonablemente bien documentada y el portal de desarrolladores proporciona información clara sobre límites de tasa.

import { TwitterApi } from 'twitter-api-v2';

const client = new TwitterApi({
  appKey: process.env.TWITTER_API_KEY!,
  appSecret: process.env.TWITTER_API_SECRET!,
  accessToken: process.env.TWITTER_ACCESS_TOKEN!,
  accessSecret: process.env.TWITTER_ACCESS_SECRET!,
});

async function postTweet(text: string, mediaIds?: string[]): Promise<string> {
  const tweet = await client.v2.tweet({
    text,
    ...(mediaIds && { media: { media_ids: mediaIds } }),
  });
  return tweet.data.id;
}

async function uploadMedia(filePath: string): Promise<string> {
  const mediaId = await client.v1.uploadMedia(filePath);
  return mediaId;
}

Limitaciones clave: Las publicaciones tienen un límite de 280 caracteres (más para usuarios premium). La carga de imágenes soporta JPEG, PNG, GIF y WEBP hasta 5MB. La carga de video soporta MP4 hasta 512MB pero requiere carga fragmentada para archivos mayores a 15MB. Los hilos requieren crear múltiples publicaciones con configuración de reply, lo cual agrega complejidad.

API Graph de Instagram

La API de Instagram es parte del ecosistema de Meta y requiere una Página de Negocio de Facebook vinculada a una Cuenta Profesional de Instagram. El proceso de configuración es notoriamente complicado — necesitas una App de Meta, verificación de negocio (para ciertas funcionalidades), y la combinación correcta de permisos.

El flujo de publicación es de dos pasos: primero crear un contenedor de medios, luego publicarlo.

async function postToInstagram(
  igUserId: string,
  imageUrl: string,
  caption: string,
  accessToken: string
): Promise<string> {
  // Step 1: Create media container
  const containerResponse = await fetch(
    `https://graph.facebook.com/v19.0/${igUserId}/media`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        image_url: imageUrl, // Must be a publicly accessible URL
        caption,
        access_token: accessToken,
      }),
    }
  );
  const container = await containerResponse.json();

  // Step 2: Publish the container
  const publishResponse = await fetch(
    `https://graph.facebook.com/v19.0/${igUserId}/media_publish`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        creation_id: container.id,
        access_token: accessToken,
      }),
    }
  );
  const result = await publishResponse.json();
  return result.id;
}

Detalles importantes:

  • Las imágenes deben estar alojadas en una URL públicamente accesible. No puedes subir datos binarios directamente. Esto significa que necesitas alojar los medios en S3 o un CDN antes de publicar.
  • Las publicaciones en carrusel (múltiples imágenes) requieren crear contenedores individuales para cada imagen, luego un contenedor de carrusel que los referencia.
  • Los Reels (video) tienen requisitos adicionales: relación de aspecto 9:16, entre 3 y 90 segundos, y el video debe estar completamente procesado antes de publicar.
  • Los tokens de acceso expiran. Los tokens de larga duración duran 60 días y necesitan ser renovados periódicamente.

API de LinkedIn

La API de LinkedIn para publicar contenido usa la "Community Management API" (anteriormente la "Share API"). La autenticación usa OAuth 2.0 con el scope w_member_social para perfiles personales o w_organization_social para páginas de empresa.

async function postToLinkedIn(
  authorUrn: string, // "urn:li:person:xxx" or "urn:li:organization:xxx"
  text: string,
  accessToken: string
): Promise<string> {
  const response = await fetch('https://api.linkedin.com/v2/posts', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
      'X-Restli-Protocol-Version': '2.0.0',
      'LinkedIn-Version': '202401',
    },
    body: JSON.stringify({
      author: authorUrn,
      commentary: text,
      visibility: 'PUBLIC',
      distribution: {
        feedDistribution: 'MAIN_FEED',
        targetEntities: [],
        thirdPartyDistributionChannels: [],
      },
      lifecycleState: 'PUBLISHED',
    }),
  });

  const postId = response.headers.get('x-restli-id');
  return postId || '';
}

Limitaciones clave: La documentación de la API de LinkedIn está dispersa entre múltiples versiones y convenciones de nomenclatura. El límite de tasa es de 100 llamadas API por día por usuario por aplicación para la mayoría de los endpoints, lo cual es generoso para publicar pero ajustado si también estás extrayendo analítica. Las cargas de imágenes requieren un flujo de carga separado con una solicitud de carga registrada.

API de Publicación de Contenido de TikTok

La API de TikTok para publicación de contenido es relativamente nueva y más restringida que otras plataformas. Necesitas solicitar acceso a la Content Posting API específicamente, y la aprobación puede tomar semanas.

El flujo de publicación implica inicializar una publicación, subir el video y luego confirmar la publicación:

async function postToTikTok(
  videoUrl: string,
  caption: string,
  accessToken: string
): Promise<string> {
  // Initialize the publish
  const initResponse = await fetch(
    'https://open.tiktokapis.com/v2/post/publish/video/init/',
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        post_info: {
          title: caption,
          privacy_level: 'SELF_ONLY', // Start as private, change after review
          disable_duet: false,
          disable_comment: false,
          disable_stitch: false,
        },
        source_info: {
          source: 'PULL_FROM_URL',
          video_url: videoUrl,
        },
      }),
    }
  );

  const result = await initResponse.json();
  return result.data.publish_id;
}

Detalles importantes: TikTok requiere que todas las publicaciones automatizadas se divulguen como tales — hay flags específicas en la API para esto. Los videos deben cumplir con las directrices de contenido y requisitos de formato de TikTok. La API tiene límites diarios estrictos de publicación por cuenta de usuario. Lo más crítico es que el ecosistema de API de TikTok cambia frecuentemente, así que verifica la documentación actual antes de implementar.

Flujos de Autenticación

Las cuatro plataformas usan OAuth 2.0, pero los detalles de implementación difieren lo suficiente como para requerir un manejo específico por plataforma.

El Flujo de OAuth

El flujo general es:

  1. Redirigir al usuario a la URL de autorización de la plataforma con el client_id de tu app y los scopes solicitados.
  2. El usuario otorga permiso. La plataforma redirige de vuelta a tu redirect_uri con un code de autorización.
  3. Intercambiar el code por un access_token (y usualmente un refresh_token).
  4. Usar el access_token para llamadas a la API. Renovarlo cuando expire.
// Generic OAuth handler
interface OAuthConfig {
  clientId: string;
  clientSecret: string;
  redirectUri: string;
  authorizationUrl: string;
  tokenUrl: string;
  scopes: string[];
}

async function exchangeCodeForToken(
  config: OAuthConfig,
  code: string
): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> {
  const response = await fetch(config.tokenUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      redirect_uri: config.redirectUri,
      client_id: config.clientId,
      client_secret: config.clientSecret,
    }),
  });

  const data = await response.json();
  return {
    accessToken: data.access_token,
    refreshToken: data.refresh_token,
    expiresIn: data.expires_in,
  };
}

Gestión de Tokens

Los tokens expiran. Los tokens de Instagram duran 60 días. Los tokens de LinkedIn duran 60 días. Los tokens de Twitter duran hasta ser revocados (para OAuth 1.0a) o son de corta duración con refresh tokens (para OAuth 2.0). Los tokens de TikTok duran 24 horas con un refresh token válido por 365 días.

Necesitas un sistema de renovación de tokens:

interface StoredToken {
  platform: string;
  accessToken: string;
  refreshToken: string;
  expiresAt: Date;
}

async function getValidToken(platform: string): Promise<string> {
  const stored = await db.tokens.findUnique({ where: { platform } });
  if (!stored) throw new Error(`No token stored for ${platform}`);

  // Refresh if expiring within 5 minutes
  if (stored.expiresAt < new Date(Date.now() + 5 * 60 * 1000)) {
    const refreshed = await refreshToken(platform, stored.refreshToken);
    await db.tokens.update({
      where: { platform },
      data: {
        accessToken: refreshed.accessToken,
        refreshToken: refreshed.refreshToken || stored.refreshToken,
        expiresAt: new Date(Date.now() + refreshed.expiresIn * 1000),
      },
    });
    return refreshed.accessToken;
  }

  return stored.accessToken;
}

Arquitectura de Programación de Contenido

El sistema de programación es el núcleo del pipeline de automatización. Necesita manejar zonas horarias, formato específico por plataforma, procesamiento de medios y recuperación ante fallos.

Modelo de Datos

interface ScheduledPost {
  id: string;
  content: {
    text: string;
    media?: MediaAttachment[];
    platformOverrides?: Record<string, { text?: string }>;
  };
  platforms: Platform[];
  scheduledAt: Date;  // UTC
  timezone: string;   // IANA timezone for display
  status: 'draft' | 'approved' | 'scheduled' | 'publishing' | 'published' | 'failed';
  results: Record<string, PostResult>;
  retryCount: number;
  createdBy: string;
  approvedBy?: string;
  approvedAt?: Date;
}

interface MediaAttachment {
  originalUrl: string;
  processedUrls: Record<string, string>; // Platform-specific processed versions
  type: 'image' | 'video';
  altText?: string;
}

interface PostResult {
  platform: string;
  postId?: string;
  url?: string;
  error?: string;
  publishedAt?: Date;
}

El Patrón platformOverrides

Cada plataforma tiene diferentes límites de caracteres, convenciones de hashtags y normas de formato. En lugar de crear publicaciones completamente separadas, uso un texto base con sobreescrituras específicas por plataforma:

const post: ScheduledPost = {
  id: 'post-1',
  content: {
    text: 'Excited to announce our new feature: real-time order tracking. Customers can now see exactly where their delivery is. #product #launch',
    platformOverrides: {
      twitter: {
        text: 'Just shipped: real-time order tracking. Your customers can now watch their delivery in real-time. #buildinpublic',
      },
      linkedin: {
        text: 'Excited to announce our latest feature: real-time order tracking.\n\nThis was one of the most requested features from our restaurant partners. Customers can now see exactly where their delivery is, reducing "where is my food?" support tickets by an estimated 40%.\n\nBuilt with WebSockets and a lightweight location service. Happy to share the technical architecture with anyone interested.\n\n#productdevelopment #startups #foodtech',
      },
    },
  },
  platforms: ['twitter', 'instagram', 'linkedin'],
  scheduledAt: new Date('2026-03-15T14:00:00Z'),
  timezone: 'America/New_York',
  status: 'approved',
  results: {},
  retryCount: 0,
  createdBy: 'user-1',
};

Twitter recibe una versión concisa. LinkedIn recibe una versión más larga y profesional. Instagram usa el texto base como caption. Este enfoque respeta las normas de cada plataforma sin mantener calendarios de contenido completamente separados.

Manejo de Medios

El procesamiento de medios es la parte más propensa a errores del pipeline. Cada plataforma tiene diferentes requisitos para dimensiones de imagen, tamaños de archivo, formatos y relaciones de aspecto.

Pipeline de Procesamiento

interface MediaRequirements {
  maxWidth: number;
  maxHeight: number;
  maxFileSize: number; // bytes
  supportedFormats: string[];
  aspectRatios?: { min: number; max: number };
}

const platformRequirements: Record<string, MediaRequirements> = {
  twitter: {
    maxWidth: 4096,
    maxHeight: 4096,
    maxFileSize: 5 * 1024 * 1024, // 5MB
    supportedFormats: ['jpeg', 'png', 'gif', 'webp'],
  },
  instagram: {
    maxWidth: 1440,
    maxHeight: 1440,
    maxFileSize: 8 * 1024 * 1024,
    supportedFormats: ['jpeg', 'png'],
    aspectRatios: { min: 4 / 5, max: 1.91 },
  },
  linkedin: {
    maxWidth: 4096,
    maxHeight: 4096,
    maxFileSize: 10 * 1024 * 1024,
    supportedFormats: ['jpeg', 'png', 'gif'],
  },
};

async function processMediaForPlatform(
  originalUrl: string,
  platform: string
): Promise<string> {
  const requirements = platformRequirements[platform];
  const image = await sharp(await downloadFile(originalUrl));
  const metadata = await image.metadata();

  let processed = image;

  // Resize if exceeding max dimensions
  if (
    (metadata.width && metadata.width > requirements.maxWidth) ||
    (metadata.height && metadata.height > requirements.maxHeight)
  ) {
    processed = processed.resize(requirements.maxWidth, requirements.maxHeight, {
      fit: 'inside',
      withoutEnlargement: true,
    });
  }

  // Enforce aspect ratio for Instagram
  if (requirements.aspectRatios && metadata.width && metadata.height) {
    const currentRatio = metadata.width / metadata.height;
    if (
      currentRatio < requirements.aspectRatios.min ||
      currentRatio > requirements.aspectRatios.max
    ) {
      // Crop to acceptable ratio
      const targetRatio = Math.max(
        requirements.aspectRatios.min,
        Math.min(currentRatio, requirements.aspectRatios.max)
      );
      const newWidth = Math.round(metadata.height * targetRatio);
      processed = processed.resize(newWidth, metadata.height, { fit: 'cover' });
    }
  }

  // Convert to supported format
  const format = requirements.supportedFormats.includes('webp') ? 'webp' : 'jpeg';
  const buffer = await processed.toFormat(format, { quality: 85 }).toBuffer();

  // Upload processed image and return URL
  const processedUrl = await uploadToStorage(buffer, `processed/${platform}/${Date.now()}.${format}`);
  return processedUrl;
}

Usar Sharp para el procesamiento de imágenes mantiene el pipeline rápido y maneja casos extremos como rotación EXIF, conversión de perfil de color y compatibilidad de formato. Para procesamiento de video, FFmpeg maneja la transcodificación, pero el procesamiento de video es significativamente más complejo y consume más recursos — normalmente lo delego a un worker dedicado o una cloud function.

Límites de Tasa y Lógica de Reintentos

Cada plataforma aplica límites de tasa, y alcanzarlos de forma brusca causa publicaciones fallidas y potencialmente bloqueos temporales de la API.

Seguimiento de Límites de Tasa

interface RateLimitState {
  platform: string;
  remaining: number;
  resetAt: Date;
  limit: number;
}

class RateLimiter {
  private limits: Map<string, RateLimitState> = new Map();

  updateFromHeaders(platform: string, headers: Headers): void {
    const remaining = parseInt(headers.get('x-rate-limit-remaining') || '100');
    const resetTimestamp = parseInt(headers.get('x-rate-limit-reset') || '0');

    this.limits.set(platform, {
      platform,
      remaining,
      resetAt: new Date(resetTimestamp * 1000),
      limit: parseInt(headers.get('x-rate-limit-limit') || '100'),
    });
  }

  async waitIfNeeded(platform: string): Promise<void> {
    const state = this.limits.get(platform);
    if (!state) return;

    if (state.remaining <= 1 && state.resetAt > new Date()) {
      const waitMs = state.resetAt.getTime() - Date.now() + 1000; // 1s buffer
      console.log(`Rate limited on ${platform}. Waiting ${waitMs}ms`);
      await new Promise((resolve) => setTimeout(resolve, waitMs));
    }
  }
}

Reintentos con Backoff Exponencial

No todos los fallos son permanentes. Timeouts de red, errores temporales del servidor y límites de tasa son todos reintentables. Errores de base de datos y fallos de autenticación no lo son.

async function publishWithRetry(
  post: ScheduledPost,
  platform: string,
  maxRetries: number = 3
): Promise<PostResult> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      await rateLimiter.waitIfNeeded(platform);
      const result = await publishToPlatform(post, platform);
      return { platform, postId: result.id, url: result.url, publishedAt: new Date() };
    } catch (error) {
      const isRetryable =
        error instanceof RateLimitError ||
        error instanceof NetworkError ||
        (error instanceof ApiError && error.statusCode >= 500);

      if (!isRetryable || attempt === maxRetries) {
        return {
          platform,
          error: error instanceof Error ? error.message : 'Unknown error',
        };
      }

      const backoffMs = Math.min(1000 * Math.pow(2, attempt), 30000);
      const jitter = Math.random() * 1000;
      await new Promise((resolve) => setTimeout(resolve, backoffMs + jitter));
    }
  }

  return { platform, error: 'Max retries exceeded' };
}

El jitter es importante — sin él, múltiples clientes reintentando sincronizados en el mismo programa de backoff crean problemas de "thundering herd" donde todos reintentan simultáneamente.

Flujo de Aprobación de Contenido

Publicar automáticamente sin revisión humana es arriesgado. Un error de formato, una publicación inoportuna durante una crisis, o un enlace incorrecto pueden causar daño real. Incluyo un paso obligatorio de aprobación en el pipeline.

El Flujo de Aprobación

Content Created → Status: DRAFT
       │
       ▼
Preview Generated → Reviewer notified (Slack/email)
       │
       ▼
Reviewer Approves → Status: APPROVED → Scheduled for posting
       │
   or Rejects → Status: DRAFT (with feedback) → Back to creator

El paso de vista previa es importante — muestra al revisor exactamente lo que se publicará en cada plataforma, con conteo de caracteres, vistas previas de medios y vistas previas de enlaces. Esto detecta problemas de formato que no son obvios en el editor de contenido.

async function generatePreview(post: ScheduledPost): Promise<PlatformPreview[]> {
  return post.platforms.map((platform) => {
    const text =
      post.content.platformOverrides?.[platform]?.text || post.content.text;

    return {
      platform,
      text,
      characterCount: text.length,
      characterLimit: getCharacterLimit(platform),
      isOverLimit: text.length > getCharacterLimit(platform),
      mediaPreview: post.content.media?.map((m) => ({
        url: m.processedUrls[platform] || m.originalUrl,
        type: m.type,
        altText: m.altText,
      })),
      estimatedReach: getEstimatedReach(platform),
    };
  });
}

function getCharacterLimit(platform: string): number {
  const limits: Record<string, number> = {
    twitter: 280,
    instagram: 2200,
    linkedin: 3000,
    tiktok: 2200,
  };
  return limits[platform] || 2200;
}

Recolección de Analítica

Después de publicar, recopilar datos de engagement ayuda a optimizar el contenido futuro. Cada plataforma proporciona analítica a través de su API, pero la forma de los datos y su disponibilidad varía.

interface PostAnalytics {
  postId: string;
  platform: string;
  collectedAt: Date;
  impressions: number;
  engagements: number;
  clicks: number;
  shares: number;
  comments: number;
  likes: number;
  engagementRate: number;
}

async function collectAnalytics(postResult: PostResult): Promise<PostAnalytics | null> {
  if (!postResult.postId) return null;

  switch (postResult.platform) {
    case 'twitter':
      return collectTwitterAnalytics(postResult.postId);
    case 'linkedin':
      return collectLinkedInAnalytics(postResult.postId);
    case 'instagram':
      return collectInstagramAnalytics(postResult.postId);
    default:
      return null;
  }
}

Recolecto analítica a la 1 hora, 24 horas y 7 días después de publicar. Esto captura el engagement inmediato, el rendimiento diario y el alcance a largo plazo. Los datos alimentan un dashboard simple que muestra qué tipos de contenido, horarios de publicación y plataformas generan más engagement.

Construir vs Comprar: La Evaluación Honesta

Antes de construir un sistema personalizado, considera si las herramientas existentes cubren tus necesidades.

Cuándo Usar Herramientas Existentes

Buffer ($5-100/mes) maneja programación, publicación en múltiples plataformas y analítica básica. Soporta Twitter, Instagram, LinkedIn, Facebook, Pinterest y TikTok. La API es sólida para programación programática. Para la mayoría de equipos pequeños, Buffer cubre el 90% de las necesidades.

Hootsuite ($99+/mes) agrega colaboración en equipo, flujos de aprobación de contenido y analítica más profunda. Mejor para equipos más grandes con múltiples stakeholders.

Typefully ($12-29/mes) se especializa en Twitter/X con soporte de hilos, colaboración en borradores y analítica de audiencia. Si Twitter es tu plataforma principal, es excelente.

Later ($16.67+/mes) es fuerte para plataformas visuales (Instagram, Pinterest, TikTok) con gestión de biblioteca de medios y planificación de calendario visual.

Cuándo Construir Personalizado

Construir personalizado tiene sentido cuando:

  • Necesitas integración profunda con sistemas internos (CMS, base de datos de productos, CRM)
  • Tu generación de contenido está parcialmente automatizada (borradores generados por IA, publicaciones con plantillas a partir de eventos de producto)
  • Necesitas flujos de aprobación personalizados que las herramientas existentes no soportan
  • Publicas a un volumen donde el precio SaaS se vuelve costoso (50+ publicaciones/día entre cuentas)
  • Las funcionalidades de API de la plataforma que necesitas no están expuestas por herramientas de terceros

El Enfoque Híbrido

Usa una herramienta de programación (Buffer, Hootsuite) para contenido manual y construye automatización personalizada solo para publicaciones programáticas. Muchas herramientas de programación tienen APIs que te permiten crear publicaciones programáticamente mientras usas su dashboard para revisión y aprobación.

// Using Buffer's API for scheduling
async function scheduleViaBuffer(
  profileIds: string[],
  text: string,
  mediaUrls: string[],
  scheduledAt: Date
): Promise<string> {
  const response = await fetch('https://api.bufferapp.com/1/updates/create.json', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      access_token: process.env.BUFFER_ACCESS_TOKEN!,
      text,
      'profile_ids[]': profileIds.join(','),
      scheduled_at: scheduledAt.toISOString(),
      ...(mediaUrls.length && { 'media[photo]': mediaUrls[0] }),
    }),
  });

  const result = await response.json();
  return result.updates[0].id;
}

Esto te da la fiabilidad y la interfaz de una herramienta establecida mientras mantienes la flexibilidad programática del código personalizado.

Notificaciones por Webhook

Cuando una publicación se publica, falla o recibe engagement significativo, el sistema debe notificar al equipo. Uso webhooks para enviar eventos a Slack.

async function notifySlack(event: PostEvent): Promise<void> {
  const color = event.type === 'published' ? '#36a64f' : '#ff0000';
  const title =
    event.type === 'published'
      ? `Posted to ${event.platform}`
      : `Failed to post to ${event.platform}`;

  await fetch(process.env.SLACK_WEBHOOK_URL!, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      attachments: [
        {
          color,
          title,
          text: event.type === 'published'
            ? `"${event.text.substring(0, 100)}..." — ${event.url}`
            : `Error: ${event.error}`,
          fields: [
            { title: 'Platform', value: event.platform, short: true },
            { title: 'Time', value: new Date().toLocaleString(), short: true },
          ],
        },
      ],
    }),
  });
}

Patrones de Manejo de Errores

Las APIs de redes sociales son poco fiables. Los servidores se caen, los límites de tasa cambian, el procesamiento de medios falla. El sistema necesita manejar los fallos de forma elegante sin perder contenido.

La Cola de Mensajes Fallidos

Las publicaciones que fallan después de todos los reintentos van a una cola de mensajes fallidos para revisión manual:

async function handleFailedPost(post: ScheduledPost, platform: string, error: string): Promise<void> {
  // Update post status
  await db.scheduledPosts.update({
    where: { id: post.id },
    data: {
      status: 'failed',
      results: {
        ...post.results,
        [platform]: { platform, error },
      },
    },
  });

  // Add to dead letter queue
  await db.failedPosts.create({
    data: {
      postId: post.id,
      platform,
      error,
      originalContent: post.content,
      failedAt: new Date(),
      resolved: false,
    },
  });

  // Notify team
  await notifySlack({
    type: 'failed',
    platform,
    text: post.content.text,
    error,
  });
}

Un dashboard muestra todas las publicaciones fallidas con botones de reintento. La mayoría de los fallos son transitorios y tienen éxito al reintentar manualmente. Los fallos persistentes usualmente indican expiración de tokens o cambios en la API que requieren actualizaciones de código.

Manejo de Éxito Parcial

Una publicación dirigida a tres plataformas podría tener éxito en dos y fallar en una. El sistema necesita rastrear resultados por plataforma de forma independiente en lugar de tratar toda la publicación como fallida:

async function publishToAllPlatforms(post: ScheduledPost): Promise<void> {
  const results = await Promise.allSettled(
    post.platforms.map((platform) => publishWithRetry(post, platform))
  );

  const postResults: Record<string, PostResult> = {};
  let hasFailure = false;

  results.forEach((result, index) => {
    const platform = post.platforms[index];
    if (result.status === 'fulfilled') {
      postResults[platform] = result.value;
      if (result.value.error) hasFailure = true;
    } else {
      postResults[platform] = { platform, error: result.reason.message };
      hasFailure = true;
    }
  });

  await db.scheduledPosts.update({
    where: { id: post.id },
    data: {
      status: hasFailure ? 'failed' : 'published',
      results: postResults,
    },
  });
}

Usar Promise.allSettled en lugar de Promise.all asegura que un fallo en una plataforma no impida publicar en las demás.

Juntando Todo

El pipeline completo de automatización se ve así:

  1. El contenido se crea a través de un dashboard web o API (con generación de borradores asistida por IA opcional).
  2. Los medios se suben y procesan para cada plataforma objetivo usando Sharp.
  3. La publicación entra en una cola de aprobación donde un revisor ve vistas previas específicas por plataforma.
  4. Al aprobar, la publicación se programa en la base de datos con una marca de tiempo UTC.
  5. Un cron job (o cloud function programada) verifica publicaciones pendientes dentro del próximo minuto.
  6. Para cada publicación pendiente, el publicador se autentica con cada plataforma, maneja las cargas de medios y publica.
  7. Los resultados se registran por plataforma. Los fallos disparan reintentos con backoff exponencial.
  8. Los reintentos agotados van a la cola de mensajes fallidos con notificaciones de Slack.
  9. La analítica se recolecta a intervalos de 1h, 24h y 7d.

¿Es esto sobre-ingeniería para un fundador solo que publica dos veces por semana? Absolutamente — usa Buffer. Pero para un producto con necesidades de contenido programático, publicaciones dirigidas por eventos, o flujos de aprobación complejos, un pipeline personalizado se paga solo en consistencia y control. Las APIs de las plataformas son lo suficientemente estables y las herramientas lo suficientemente maduras como para que la implementación sea ingeniería directa en lugar de pelear contra APIs mal documentadas. Eso no era el caso hace apenas dos años.

DU

Danil Ulmashev

Full Stack Developer

Interesado en trabajar juntos?