# Zoosial — API para agentes (llms.txt) > Gateway REST multi-tenant para operar redes sociales (Meta/FB+IG, TikTok, Google/YouTube, > Telegram, WhatsApp) vía APIs oficiales + WaSender. Un agente puede publicar, gestionar > comentarios/DMs, automatizar respuestas, correr campañas, leer analíticas y enviar WhatsApp. Base URL (prod): https://zoosial.com Content-Type: application/json en requests con body. ## Autenticación y modelo de seguridad (LEER PRIMERO) Toda ruta (salvo las públicas de abajo) requiere `Authorization: Bearer `. Dos tipos: - **API key por owner** (`sk_live_…`): la credencial normal. Cada key pertenece a UN owner y **solo puede ver/operar los datos de ese owner** (profiles, cuentas, posts, etc. ajenos → 403). Puede tener `scope` (todos los profiles o unos específicos) y `permission` (`read-write` o `read` = solo GET). Es la que usa un agente en nombre de un usuario. - **API_KEY admin** (una global, del backend): acceso total, bypass del aislamiento. NO se la des a un agente de usuario; es para operación interna. Rutas públicas (sin token): `GET /health`, `GET /llms.txt`, `GET /webhooks/*` (entrantes de plataformas, validan su propia firma), páginas del sitio, y los `GET /connect/*/callback` (OAuth). Reglas de seguridad que ya aplica el sistema (no hay que reimplementarlas): - **Aislamiento por owner (anti-BOLA):** cada request se acota al owner del token; recurso ajeno → 403, inexistente → 404. - **Least-privilege:** una key `read` no puede POST/PATCH/DELETE (403); una key con scope limitado no toca profiles fuera de su set. - **Una API key NO puede gestionar API keys** (crear/borrar) — eso requiere sesión de usuario real. Evita escalada. - Errores: `{ "error": "mensaje" }` con status 400 (validación) / 401 (sin auth) / 403 (no autorizado) / 404 / 409 (conflicto) / 429 (rate-limit) / 500. ## Gestión de API keys (solo con sesión de usuario, no con sk_) - `POST /api-keys` `{ name, expiresIn?(días), scope?('full'|'profiles'), profileIds?, permission?('read-write'|'read') }` → devuelve `{ apiKey, key }`. La `key` (`sk_live_…`) se muestra **una sola vez**. - `GET /api-keys` → lista (sin la key). `DELETE /api-keys/:id` → revoca. ## Estructura de datos Owner (usuario) → **Profiles** (marcas/proyectos, ≤1 cuenta por red c/u) → **Accounts** (cuentas conectadas: FB page, IG, WhatsApp…). - `POST /profiles { name }` · `GET /profiles` → `{ profiles, plan: { status, limits: { maxProfiles, maxAccountsPerProfile } } }`. - `GET /accounts` (cuentas del owner, sin tokens) · `GET /accounts/health` (estado: connected / needs_reconnection) · `GET /adaccounts` Límites por plan (se cobra por PERFIL, no por cuenta): - Regla estructural: 1 cuenta por red social por perfil (1 Facebook, 1 Instagram, 1 YouTube, 1 Meta ads, 1 Google ads, etc.). Para otra cuenta de la misma red, crea otro perfil. - Free: 1 perfil, 3 cuentas en total por perfil. - Trial (14 días desde el registro): 1 perfil, todas las redes (1 c/u). Errores (campo "code" en la respuesta): - 409 one_per_network: ya hay una cuenta de esa red en el perfil. - 402 plan_account_limit: superas el nº de cuentas de tu plan. - 402 plan_profile_limit: superas el nº de perfiles de tu plan. - 403 profile_paused: el perfil quedó pausado (no cubierto por tu plan tras un downgrade o cancelación — el/los perfil(es) más antiguo(s) siguen activos, los demás se pausan). Bloquea publicar/conectar en ese perfil hasta que mejores el plan o liberes otro. `GET /profiles` marca cada perfil con `paused: true|false`. ## Conectar cuentas **Si estás construyendo TU PROPIA app sobre esta API (no un uso interactivo tuyo vía Zoosial), tus usuarios finales NUNCA deben pisar zoosial.com ni loguearse ahí — solo deben ver TU interfaz.** Dos requisitos para lograrlo, ambos obligatorios: 1. **`profileId` viene SIEMPRE de `POST /profiles`, nunca lo inventes.** No uses un ID interno tuyo (UUID de tu propio usuario, etc.) como `profileId` — ese profile no existirá en Zoosial y el flujo de conexión fallará (404) en cuanto llegues al picker. Crea el profile primero, guarda el `id` (`prof_…`) que te devuelve, y usa ESE en todo lo demás. 2. **Pasa `redirectUri` (pre-registrado en tu API key) + `clientState` (opaco, tuyo) en el `GET /connect/meta`.** Sin `redirectUri`, el callback de Meta cae por default a la pantalla hosted de Zoosial (`/app/connect-select`, exige sesión de Zoosial) — ese es el fallback para uso interactivo directo en zoosial.com, NO para integraciones de terceros. - Meta (FB/IG), flujo white-label completo: 1. `POST /profiles { name }` → `{ profile: { id } }` (guarda `id`, es tu `profileId`). 2. Registra tu `redirectUri` una vez en tu API key: `POST /api-keys { redirectUris:['https://tuapp.com/callback'] }` al crearla, o `PATCH /api-keys/:id { redirectUris }` después (requiere sesión de usuario, no `sk_`). 3. `GET /connect/meta?profileId=&redirectUri=https://tuapp.com/callback&clientState=lo-que-quieras` → `{ authUrl }`. Mándalo al navegador del usuario. 4. Meta redirige a Zoosial, que a su vez redirige a **tu** `redirectUri` (nunca a Zoosial) con `?profileId=...&ct=&platform=meta&clientState=`. 5. En tu propia pantalla: `GET /profiles/:id/connect/meta/available?ct=` (lista páginas, llamado con tu `sk_` key, sin sesión de Zoosial) → `POST /profiles/:id/connect/meta { ct, pageId }` (conecta UNA, ≤1 FB por profile). - Si omites `redirectUri` (uso interactivo tuyo, no de terceros), cae a `/app/connect-select` — es el comportamiento esperado SOLO en ese caso. - **TikTok y Google/YouTube (`GET /connect/tiktok`, `GET /connect/google`) TODAVÍA NO soportan `redirectUri`** — su callback siempre aterriza en `/app/dashboard` (Zoosial), sin bypass. Si tu app necesita blanco-etiquetar también estas plataformas, avisa antes de ofrecerlas a tus usuarios; hoy solo Meta tiene el flujo de terceros completo. - WhatsApp WaSender (QR): `POST /connect/wasender { profileId, phone, displayName? }` → `{ accountId, qr, status }`; polling `GET /accounts/:id/wasender/qr`. - WhatsApp Cloud API oficial: `POST /connect/whatsapp-cloud { profileId, phoneNumberId, accessToken, wabaId? }`. - Telegram: `POST /connect/telegram { profileId, botToken, chatId }`. ## Publicar - `POST /posts { platforms:[{accountId}], content?, mediaUrls?, scheduledFor?(ISO), publishNow? }` → publica ya o programa. `GET /posts` (historial del owner). - `POST /upload` (multipart) → sube media al CDN, devuelve `{ url }` para usar en `mediaUrls`. - Cola con slots recurrentes: `POST /queue/slots { profileId, weekday(0-6), time("HH:MM"), timezone(IANA) }` · `GET/DELETE /queue/slots[/:id]` · `POST /queue { platforms, content? }` (auto-agenda al próximo slot) · `GET /queue`. ## IA generativa (copy, imagen, video) - `GET /ai/models` → catálogo de modelos disponibles `{ id, kind('image'|'video'), label, creditCost, validAspectRatios[], maxReferenceImages, durationOptions? }`. Un modelo nuevo aparece acá solo (no hay que hardcodear ids en el cliente). `maxReferenceImages` es 0 si el modelo no soporta imagen de referencia; `durationOptions` solo está presente en modelos de video con duración configurable. - `POST /ai/generate-copy { profileId, mediaUrl, mediaType('image'|'video'), context? }` → `{ caption, hashtags[] }`. Gratis (rate-limit diario, no gasta crédito), síncrono (rápido). - `POST /ai/generate-image { profileId, prompt, model, aspectRatio?, imageUrls?, presetId? }` → `{ taskId, state:'generating' }`. `imageUrls` (hasta `maxReferenceImages` del modelo) usa el slug `kieModelWithImage` del modelo si existe; `presetId` es solo metadata que vuelve en el historial (no cambia el resultado). - `POST /ai/generate-video { profileId, prompt, model, imageUrls?, aspectRatio?, duration?, presetId? }` → `{ taskId, state:'generating' }`. `duration` solo aplica a modelos con `durationOptions` en `GET /ai/models`. - `GET /ai/generate/:taskId` → poll único para imagen Y video (mismo endpoint): `{ state:'generating' }` mientras corre, `{ state:'success', url, creditsUsed }` o `{ state:'fail', error }` al terminar. El crédito se cobra recién al confirmar éxito (fallos no cobran). - `GET /ai/generations?profileId?` → historial de generaciones del owner (borrado perezoso de las expiradas), `{ generations: AiGeneration[] }` con `AiGeneration = { id, ownerId, profileId, kind('image'|'video'), modelId, prompt, presetId?, mediaUrl, creditsUsed, createdAt, expiresAt }`. `profileId` es opcional — sin él devuelve el historial de todos los perfiles del owner. - **Por qué imagen y video son ASYNC por igual** (crear tarea + poll aparte, nunca una request bloqueada): medido en vivo contra kie.ai, una generación de imagen (Seedream 5.0 Pro) tardó 124s reales — más de lo que aguanta bloqueada una request HTTP detrás de cualquier proxy. - **La URL que devuelve `url` es TEMPORAL** (la sirve kie.ai, no Zoosial — no hay garantía documentada de retención más allá de una ventana corta; kie.ai ni siquiera promete persistencia de los archivos que ELLOS generan más allá de un link de descarga de 20 minutos). Zoosial NO re-sube ese archivo a su propio CDN (Bunny) — usa la URL de kie.ai tal cual, a propósito, para no duplicar infraestructura. Consecuencia práctica: publica o adjunta a un post pronto después de generar; no lo dejes programado para dentro de mucho tiempo sin volver a generarlo antes de esa fecha. - Los dos endpoints de kie.ai por generación (interno, no expuesto a los agentes de Zoosial): uno **solicita** la generación (crea la tarea) y otro **recibe** el resultado — kie.ai soporta tanto polling (lo que usa Zoosial hoy, `GET /jobs/recordInfo` o `/veo/record-info`) como un webhook `callBackUrl` firmado (HMAC-SHA256, header `X-Webhook-Signature`, secreto `webhookHmacKey` de la consola de kie.ai). Polling ya está verificado en vivo y es suficiente por ahora; el webhook queda **documentado como plan B** — si el polling se vuelve poco confiable en producción (tareas que kie.ai pierde, rate-limits del propio polling, etc.), implementar `POST /webhooks/kie` (mismo patrón que los webhooks de Meta/Stripe/WaSender ya existentes) es la salida. ## Gestión orgánica (Meta) — todas requieren una cuenta del owner - Historial: `GET /accounts/:id/history` · borrar post: `DELETE /accounts/:id/posts/:objectId` - Comentarios: `GET /accounts/:id/comments?objectId=` · `POST /accounts/:id/comments { objectId, message }` · ocultar `POST /accounts/:id/comments/hide { commentId, hide }` · borrar `DELETE /accounts/:id/comments/:commentId` - DMs: `GET /accounts/:id/conversations` · `GET /accounts/:id/conversations/:convId` · `POST /accounts/:id/messages { recipientId, text }` - WhatsApp: `POST /accounts/:id/whatsapp/send { to, text }` (enruta por proveedor wasender/cloud; maneja rate-limit) - Insights: `GET /accounts/:id/insights` · `GET /accounts/:id/post-insights?objectId=` ## Automatizaciones comment-to-DM (tipo ManyChat) - `POST /automations { accountId, platform('facebook'|'instagram'), platformPostId?, keywords?[], dmMessage, publicReply?, active? }` → cuando alguien comenta con una keyword, se le manda un DM (private reply) + respuesta pública opcional, con dedup. `GET/GET:id/PATCH/DELETE /automations[/:id]`. ## Leads (Lead Ads) - `GET /accounts/:id/lead-forms` (lista) · `GET /accounts/:id/lead-forms/:formId` (detalles + preguntas) · `GET /accounts/:id/leads?formId=…&limit=&after=&since=` (leads paginados por cursor `after`; `after` null = fin). - `POST /accounts/:id/lead-forms` `{ name, locale?, questions[], privacy_policy:{url,link_text}, context_card?, thank_you_page?, follow_up_action_url? }` → crea un formulario. Meta NO permite editarlo después. - `POST /accounts/:id/lead-forms/:formId/test-webhook` → emite un `lead.received` de MUESTRA (con field_data derivado de las preguntas reales del form) a tus webhooks. Determinístico, no toca Meta — sirve para probar tu receptor sin esperar un lead real. ## Ads (Meta + Google) - Leer: `GET /ads/campaigns|adsets|ads|audiences|insights|pixels|locations|custom-conversions` (con params de ad account) - Crear/operar: `POST /ads/campaigns|adsets|ads|adcreatives|audiences|boost|darkpost|pixel-events|custom-conversions` · `PATCH /ads/object` (editar/pausar) · `DELETE /ads/object` ## Commerce / Catálogos - `GET/POST /commerce/catalogs` · `GET/POST/DELETE /commerce/products` · `POST /commerce/products/batch` · `GET/POST /commerce/product-sets` · `GET /commerce/shops` ## Analytics para IA - `GET /analytics/:accountId/summary` → { followers, reach, impressions, engagement, topPosts, byMetric } (normalizado para consumo por IA). - `GET /analytics/:accountId/followers` → histórico diario de seguidores. ## Webhooks de SALIDA (tus endpoints reciben eventos firmados) - `POST /hooks { url(https), events[], secret? }` → devuelve el `secret` una vez. `GET/GET:id/PATCH/DELETE /hooks[/:id]` · `POST /hooks/:id/test` · `GET /hooks/:id/deliveries`. - Cada entrega lleva `X-SocialGate-Signature: sha256=` (verifícalo con tu secret), `X-SocialGate-Event`, `X-SocialGate-Delivery`. Reintentos con backoff. - Eventos: `post.scheduled|published|partial|failed`, `post.platform.failed`, `account.connected|needs_reconnection|disconnected`, `lead.received`, `message.received`, `automation.triggered`, `webhook.test`. ## Uso / facturación - `GET /usage[?period=YYYY-MM]` → `{ period, connectedAccounts, usage:{ posts_published, messages_sent, … } }`. Contabilidad por owner lista para cobro (Stripe metered, aún sin cobrar). - `POST /billing/checkout` (requiere sesión de usuario, no `sk_`) → crea/recupera el Customer de Stripe y una Checkout Session de suscripción; devuelve `{ url }` para redirigir al usuario a pagar. - `POST /billing/portal` (requiere sesión de usuario) → devuelve `{ url }` al Billing Portal de Stripe (gestionar tarjeta, cancelar, ver facturas). 400 si el owner aún no tiene `stripeCustomerId` (nunca inició un checkout). ## Cómo probar de forma segura 1. Crea un profile: `POST /profiles { name }`. 2. Conecta una cuenta (OAuth Meta o WhatsApp). 3. Publica o envía; consulta `GET /accounts/health`, `GET /usage`. Todo lo que hagas con una `sk_` key queda acotado a ESE owner: nunca verás ni tocarás datos de otro. Para no gastar de más: los envíos de WhatsApp respetan rate-limit; los webhooks de salida solo van a https.