{"openapi":"3.0.3","info":{"title":"Henry Social API","version":"1.0.0","description":"\nConecta cuentas de **Facebook, Instagram y TikTok** por OAuth sin registrar nada en los portales de Meta o\nTikTok, lee sus métricas y publicaciones, y programa y publica por ellas. Tú te quedas con tu\nproducto; el servicio se queda con los tokens, las renovaciones, las versiones de las APIs y el App Review.\n\n## Guía rápida\n\nCinco llamadas para ver las métricas de una cuenta recién conectada. `$KEY` es tu API key.\n\n```bash\n# 1. La marca (idempotente por external_ref)\ncurl -s https://publish.henry.cm/api/v1/profiles -H \"Authorization: Bearer $KEY\" \\\n  -H \"content-type: application/json\" -d '{\"name\":\"Marca A\",\"external_ref\":\"ws-7f3a\"}'\n\n# 2. Una sesión de conexión → connect_url\ncurl -s https://publish.henry.cm/api/v1/oauth/sessions -H \"Authorization: Bearer $KEY\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\"platform\":\"instagram\",\"return_url\":\"https://tuapp.com/social/listo\",\"profile\":{\"external_ref\":\"ws-7f3a\"}}'\n\n# 3. Manda al usuario a connect_url. Vuelve a tu return_url con ?hs_session=…&hs_status=ok&hs_cuentas=1\n\n# 4. Las cuentas de la marca\ncurl -s \"https://publish.henry.cm/api/v1/accounts?profile_id=<profile.id>\" -H \"Authorization: Bearer $KEY\"\n\n# 5. Sus métricas de los últimos 28 días y sus publicaciones\ncurl -s \"https://publish.henry.cm/api/v1/accounts/<account.id>/insights\" -H \"Authorization: Bearer $KEY\"\ncurl -s \"https://publish.henry.cm/api/v1/accounts/<account.id>/posts?limit=25\" -H \"Authorization: Bearer $KEY\"\n```\n\nRegistra un webhook (`POST /v1/webhooks`) y te enteras cuando una cuenta se conecta, se desconecta o su token\ndeja de servir, sin consultar.\n\n## Autenticación\n\nToda llamada a `/v1/*` lleva tu API key:\n\n```\nAuthorization: Bearer hs_live_…\n```\n\nTu tenant se deriva de la llave. Nunca mandas un `tenant_id`: no existe como parámetro.\nTodo lo que lees o escribes queda dentro de tu tenant, en la capa de consulta.\n\n## El modelo\n\n| Recurso | Qué es |\n|---|---|\n| **profile** | La unidad de aislamiento: tu marca / cliente final. Tú decides el grano. |\n| **account** | Una cuenta social conectada (página de FB, IG profesional, cuenta de TikTok). Se asigna a un profile. |\n| **grant** | Una autorización de OAuth. Un grant de Meta puede traer N páginas e IGs. No lo manejas directo; aparece en `account.grant_id`. |\n| **session** | Un flujo de conexión hospedado, de principio a fin. |\n\n**Los tokens nunca salen por la API.** Tú ves handles y `connected`; el servicio administra los tokens.\n\n## El flujo de conexión\n\n1. `POST /v1/profiles` — crea (o reutiliza, por `external_ref`) el profile de la marca.\n2. `POST /v1/oauth/sessions` con `platform`, `return_url` y el `profile` — recibes una `connect_url`.\n3. Manda al usuario a `connect_url`. Pasa por el sign-in del proveedor y, si entró por Facebook, elige qué páginas (o qué Instagrams, según `platform`) conectar.\n4. El usuario aterriza en tu `return_url` (siempre por **GET**) con `?hs_session=<id>&hs_status=ok|nada&hs_cuentas=<n>`.\n   `ok` = conectó `n` cuentas; `nada` = terminó el diálogo sin elegir ninguna.\n5. `GET /v1/oauth/sessions/{id}` — ves qué cuentas resultaron. O `GET /v1/accounts?profile_id=…`.\n\n**Si algo falla** (canceló en el proveedor, tardó más de 30 min, la cuenta no tiene páginas…) el usuario **no vuelve a tu\n`return_url`**: ve una página del servicio con el motivo y un \"cierra esta ventana\". Tú lo ves en\n`GET /v1/oauth/sessions/{id}` → `status: \"failed\" | \"expired\"` y `error`: `state_vencido`, `sin_paginas`,\n`meta:<error>:<reason>` / `instagram:…` / `tiktok:…` (lo que devolvió el proveedor; `access_denied`/`user_denied` = canceló), o `finalize`.\nNo hay evento de webhook para sesiones: si tu UI espera el regreso, dale un tiempo máximo y consulta la sesión.\n\n**`return_url` es tuya y no se registra en ningún portal.** Es un parámetro; el redirect URI de OAuth es del servicio.\nAcepta `https`, o `http` solo en `localhost`.\n\n### Instagram: dos formas de entrar\n\n| `login_method` | El usuario entra con | Requiere página de FB | Para quién |\n|---|---|---|---|\n| `instagram` (default) | su cuenta de **Instagram** | No | El dueño de la cuenta; Business y Creator |\n| `facebook` | su cuenta de **Facebook** | Sí | Agencias que administran la página del cliente sin su contraseña de IG |\n\nSon dos productos distintos de Meta con credenciales distintas; el servicio maneja los dos. Una cuenta de\nIG conectada por cualquiera de los dos caminos se ve igual en la API.\n\n### Modo agency\n\nSi omites `profile` en la sesión, el usuario conecta una vez y las cuentas quedan **sin asignar**. Después las\nrepartes con `PATCH /v1/accounts/{id}`. Sirve para que un solo login de Facebook cubra a todos los clientes de\nuna agencia: cuando ese login expira, se renueva uno y se sanan todos.\n\n## Webhooks\n\nRegistra un endpoint con `POST /v1/webhooks` y recibes un POST firmado por cada evento:\n\n| Evento | Cuándo | `data` |\n|---|---|---|\n| `account.connected` | Una cuenta quedó conectada (uno por cuenta) | `{ account, session_id }` |\n| `account.updated` | Cambió su profile (PATCH) | `{ account, changed }` |\n| `account.disconnected` | La desconectaste, el usuario quitó la app, o el token murió | `{ account, reason }` |\n| `account.health_changed` | Cambió `token_health` | `{ account, previous, current }` |\n| `job.published` | Todos los legs del job publicaron | `{ job }` |\n| `job.partial` | Algunos legs publicaron, otros fallaron | `{ job }` |\n| `job.failed` | Ningún leg publicó | `{ job }` |\n| `ping` | `POST /v1/webhooks/{id}/test` | `{ webhook_id }` |\n\nEl cuerpo es siempre `{ \"id\": \"<id de entrega>\", \"event\": \"<evento>\", \"created_at\": \"<ISO>\", \"data\": { … } }`.\n`account` dentro de `data` es el mismo objeto de la API más `profile: { id, external_ref, name }`, para que\nno tengas que hacer un lookup. `reason` en `account.disconnected`: `api` (lo pediste tú), `deauthorized_by_user`\n(el usuario quitó la app en Meta), `token_expired`, o `graph:<code>` / `instagram:<code>` / `tiktok:<code>`\n(el proveedor rechazó el token; lo detectó el barrido de salud o un refresh).\n\n**Firma.** Cada entrega lleva `X-HenrySocial-Signature: t=<unix>,v1=<hex>` donde\n`v1 = HMAC-SHA256(secret, \"<t>.<raw body>\")`. Verifica sobre el **cuerpo crudo**, no sobre\nun JSON re-serializado, y rechaza si `|ahora - t| > 300 s`. También llegan\n`X-HenrySocial-Event` y `X-HenrySocial-Delivery` (id único: deduplica por él).\n\n**Entrega.** Responde 2xx en < 10 s. Si no, se reintenta con backoff durante ~10 h. Tras 20\nentregas muertas seguidas el endpoint se desactiva; lo reactivas con `PATCH {active:true}`.\nUn evento nunca se pierde por un fallo del transporte: se registra antes de enviarse.\nRegistrar dos veces la misma URL crea dos endpoints (y dos entregas por evento): lista antes de crear.\n\n## Insights\n\nLectura **en vivo** contra Meta / TikTok, con el token de la cuenta: `GET /v1/accounts/{id}/insights` (resumen\ndel periodo) y `GET /v1/accounts/{id}/posts` (publicaciones con métricas). Lo que el proveedor no da va en\n`null`, nunca en 0. Cada llamada tarda 1–4 s: cachea de tu lado. El periodo por defecto son los últimos 28 días;\nel máximo, 90 (para más atrás, guarda lo que lees).\n\n### Métricas: qué son y de dónde salen\n\nLos nombres son los que las plataformas entregan **hoy** (Graph v26), no los de hace dos años. Meta retiró\n`impressions` en 2025 y lo que queda es `views` (la misma cuenta que muestran sus apps); ningún proveedor\npuede dar ya impresiones o alcance de un **post de página de Facebook**. Cada número viene de una sola llamada\nal proveedor; nada se estima.\n\n| Métrica | Instagram | Facebook (página) | TikTok |\n|---|---|---|---|\n| `followers` | `followers_count` | `followers_count` | `follower_count` |\n| `totals.reach` | `reach` del periodo | — | — |\n| `totals.views` | `views` (antes *impressions*) | — | — |\n| `totals.profile_views` / `accounts_engaged` / `interactions` / `website_clicks` | mismos nombres (`total_interactions` → `interactions`) | — | — |\n| `totals.likes` / `comments` / `shares` / `saves` | del periodo | — | `likes` de vida |\n| `totals.post_engagements` / `page_views` / `follows` / `video_views` / `reactions` | — | `page_post_engagements`, `page_views_total`, `page_daily_follows_unique`, `page_video_views`, `page_actions_post_reactions_total` | — |\n| `daily[]` | `reach`, `new_followers` por día | las cinco de arriba por día | — (TikTok no da series) |\n| Post: `likes` / `comments` / `shares` / `saves` | `likes`, `comments`, `shares`, `saved` | `post_reactions_by_type_total.like`, `post_activity_by_action_type.comment`, `shares.count` | `like_count`, `comment_count`, `share_count` |\n| Post: `reach` / `views` | `reach`, `views` (feed y reels) | **no existe** | `view_count` (solo `views`) |\n| Post: `reactions` / `clicks` / `video_views` | — | `post_reactions_by_type_total` (todas), `post_clicks`, `post_video_views` (de vida) | — |\n| Post: `interactions` / `duration_s` | `total_interactions` | — | `duration` |\n\nSi vienes de otro proveedor: `impressions` ↔ `views`; *engagement rate* no se entrega, calcúlalo tú con la\nfórmula que uses hoy (`(likes + comments + shares + saves) / followers` o sobre `reach`); los seguidores son el\nconteo de **hoy** — guarda un snapshot diario si quieres crecimiento.\n\n### Contenido\n\nCada post trae `permalink` (la URL pública en la red), `caption`, `type`, `published_at` y `thumbnail_url`\n(en Instagram también `media_url`). Las URLs de imagen/video son del CDN de la plataforma y **caducan en horas**;\npara un reporte guardado o una galería, descarga la imagen cuando la recibes y sirve tu copia. En Facebook,\n`thumbnail_url` es `full_picture` (la imagen principal; para álbumes, la primera). TikTok da la portada del video.\n\n## Errores\n\nSiempre `{ \"error\": \"<código>\", \"detail\"?: … }`. Ramifica por `error`, nunca por `detail`. Códigos\ntransversales: `unauthorized` (401), `invalid_id` (400, un id de la ruta no es UUID), `invalid_json` /\n`invalid_body` (400), `not_found` (404), `internal` y `*_failed` (500, reintentable).\n\n## Convenciones\n\nFechas en ISO 8601 UTC. Ids son UUID. Sin límite de tasa publicado hoy; reintenta 5xx con backoff.\nLas API keys las emite Henry por cada tenant (no hay autoservicio aún); si pierdes la tuya se revoca y se emite otra.\nLos permisos que el usuario otorga son los mínimos para conectar, leer métricas y publicar; el servicio no lee\nmensajes ni comentarios de terceros. Versionado: la ruta lleva `/v1`; un cambio incompatible sería `/v2`, y lo\nque se agrega dentro de `/v1` (campos, métricas nuevas) no rompe lo que ya lees.\n\n## Publicar\n\n`POST /v1/jobs` programa (o publica ahora) un post en una o varias cuentas de la misma marca. Tres reglas:\n\n1. **Handoff temprano.** Entregas el job al programar, con payload autocontenido (caption, URLs https de media,\n   cuentas destino). Desde ese instante el reloj es del servicio: si tu sistema se cae, el post sale igual. El\n   media se copia al bucket del servicio al despachar; tu storage deja de importar.\n2. **Cancelar siempre se confirma.** `DELETE` devuelve `confirmed` (no va a salir), `in_flight` (ya se está\n   publicando; consulta el resultado) o `already_published`. Nunca fire-and-forget.\n3. **Idempotencia por contenido.** Único por `(external_ref, content_hash)`. Repetir el mismo POST devuelve el\n   mismo job (200) en vez de crear otro (201). Si mandas tu propio `content_hash` (sha256 de tu post), úsalo\n   para que reprogramar no cambie el hash y editar sí. Si el job anterior falló o se canceló, repetir el POST lo\n   re-agenda con los legs en cero: es \"inténtalo otra vez\".\n\nCada **leg** es una cuenta destino con su `post_type` (`text` solo Facebook; `image`, `carousel` hasta 10,\n`video`, `reel`, `story`). Si no lo mandas se infiere del media; `story` hay que pedirlo. Instagram exige\nmedia; los videos de Instagram se publican como reels. Una **story** con varios media publica una story por\nelemento, **en orden** (la siguiente sale cuando la anterior ya existe); el leg devuelve `external_post_ids`.\nEn Facebook los reels van por `/video_reels` y las stories por la Page Stories API. Con `payload.settings.instagram`\nmandas portada del reel (`cover_url` o `thumb_offset_ms`), `share_to_feed` y `collaborators`. Un job termina `published` (todos), `partial` (algunos) o `failed` (ninguno), y te avisa por webhook\n`job.published` / `job.partial` / `job.failed` con el job completo en `data.job`. Si prefieres consultar,\n`GET /v1/jobs/{id}`: cada leg trae `external_post_id`, `permalink` y, si falló, `error.code` (el código\nnumérico del proveedor) y `error.message`.\n\nReintentos: una publicación que falla por red o rate limit se reintenta sola dentro del despacho; lo que Meta\nrechaza por contenido (media inválido, cuota de 100 posts/24 h de Instagram) queda `failed` con su código.\nUn job a medio publicar tras un fallo del servicio se reanuda sin duplicar: lo que ya existe en la red se adopta.\n\n### TikTok\n\nDirect Post de **video** (un archivo) o de **fotos** (1 a 10 en un post; TikTok admite hasta 35, el tope del job\nes 10). Sin texto solo ni stories. TikTok publica **asíncrono**: acepta el post, descarga el media del bucket del\nservicio (dominio verificado en TikTok), lo procesa y lo modera — normalmente en un minuto, a veces horas. El leg\nespera hasta 3 minutos; si TikTok tarda más, queda `published` con el `publish_id` como `external_post_id` y\nel servicio vuelve a emitir `job.*` cuando TikTok avisa por webhook (id público, o `failed` si la moderación\nlo tiró). Un post **SELF_ONLY** no tiene id público ni `permalink`: TikTok solo los da para posts públicos.\n\n`payload.settings.tiktok` es **obligatorio** para un leg de TikTok y `privacy_level` no tiene default: la\nguía de UX de TikTok exige que lo elija el usuario de entre las opciones que devuelve\n`GET /v1/accounts/{id}/creator-info` (consúltalo al abrir tu editor; también dice si la cuenta tiene\ncomentarios/duet/stitch apagados y si puede publicar ahora). Mientras la app no esté auditada por TikTok\n(`audited: false` en creator-info), **solo SELF_ONLY** está disponible y cualquier otro valor se fuerza a\nSELF_ONLY: avísalo en tu UI. `allow_comment`/`allow_duet`/`allow_stitch` van apagados si no los mandas\n(TikTok exige casillas apagadas por defecto); `brand_content_toggle` (partnership pagado, no puede ser\nSELF_ONLY) y `brand_organic_toggle` (promoción de la propia marca) son la declaración de contenido comercial;\n`is_aigc` etiqueta contenido generado con IA. El caption es el `title` del video (≤ 2200) o la\n`description` de las fotos (≤ 4000); `settings.tiktok.title` (≤ 90) es el encabezado opcional de las fotos.\nErrores de TikTok en `error.code` (el texto trae el código textual de TikTok): 190 token inválido ·\n7001 invalid_param · 7002 privacy_level_option_mismatch · 7003 unaudited_client_can_only_post_to_private_accounts ·\n7004 url_ownership_unverified · 7005 spam_risk_too_many_posts (límite diario de la cuenta) · 7006 usuario bloqueado ·\n7007 reached_active_user_cap (tope de la app, se reintenta) · 7008 demasiados pendientes · 7009 scope_not_authorized\n(reconectar) · 7011 rate_limit_exceeded · 7021–7024 formato/duración/fps/tamaño rechazado · 7025 internal ·\n7026/7027 TikTok no pudo descargar el media · 7028 cancelado · 7029 auth_removed · 7030/7031 spam · 7040 falta\n`privacy_level` · 7041 media inválido para TikTok · 7099 otro.\n\n## Próximamente\n\nInbox (mensajes y comentarios).\n"},"servers":[{"url":"https://publish.henry.cm/api"}],"security":[{"apiKey":[]}],"tags":[{"name":"Profiles","description":"Tus marcas / clientes finales."},{"name":"OAuth","description":"Sesiones de conexión hospedadas."},{"name":"Accounts","description":"Cuentas sociales conectadas."},{"name":"Webhooks","description":"Endpoints a los que se avisa de cada cambio."},{"name":"Insights","description":"Métricas de cuenta y de publicaciones, leídas en vivo del proveedor."},{"name":"Jobs","description":"Publicaciones: programar, consultar, re-agendar y cancelar."}],"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","bearerFormat":"hs_live_…","description":"Tu API key de tenant."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Código estable, para ramificar en código. Nunca cambia de texto."},"detail":{"description":"Contexto opcional para humanos o para depurar. Puede cambiar."}}},"Profile":{"type":"object","required":["id","name","external_ref","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"external_ref":{"type":"string","nullable":true,"description":"Tu identificador de esta marca (p. ej. el id de tu workspace). Hace idempotente la creación."},"created_at":{"type":"string","format":"date-time"}}},"Account":{"type":"object","required":["id","platform","profile_id","grant_id","platform_account_id","username","display_name","avatar_url","connected","token_health","disconnected_at","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"profile_id":{"type":"string","format":"uuid","nullable":true,"description":"`null` = sin asignar (modo agency). Asígnala con PATCH."},"grant_id":{"type":"string","format":"uuid","description":"La autorización de OAuth de la que cuelga. Varias cuentas pueden compartir un grant."},"platform_account_id":{"type":"string","description":"El id en la plataforma: page id, IG user id, TikTok open_id."},"username":{"type":"string","nullable":true},"display_name":{"type":"string","nullable":true},"avatar_url":{"type":"string","nullable":true,"description":"Foto de perfil tal como la dio el proveedor al conectar. Es una URL de su CDN y **puede caducar**: descárgala y guarda tu copia si la vas a mostrar después."},"connected":{"type":"boolean"},"token_health":{"type":"string","enum":["valid","expiring","invalid"],"description":"Lo último que se sabe. Para un chequeo vivo usa /health."},"disconnected_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"}}},"Session":{"type":"object","required":["id","platform","profile_id","status","error","return_url","accounts","expires_at","created_at","completed_at"],"properties":{"id":{"type":"string","format":"uuid"},"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"profile_id":{"type":"string","format":"uuid","nullable":true},"status":{"type":"string","enum":["pending","completed","expired","failed"]},"error":{"type":"string","nullable":true,"description":"Solo con `failed`: `state_vencido`, `sin_paginas`, `finalize`, o `meta:<error>:<reason>` / `instagram:…` / `tiktok:…` tal como lo devolvió el proveedor."},"return_url":{"type":"string","format":"uri"},"accounts":{"type":"array","items":{"type":"object","required":["id","platform","profile_id","grant_id","platform_account_id","username","display_name","avatar_url","connected","token_health","disconnected_at","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"profile_id":{"type":"string","format":"uuid","nullable":true,"description":"`null` = sin asignar (modo agency). Asígnala con PATCH."},"grant_id":{"type":"string","format":"uuid","description":"La autorización de OAuth de la que cuelga. Varias cuentas pueden compartir un grant."},"platform_account_id":{"type":"string","description":"El id en la plataforma: page id, IG user id, TikTok open_id."},"username":{"type":"string","nullable":true},"display_name":{"type":"string","nullable":true},"avatar_url":{"type":"string","nullable":true,"description":"Foto de perfil tal como la dio el proveedor al conectar. Es una URL de su CDN y **puede caducar**: descárgala y guarda tu copia si la vas a mostrar después."},"connected":{"type":"boolean"},"token_health":{"type":"string","enum":["valid","expiring","invalid"],"description":"Lo último que se sabe. Para un chequeo vivo usa /health."},"disconnected_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"}}},"description":"Las cuentas que resultaron de esta sesión (vacío hasta que se complete)."},"expires_at":{"type":"string","format":"date-time"},"created_at":{"type":"string","format":"date-time"},"completed_at":{"type":"string","format":"date-time","nullable":true}}},"Webhook":{"type":"object","required":["id","url","events","active","consecutive_failures","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string"},"nullable":true,"description":"`null` = todos."},"active":{"type":"boolean"},"consecutive_failures":{"type":"integer"},"created_at":{"type":"string","format":"date-time"}}},"Insights":{"type":"object","required":["account_id","platform","period","source","followers","totals","daily","fetched_at"],"properties":{"account_id":{"type":"string","format":"uuid"},"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"period":{"type":"object","properties":{"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date"}},"description":"Días UTC, inclusivos."},"source":{"type":"string","enum":["graph.facebook.com","graph.instagram.com","open.tiktokapis.com"],"description":"Instagram vía la página lee de graph.facebook.com; por Instagram Login, de graph.instagram.com."},"followers":{"type":"integer","nullable":true,"description":"Seguidores HOY (dato de vida, no del periodo)."},"totals":{"type":"object","additionalProperties":{"type":"integer","nullable":true},"description":"Totales del periodo. `null` = el proveedor no lo dio. **instagram**: `reach`, `views` (antes impressions), `profile_views`, `accounts_engaged`, `interactions`, `likes`, `comments`, `shares`, `saves`, `website_clicks`. **facebook** (página): `post_engagements`, `page_views`, `follows` (nuevos), `video_views`, `reactions`. **tiktok** (de vida, no del periodo): `likes`, `videos`, `following`."},"daily":{"type":"array","items":{"type":"object","required":["date"],"properties":{"date":{"type":"string","format":"date","description":"Día según el corte del proveedor (Meta: medianoche del Pacífico)."}},"additionalProperties":{"type":"number"},"description":"`date` + una llave numérica por métrica (las mismas llaves que `totals`, cuando el proveedor da serie diaria)."},"description":"instagram: `reach`, `new_followers`. facebook: `post_engagements`, `page_views`, `follows`, `video_views`, `reactions`. tiktok: vacío."},"fetched_at":{"type":"string","format":"date-time"}}},"Post":{"type":"object","required":["id","platform","type","permalink","caption","published_at","thumbnail_url","media_url","metrics"],"properties":{"id":{"type":"string","description":"Id en la plataforma: media id de IG, post id de FB (`<page>_<post>`), video id de TikTok."},"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"type":{"type":"string","enum":["image","carousel","video","reel","text","link","unknown"]},"permalink":{"type":"string","nullable":true},"caption":{"type":"string","nullable":true},"published_at":{"type":"string","format":"date-time"},"thumbnail_url":{"type":"string","nullable":true,"description":"Imagen de vista previa (para video/reel, su portada). Es una URL del CDN del proveedor y **caduca en horas**: si la vas a mostrar después, descárgala al recibirla y guarda tu copia (ver *Contenido*)."},"media_url":{"type":"string","nullable":true,"description":"El archivo tal cual (imagen o video). Solo Instagram; misma caducidad."},"metrics":{"type":"object","additionalProperties":{"type":"integer","nullable":true},"description":"`null` = el proveedor no lo dio. **instagram**: `likes`, `comments`, `shares`, `saves`, `reach`, `views`, `interactions`. **facebook**: `reactions`, `likes`, `comments`, `shares`, `clicks`, `video_views`. Un post de página no tiene `reach` ni `views`: Meta las retiró de la API (ver *Métricas*). **tiktok**: `views`, `likes`, `comments`, `shares`, `duration_s`."}}},"Job":{"type":"object","required":["id","external_ref","profile_id","status","scheduled_at","content_hash","payload","legs","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"external_ref":{"type":"string","description":"Tu id del post (p. ej. content_posts.id)."},"profile_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["scheduled","dispatching","published","partial","failed","canceled"],"description":"`partial` = algunos legs publicaron y otros fallaron."},"scheduled_at":{"type":"string","format":"date-time"},"content_hash":{"type":"string","description":"sha256 del contenido normalizado. Con `external_ref` hace único al job."},"payload":{"type":"object","properties":{"caption":{"type":"string"},"media":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"type":{"type":"string","enum":["image","video"]}}}},"link":{"type":"string","format":"uri"},"settings":{"type":"object"}}},"legs":{"type":"array","items":{"type":"object","required":["id","account_id","platform","post_type","status","external_post_id","permalink","attempts","error","published_at"],"properties":{"id":{"type":"string","format":"uuid"},"account_id":{"type":"string","format":"uuid"},"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"post_type":{"type":"string","enum":["text","image","carousel","video","reel","story"]},"status":{"type":"string","enum":["pending","in_flight","published","failed","canceled"]},"external_post_id":{"type":"string","nullable":true,"description":"Id del post en la red (media id de IG, post id de FB). En stories, la última publicada."},"external_post_ids":{"type":"array","items":{"type":"string"},"description":"Solo stories: una por media, en el orden del post."},"permalink":{"type":"string","nullable":true,"description":"URL pública del post. Puede llegar `null` si la red aún no la expone (video recién subido)."},"attempts":{"type":"integer"},"error":{"type":"object","nullable":true,"properties":{"code":{"type":"integer","description":"Código numérico del proveedor (Meta) o negativo si es del servicio. Ramifica por esto."},"message":{"type":"string","description":"Texto del proveedor, puede venir en cualquier idioma."}}},"started_at":{"type":"string","format":"date-time","nullable":true},"published_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time"}}}},"created_at":{"type":"string","format":"date-time"},"dispatched_at":{"type":"string","format":"date-time","nullable":true},"completed_at":{"type":"string","format":"date-time","nullable":true},"canceled_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time"}}},"JobLeg":{"type":"object","required":["id","account_id","platform","post_type","status","external_post_id","permalink","attempts","error","published_at"],"properties":{"id":{"type":"string","format":"uuid"},"account_id":{"type":"string","format":"uuid"},"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"post_type":{"type":"string","enum":["text","image","carousel","video","reel","story"]},"status":{"type":"string","enum":["pending","in_flight","published","failed","canceled"]},"external_post_id":{"type":"string","nullable":true,"description":"Id del post en la red (media id de IG, post id de FB). En stories, la última publicada."},"external_post_ids":{"type":"array","items":{"type":"string"},"description":"Solo stories: una por media, en el orden del post."},"permalink":{"type":"string","nullable":true,"description":"URL pública del post. Puede llegar `null` si la red aún no la expone (video recién subido)."},"attempts":{"type":"integer"},"error":{"type":"object","nullable":true,"properties":{"code":{"type":"integer","description":"Código numérico del proveedor (Meta) o negativo si es del servicio. Ramifica por esto."},"message":{"type":"string","description":"Texto del proveedor, puede venir en cualquier idioma."}}},"started_at":{"type":"string","format":"date-time","nullable":true},"published_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time"}}}}},"paths":{"/v1/profiles":{"post":{"tags":["Profiles"],"summary":"Crear un profile","description":"Con `external_ref` es idempotente: repetir devuelve el mismo profile (200) en vez de duplicarlo. Sin él, crea uno nuevo cada vez (201).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":200},"external_ref":{"type":"string","maxLength":200,"description":"Tu id de esta marca (p. ej. el id de tu workspace)."}}},"example":{"name":"Marca A","external_ref":"ws-7f3a…"}}}},"responses":{"200":{"description":"Ya existía (por `external_ref`).","content":{"application/json":{"schema":{"type":"object","properties":{"profile":{"type":"object","required":["id","name","external_ref","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"external_ref":{"type":"string","nullable":true,"description":"Tu identificador de esta marca (p. ej. el id de tu workspace). Hace idempotente la creación."},"created_at":{"type":"string","format":"date-time"}}}}}}}},"201":{"description":"Creado.","content":{"application/json":{"schema":{"type":"object","properties":{"profile":{"type":"object","required":["id","name","external_ref","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"external_ref":{"type":"string","nullable":true,"description":"Tu identificador de esta marca (p. ej. el id de tu workspace). Hace idempotente la creación."},"created_at":{"type":"string","format":"date-time"}}}}}}}},"400":{"description":"`invalid_body` / `invalid_json`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Profiles"],"summary":"Listar profiles","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"profiles":{"type":"array","items":{"type":"object","required":["id","name","external_ref","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"external_ref":{"type":"string","nullable":true,"description":"Tu identificador de esta marca (p. ej. el id de tu workspace). Hace idempotente la creación."},"created_at":{"type":"string","format":"date-time"}}}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/profiles/{id}":{"get":{"tags":["Profiles"],"summary":"Ver un profile con sus cuentas","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Id del profile"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"profile":{"type":"object","required":["id","name","external_ref","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"external_ref":{"type":"string","nullable":true,"description":"Tu identificador de esta marca (p. ej. el id de tu workspace). Hace idempotente la creación."},"created_at":{"type":"string","format":"date-time"}}},"accounts":{"type":"array","items":{"type":"object","required":["id","platform","profile_id","grant_id","platform_account_id","username","display_name","avatar_url","connected","token_health","disconnected_at","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"profile_id":{"type":"string","format":"uuid","nullable":true,"description":"`null` = sin asignar (modo agency). Asígnala con PATCH."},"grant_id":{"type":"string","format":"uuid","description":"La autorización de OAuth de la que cuelga. Varias cuentas pueden compartir un grant."},"platform_account_id":{"type":"string","description":"El id en la plataforma: page id, IG user id, TikTok open_id."},"username":{"type":"string","nullable":true},"display_name":{"type":"string","nullable":true},"avatar_url":{"type":"string","nullable":true,"description":"Foto de perfil tal como la dio el proveedor al conectar. Es una URL de su CDN y **puede caducar**: descárgala y guarda tu copia si la vas a mostrar después."},"connected":{"type":"boolean"},"token_health":{"type":"string","enum":["valid","expiring","invalid"],"description":"Lo último que se sabe. Para un chequeo vivo usa /health."},"disconnected_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"}}}}}}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de tu tenant. No se distingue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Profiles"],"summary":"Borrar un profile","description":"**No borra sus cuentas**: quedan sin asignar. Se rechaza (409) si el profile tiene jobs programados.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Id del profile"}],"responses":{"200":{"description":"Borrado.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"},"id":{"type":"string"}}}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de tu tenant. No se distingue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`profile_has_scheduled_jobs` — cancela o espera esos jobs primero.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/oauth/sessions":{"post":{"tags":["OAuth"],"summary":"Iniciar una conexión","description":"Devuelve la `connect_url` a la que mandas al usuario. Expira en 30 minutos. `facebook` abre el diálogo de Facebook y luego un selector de páginas: conecta **solo páginas** (el Instagram vinculado se conecta con `platform: \"instagram\"`, que tiene su propio flujo). `instagram` abre Instagram Login (instagram.com) o, con `login_method: \"facebook\"`, el diálogo de Facebook con un selector solo de Instagrams. `tiktok` abre TikTok.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["platform","return_url"],"properties":{"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"login_method":{"type":"string","enum":["instagram","facebook"],"description":"Solo para instagram. Default `instagram` (instagram.com, sin página de FB). `facebook` = vía la página, con selector."},"return_url":{"type":"string","format":"uri","description":"A dónde vuelve el usuario al terminar. https (o http en localhost). Recibe `?hs_session=&hs_status=`."},"profile":{"type":"object","description":"Omítelo para modo agency (cuentas sin asignar). Crea el profile si no existe (idempotente por `external_ref`).","properties":{"external_ref":{"type":"string"},"name":{"type":"string","description":"Nombre del profile si se crea. (`nombre` sigue aceptándose como alias.)"}},"required":["external_ref"]}}},"example":{"platform":"instagram","return_url":"https://app.tuherramienta.com/cuentas?connected=1","profile":{"external_ref":"ws-7f3a…","name":"Marca A"}}}}},"responses":{"201":{"description":"Sesión creada.","content":{"application/json":{"schema":{"type":"object","properties":{"session_id":{"type":"string","format":"uuid"},"connect_url":{"type":"string","format":"uri"},"expires_at":{"type":"string","format":"date-time"}}},"example":{"session_id":"248dc168-…","connect_url":"https://publish.henry.cm/connect/start?s=…","expires_at":"2026-09-12T01:08:36Z"}}}},"400":{"description":"`invalid_body` / `invalid_return_url`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/oauth/sessions/{id}":{"get":{"tags":["OAuth"],"summary":"Ver una sesión y las cuentas que resultaron","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Id de la sesión (`hs_session` en tu return_url)"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"session":{"type":"object","required":["id","platform","profile_id","status","error","return_url","accounts","expires_at","created_at","completed_at"],"properties":{"id":{"type":"string","format":"uuid"},"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"profile_id":{"type":"string","format":"uuid","nullable":true},"status":{"type":"string","enum":["pending","completed","expired","failed"]},"error":{"type":"string","nullable":true,"description":"Solo con `failed`: `state_vencido`, `sin_paginas`, `finalize`, o `meta:<error>:<reason>` / `instagram:…` / `tiktok:…` tal como lo devolvió el proveedor."},"return_url":{"type":"string","format":"uri"},"accounts":{"type":"array","items":{"type":"object","required":["id","platform","profile_id","grant_id","platform_account_id","username","display_name","avatar_url","connected","token_health","disconnected_at","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"profile_id":{"type":"string","format":"uuid","nullable":true,"description":"`null` = sin asignar (modo agency). Asígnala con PATCH."},"grant_id":{"type":"string","format":"uuid","description":"La autorización de OAuth de la que cuelga. Varias cuentas pueden compartir un grant."},"platform_account_id":{"type":"string","description":"El id en la plataforma: page id, IG user id, TikTok open_id."},"username":{"type":"string","nullable":true},"display_name":{"type":"string","nullable":true},"avatar_url":{"type":"string","nullable":true,"description":"Foto de perfil tal como la dio el proveedor al conectar. Es una URL de su CDN y **puede caducar**: descárgala y guarda tu copia si la vas a mostrar después."},"connected":{"type":"boolean"},"token_health":{"type":"string","enum":["valid","expiring","invalid"],"description":"Lo último que se sabe. Para un chequeo vivo usa /health."},"disconnected_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"}}},"description":"Las cuentas que resultaron de esta sesión (vacío hasta que se complete)."},"expires_at":{"type":"string","format":"date-time"},"created_at":{"type":"string","format":"date-time"},"completed_at":{"type":"string","format":"date-time","nullable":true}}}}}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de tu tenant. No se distingue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/accounts":{"get":{"tags":["Accounts"],"summary":"Listar cuentas","description":"Sin filtros devuelve todas las del tenant, incluidas las sin asignar. `profile_id=unassigned` filtra solo ésas. Sin paginación: viene la lista completa.","parameters":[{"name":"profile_id","in":"query","schema":{"type":"string"},"description":"UUID de un profile, o `unassigned`."},{"name":"platform","in":"query","schema":{"type":"string","enum":["facebook","instagram","tiktok"]}},{"name":"connected","in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"accounts":{"type":"array","items":{"type":"object","required":["id","platform","profile_id","grant_id","platform_account_id","username","display_name","avatar_url","connected","token_health","disconnected_at","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"profile_id":{"type":"string","format":"uuid","nullable":true,"description":"`null` = sin asignar (modo agency). Asígnala con PATCH."},"grant_id":{"type":"string","format":"uuid","description":"La autorización de OAuth de la que cuelga. Varias cuentas pueden compartir un grant."},"platform_account_id":{"type":"string","description":"El id en la plataforma: page id, IG user id, TikTok open_id."},"username":{"type":"string","nullable":true},"display_name":{"type":"string","nullable":true},"avatar_url":{"type":"string","nullable":true,"description":"Foto de perfil tal como la dio el proveedor al conectar. Es una URL de su CDN y **puede caducar**: descárgala y guarda tu copia si la vas a mostrar después."},"connected":{"type":"boolean"},"token_health":{"type":"string","enum":["valid","expiring","invalid"],"description":"Lo último que se sabe. Para un chequeo vivo usa /health."},"disconnected_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"}}}},"total":{"type":"integer"}}}}}},"400":{"description":"`invalid_platform`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/accounts/{id}":{"get":{"tags":["Accounts"],"summary":"Ver una cuenta","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Id de la cuenta"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"account":{"type":"object","required":["id","platform","profile_id","grant_id","platform_account_id","username","display_name","avatar_url","connected","token_health","disconnected_at","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"profile_id":{"type":"string","format":"uuid","nullable":true,"description":"`null` = sin asignar (modo agency). Asígnala con PATCH."},"grant_id":{"type":"string","format":"uuid","description":"La autorización de OAuth de la que cuelga. Varias cuentas pueden compartir un grant."},"platform_account_id":{"type":"string","description":"El id en la plataforma: page id, IG user id, TikTok open_id."},"username":{"type":"string","nullable":true},"display_name":{"type":"string","nullable":true},"avatar_url":{"type":"string","nullable":true,"description":"Foto de perfil tal como la dio el proveedor al conectar. Es una URL de su CDN y **puede caducar**: descárgala y guarda tu copia si la vas a mostrar después."},"connected":{"type":"boolean"},"token_health":{"type":"string","enum":["valid","expiring","invalid"],"description":"Lo último que se sabe. Para un chequeo vivo usa /health."},"disconnected_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"}}}}}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de tu tenant. No se distingue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Accounts"],"summary":"Asignar la cuenta a un profile","description":"Manda `profile_id` (UUID, o `null` para desasignar) o `profile_external_ref` (tu id). El profile debe ser de tu tenant.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Id de la cuenta"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"profile_id":{"type":"string","format":"uuid","nullable":true},"profile_external_ref":{"type":"string"}}},"example":{"profile_external_ref":"ws-7f3a…"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"account":{"type":"object","required":["id","platform","profile_id","grant_id","platform_account_id","username","display_name","avatar_url","connected","token_health","disconnected_at","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"profile_id":{"type":"string","format":"uuid","nullable":true,"description":"`null` = sin asignar (modo agency). Asígnala con PATCH."},"grant_id":{"type":"string","format":"uuid","description":"La autorización de OAuth de la que cuelga. Varias cuentas pueden compartir un grant."},"platform_account_id":{"type":"string","description":"El id en la plataforma: page id, IG user id, TikTok open_id."},"username":{"type":"string","nullable":true},"display_name":{"type":"string","nullable":true},"avatar_url":{"type":"string","nullable":true,"description":"Foto de perfil tal como la dio el proveedor al conectar. Es una URL de su CDN y **puede caducar**: descárgala y guarda tu copia si la vas a mostrar después."},"connected":{"type":"boolean"},"token_health":{"type":"string","enum":["valid","expiring","invalid"],"description":"Lo último que se sabe. Para un chequeo vivo usa /health."},"disconnected_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"}}}}}}}},"400":{"description":"`invalid_body`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` (la cuenta) o `profile_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Accounts"],"summary":"Desconectar una cuenta","description":"**Nunca borra la fila.** La cuenta queda `connected: false`, su token se elimina y el id sigue existiendo para que puedas mostrar \"reconecta esta cuenta\". Idempotente. Se rechaza (409) si tiene publicaciones en vuelo.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Id de la cuenta"}],"responses":{"200":{"description":"Desconectada (o ya lo estaba).","content":{"application/json":{"schema":{"type":"object","properties":{"account":{"type":"object","required":["id","platform","profile_id","grant_id","platform_account_id","username","display_name","avatar_url","connected","token_health","disconnected_at","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"profile_id":{"type":"string","format":"uuid","nullable":true,"description":"`null` = sin asignar (modo agency). Asígnala con PATCH."},"grant_id":{"type":"string","format":"uuid","description":"La autorización de OAuth de la que cuelga. Varias cuentas pueden compartir un grant."},"platform_account_id":{"type":"string","description":"El id en la plataforma: page id, IG user id, TikTok open_id."},"username":{"type":"string","nullable":true},"display_name":{"type":"string","nullable":true},"avatar_url":{"type":"string","nullable":true,"description":"Foto de perfil tal como la dio el proveedor al conectar. Es una URL de su CDN y **puede caducar**: descárgala y guarda tu copia si la vas a mostrar después."},"connected":{"type":"boolean"},"token_health":{"type":"string","enum":["valid","expiring","invalid"],"description":"Lo último que se sabe. Para un chequeo vivo usa /health."},"disconnected_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"}}}}}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de tu tenant. No se distingue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`account_has_pending_legs`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/webhooks":{"post":{"tags":["Webhooks"],"summary":"Registrar un endpoint","description":"El `secret` (`whsec_…`) se devuelve **una sola vez**. Sin `events`, recibe todos. No es idempotente por URL.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"https (o http en localhost)."},"events":{"type":"array","items":{"type":"string","enum":["ping","account.connected","account.updated","account.disconnected","account.health_changed"]}}}},"example":{"url":"https://app.tuherramienta.com/webhooks/henry-social"}}}},"responses":{"201":{"description":"Creado.","content":{"application/json":{"schema":{"type":"object","properties":{"webhook":{"$ref":"#/components/schemas/Webhook"},"secret":{"type":"string","description":"Guárdalo ahora."}}}}}},"400":{"description":"`invalid_body`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Webhooks"],"summary":"Listar endpoints","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}}}}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/webhooks/{id}":{"get":{"tags":["Webhooks"],"summary":"Ver un endpoint y sus últimas 20 entregas","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Id del endpoint"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"webhook":{"$ref":"#/components/schemas/Webhook"},"recent_deliveries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"event":{"type":"string"},"status":{"type":"string","enum":["pending","delivered","dead"]},"attempts":{"type":"integer"},"last_http_status":{"type":"integer","nullable":true},"last_error":{"type":"string","nullable":true},"created_at":{"type":"string"},"delivered_at":{"type":"string","nullable":true}}}}}}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de tu tenant. No se distingue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Webhooks"],"summary":"Cambiar URL o eventos, o reactivar","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Id del endpoint"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string"},"nullable":true},"active":{"type":"boolean"}}},"example":{"active":true}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"webhook":{"$ref":"#/components/schemas/Webhook"}}}}}},"400":{"description":"`invalid_body`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de tu tenant. No se distingue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Webhooks"],"summary":"Borrar un endpoint","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Id del endpoint"}],"responses":{"200":{"description":"Borrado.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"},"id":{"type":"string"}}}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de tu tenant. No se distingue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/webhooks/{id}/test":{"post":{"tags":["Webhooks"],"summary":"Mandar un ping","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Id del endpoint"}],"description":"Encola una entrega `ping`. Revisa el resultado en `GET /v1/webhooks/{id}`.","responses":{"202":{"description":"Encolado.","content":{"application/json":{"schema":{"type":"object","properties":{"delivery_id":{"type":"string"},"dispatched":{"type":"boolean"}}}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de tu tenant. No se distingue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`webhook_inactive`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/accounts/{id}/health":{"get":{"tags":["Accounts"],"summary":"Chequeo vivo del token","description":"Le pregunta al proveedor, no a la base. Si el token murió, lo anota en `token_health` para que la cuenta deje de parecer sana antes de que una publicación truene.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Id de la cuenta"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["healthy","unhealthy","disconnected"]},"token_valid":{"type":"boolean"},"issues":{"type":"array","items":{"type":"string"},"description":"`disconnected`, `no_token`, `token_expired`, `token_invalid`, `permissions`, `provider_unreachable`, `graph_<code>`, `tiktok_<code>`."},"checked_at":{"type":"string","format":"date-time"}}}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de tu tenant. No se distingue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/accounts/{id}/creator-info":{"get":{"tags":["Accounts"],"summary":"TikTok: lo que exige la guía de UX antes de publicar","description":"Solo cuentas de TikTok. Opciones de privacidad de ESA cuenta (tu selector no debe traer valor por defecto), si tiene comentarios/duet/stitch apagados (deshabilita esas casillas), duración máxima de video, y si puede publicar ahora. Con `audited: false` solo hay SELF_ONLY. Consúltalo al abrir el editor, no lo caches más de unos minutos.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Id de la cuenta"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"account_id":{"type":"string"},"creator":{"type":"object","properties":{"username":{"type":"string","nullable":true},"nickname":{"type":"string","nullable":true},"avatar_url":{"type":"string","nullable":true,"description":"Vence en 2 horas."}}},"privacy_level_options":{"type":"array","items":{"type":"string"},"description":"Lo que puedes ofrecer. Sin auditoría: solo SELF_ONLY."},"privacy_level_options_all":{"type":"array","items":{"type":"string"},"description":"Lo que TikTok devolvió, sin filtrar."},"comment_disabled":{"type":"boolean"},"duet_disabled":{"type":"boolean"},"stitch_disabled":{"type":"boolean"},"max_video_post_duration_sec":{"type":"integer","nullable":true},"audited":{"type":"boolean","description":"false = la app aún no pasa la auditoría de Content Posting: los posts salen privados."},"can_post":{"type":"boolean","description":"false = TikTok no deja publicar ahora (límite diario, cuenta bloqueada, tope de creadores de la app)."},"block":{"type":"object","nullable":true,"properties":{"code":{"type":"integer"},"tiktok_code":{"type":"string"},"message":{"type":"string"}}},"fetched_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"`platform_not_supported` — no es TikTok.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`provider_permissions` — la cuenta se conectó sin `video.publish`; reconéctala.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de tu tenant. No se distingue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`account_disconnected` / `token_invalid`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"`provider_error`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/accounts/{id}/insights":{"get":{"tags":["Insights"],"summary":"Resumen de la cuenta en un periodo","description":"Seguidores de hoy, totales del periodo y serie diaria (donde el proveedor la da). En vivo: 1–4 s. Default: últimos 28 días; tope 90.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Id de la cuenta"},{"name":"from","in":"query","schema":{"type":"string","format":"date"},"description":"YYYY-MM-DD (UTC). Default: `to` − 27 días."},{"name":"to","in":"query","schema":{"type":"string","format":"date"},"description":"YYYY-MM-DD (UTC). Default: hoy."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Insights"}}}},"400":{"description":"`invalid_period`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`provider_permissions` — el token no trae el permiso (p. ej. la app aún sin acceso avanzado).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de tu tenant. No se distingue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`account_disconnected` / `token_invalid` — reconecta la cuenta.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"`provider_error` — `detail.code` trae el código del proveedor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/accounts/{id}/posts":{"get":{"tags":["Insights"],"summary":"Publicaciones con métricas","description":"Del más reciente al más viejo, dentro del periodo. `cursor` = el `next_cursor` de la página anterior. TikTok no filtra por fecha del lado del proveedor: se pagina hasta salir del periodo (máx. 100 videos).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Id de la cuenta"},{"name":"from","in":"query","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","schema":{"type":"string","format":"date"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"cursor","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"account_id":{"type":"string","format":"uuid"},"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"period":{"type":"object","properties":{"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date"}}},"source":{"type":"string"},"posts":{"type":"array","items":{"$ref":"#/components/schemas/Post"}},"next_cursor":{"type":"string","nullable":true},"fetched_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"`invalid_period` / `invalid_limit`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`provider_permissions`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de tu tenant. No se distingue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`account_disconnected` / `token_invalid`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"`provider_error`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/jobs":{"post":{"tags":["Jobs"],"summary":"Programar o publicar ahora","description":"Sin `scheduled_at` publica ahora. Idempotente por `(external_ref, content_hash)`: repetir devuelve el mismo job (200). Máximo 90 días adelante, 20 legs, 10 media.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["external_ref","profile_id","legs","payload"],"properties":{"external_ref":{"type":"string","maxLength":200},"profile_id":{"type":"string","format":"uuid"},"scheduled_at":{"type":"string","format":"date-time","description":"Omitir = ahora. Un pasado reciente se trata como ahora."},"content_hash":{"type":"string","pattern":"^[a-f0-9]{64}$","description":"Opcional: tu sha256 del contenido. Si falta, se calcula sobre caption + media + legs."},"legs":{"type":"array","minItems":1,"maxItems":20,"items":{"type":"object","required":["account_id"],"properties":{"account_id":{"type":"string","format":"uuid"},"post_type":{"type":"string","enum":["text","image","carousel","video","reel","story"],"description":"Opcional; se infiere del media (`story` hay que pedirlo)."}}}},"payload":{"type":"object","required":["caption"],"properties":{"caption":{"type":"string","maxLength":5000},"media":{"type":"array","maxItems":10,"items":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"https pública. Se copia al bucket del servicio al despachar."},"type":{"type":"string","enum":["image","video"]}}}},"link":{"type":"string","format":"uri","description":"Solo Facebook, post de enlace."},"settings":{"type":"object","description":"Ajustes por red. Cambiarlos cambia el content_hash.","properties":{"instagram":{"type":"object","properties":{"share_to_feed":{"type":"boolean","description":"Reels: también en el feed (default de Instagram: sí)."},"cover_url":{"type":"string","format":"uri","description":"Reels: portada (JPEG https pública)."},"thumb_offset_ms":{"type":"integer","minimum":0,"description":"Reels: fotograma para la portada. Se ignora si hay cover_url."},"collaborators":{"type":"array","maxItems":3,"items":{"type":"string"},"description":"Feed y reels: usernames de colaboradores, sin @. Ellos deben aceptar."}}},"tiktok":{"type":"object","required":["privacy_level"],"description":"Obligatorio para legs de TikTok. Ver la sección TikTok arriba.","properties":{"privacy_level":{"type":"string","enum":["PUBLIC_TO_EVERYONE","MUTUAL_FOLLOW_FRIENDS","FOLLOWER_OF_CREATOR","SELF_ONLY"],"description":"Sin default: lo elige el usuario de `creator-info.privacy_level_options`."},"allow_comment":{"type":"boolean","description":"Default false."},"allow_duet":{"type":"boolean","description":"Solo video. Default false."},"allow_stitch":{"type":"boolean","description":"Solo video. Default false."},"brand_content_toggle":{"type":"boolean","description":"Partnership pagado (\"Paid partnership\"). No puede ir con SELF_ONLY."},"brand_organic_toggle":{"type":"boolean","description":"Promoción de la propia marca (\"Promotional content\")."},"is_aigc":{"type":"boolean","description":"Etiqueta de contenido generado con IA."},"video_cover_timestamp_ms":{"type":"integer","minimum":0,"description":"Video: fotograma para la portada."},"title":{"type":"string","maxLength":90,"description":"Fotos: encabezado. El caption va como description."},"photo_cover_index":{"type":"integer","minimum":0,"description":"Fotos: índice de la portada (default 0)."},"auto_add_music":{"type":"boolean","description":"Fotos: que TikTok agregue música."}}}}}}}}},"example":{"external_ref":"post-8f1c…","profile_id":"…","scheduled_at":"2026-09-20T15:00:00Z","legs":[{"account_id":"…"},{"account_id":"…"}],"payload":{"caption":"Conoce al equipo ✨","media":[{"url":"https://media.tuapp.com/posts/equipo.jpg"}]}}}}},"responses":{"200":{"description":"Ya existía con el mismo contenido (o se re-agendó uno fallido/cancelado).","content":{"application/json":{"schema":{"type":"object","properties":{"job":{"type":"object","required":["id","external_ref","profile_id","status","scheduled_at","content_hash","payload","legs","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"external_ref":{"type":"string","description":"Tu id del post (p. ej. content_posts.id)."},"profile_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["scheduled","dispatching","published","partial","failed","canceled"],"description":"`partial` = algunos legs publicaron y otros fallaron."},"scheduled_at":{"type":"string","format":"date-time"},"content_hash":{"type":"string","description":"sha256 del contenido normalizado. Con `external_ref` hace único al job."},"payload":{"type":"object","properties":{"caption":{"type":"string"},"media":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"type":{"type":"string","enum":["image","video"]}}}},"link":{"type":"string","format":"uri"},"settings":{"type":"object"}}},"legs":{"type":"array","items":{"type":"object","required":["id","account_id","platform","post_type","status","external_post_id","permalink","attempts","error","published_at"],"properties":{"id":{"type":"string","format":"uuid"},"account_id":{"type":"string","format":"uuid"},"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"post_type":{"type":"string","enum":["text","image","carousel","video","reel","story"]},"status":{"type":"string","enum":["pending","in_flight","published","failed","canceled"]},"external_post_id":{"type":"string","nullable":true,"description":"Id del post en la red (media id de IG, post id de FB). En stories, la última publicada."},"external_post_ids":{"type":"array","items":{"type":"string"},"description":"Solo stories: una por media, en el orden del post."},"permalink":{"type":"string","nullable":true,"description":"URL pública del post. Puede llegar `null` si la red aún no la expone (video recién subido)."},"attempts":{"type":"integer"},"error":{"type":"object","nullable":true,"properties":{"code":{"type":"integer","description":"Código numérico del proveedor (Meta) o negativo si es del servicio. Ramifica por esto."},"message":{"type":"string","description":"Texto del proveedor, puede venir en cualquier idioma."}}},"started_at":{"type":"string","format":"date-time","nullable":true},"published_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time"}}}},"created_at":{"type":"string","format":"date-time"},"dispatched_at":{"type":"string","format":"date-time","nullable":true},"completed_at":{"type":"string","format":"date-time","nullable":true},"canceled_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time"}}}}}}}},"201":{"description":"Creado y programado.","content":{"application/json":{"schema":{"type":"object","properties":{"job":{"type":"object","required":["id","external_ref","profile_id","status","scheduled_at","content_hash","payload","legs","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"external_ref":{"type":"string","description":"Tu id del post (p. ej. content_posts.id)."},"profile_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["scheduled","dispatching","published","partial","failed","canceled"],"description":"`partial` = algunos legs publicaron y otros fallaron."},"scheduled_at":{"type":"string","format":"date-time"},"content_hash":{"type":"string","description":"sha256 del contenido normalizado. Con `external_ref` hace único al job."},"payload":{"type":"object","properties":{"caption":{"type":"string"},"media":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"type":{"type":"string","enum":["image","video"]}}}},"link":{"type":"string","format":"uri"},"settings":{"type":"object"}}},"legs":{"type":"array","items":{"type":"object","required":["id","account_id","platform","post_type","status","external_post_id","permalink","attempts","error","published_at"],"properties":{"id":{"type":"string","format":"uuid"},"account_id":{"type":"string","format":"uuid"},"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"post_type":{"type":"string","enum":["text","image","carousel","video","reel","story"]},"status":{"type":"string","enum":["pending","in_flight","published","failed","canceled"]},"external_post_id":{"type":"string","nullable":true,"description":"Id del post en la red (media id de IG, post id de FB). En stories, la última publicada."},"external_post_ids":{"type":"array","items":{"type":"string"},"description":"Solo stories: una por media, en el orden del post."},"permalink":{"type":"string","nullable":true,"description":"URL pública del post. Puede llegar `null` si la red aún no la expone (video recién subido)."},"attempts":{"type":"integer"},"error":{"type":"object","nullable":true,"properties":{"code":{"type":"integer","description":"Código numérico del proveedor (Meta) o negativo si es del servicio. Ramifica por esto."},"message":{"type":"string","description":"Texto del proveedor, puede venir en cualquier idioma."}}},"started_at":{"type":"string","format":"date-time","nullable":true},"published_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time"}}}},"created_at":{"type":"string","format":"date-time"},"dispatched_at":{"type":"string","format":"date-time","nullable":true},"completed_at":{"type":"string","format":"date-time","nullable":true},"canceled_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time"}}}}}}}},"400":{"description":"`invalid_body` / `account_not_in_profile` / `platform_not_supported`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — el profile o una cuenta no existe (o no es tuya).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`account_disconnected`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Jobs"],"summary":"Listar jobs","parameters":[{"name":"profile_id","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"external_ref","in":"query","schema":{"type":"string"}},{"name":"status","in":"query","schema":{"type":"string"},"description":"Uno o varios separados por coma."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"Los más recientes primero.","content":{"application/json":{"schema":{"type":"object","properties":{"jobs":{"type":"array","items":{"type":"object","required":["id","external_ref","profile_id","status","scheduled_at","content_hash","payload","legs","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"external_ref":{"type":"string","description":"Tu id del post (p. ej. content_posts.id)."},"profile_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["scheduled","dispatching","published","partial","failed","canceled"],"description":"`partial` = algunos legs publicaron y otros fallaron."},"scheduled_at":{"type":"string","format":"date-time"},"content_hash":{"type":"string","description":"sha256 del contenido normalizado. Con `external_ref` hace único al job."},"payload":{"type":"object","properties":{"caption":{"type":"string"},"media":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"type":{"type":"string","enum":["image","video"]}}}},"link":{"type":"string","format":"uri"},"settings":{"type":"object"}}},"legs":{"type":"array","items":{"type":"object","required":["id","account_id","platform","post_type","status","external_post_id","permalink","attempts","error","published_at"],"properties":{"id":{"type":"string","format":"uuid"},"account_id":{"type":"string","format":"uuid"},"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"post_type":{"type":"string","enum":["text","image","carousel","video","reel","story"]},"status":{"type":"string","enum":["pending","in_flight","published","failed","canceled"]},"external_post_id":{"type":"string","nullable":true,"description":"Id del post en la red (media id de IG, post id de FB). En stories, la última publicada."},"external_post_ids":{"type":"array","items":{"type":"string"},"description":"Solo stories: una por media, en el orden del post."},"permalink":{"type":"string","nullable":true,"description":"URL pública del post. Puede llegar `null` si la red aún no la expone (video recién subido)."},"attempts":{"type":"integer"},"error":{"type":"object","nullable":true,"properties":{"code":{"type":"integer","description":"Código numérico del proveedor (Meta) o negativo si es del servicio. Ramifica por esto."},"message":{"type":"string","description":"Texto del proveedor, puede venir en cualquier idioma."}}},"started_at":{"type":"string","format":"date-time","nullable":true},"published_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time"}}}},"created_at":{"type":"string","format":"date-time"},"dispatched_at":{"type":"string","format":"date-time","nullable":true},"completed_at":{"type":"string","format":"date-time","nullable":true},"canceled_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time"}}}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}}}}},"400":{"description":"`invalid_status`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/jobs/{id}":{"get":{"tags":["Jobs"],"summary":"Ver un job con sus legs","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Id del job"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"job":{"type":"object","required":["id","external_ref","profile_id","status","scheduled_at","content_hash","payload","legs","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"external_ref":{"type":"string","description":"Tu id del post (p. ej. content_posts.id)."},"profile_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["scheduled","dispatching","published","partial","failed","canceled"],"description":"`partial` = algunos legs publicaron y otros fallaron."},"scheduled_at":{"type":"string","format":"date-time"},"content_hash":{"type":"string","description":"sha256 del contenido normalizado. Con `external_ref` hace único al job."},"payload":{"type":"object","properties":{"caption":{"type":"string"},"media":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"type":{"type":"string","enum":["image","video"]}}}},"link":{"type":"string","format":"uri"},"settings":{"type":"object"}}},"legs":{"type":"array","items":{"type":"object","required":["id","account_id","platform","post_type","status","external_post_id","permalink","attempts","error","published_at"],"properties":{"id":{"type":"string","format":"uuid"},"account_id":{"type":"string","format":"uuid"},"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"post_type":{"type":"string","enum":["text","image","carousel","video","reel","story"]},"status":{"type":"string","enum":["pending","in_flight","published","failed","canceled"]},"external_post_id":{"type":"string","nullable":true,"description":"Id del post en la red (media id de IG, post id de FB). En stories, la última publicada."},"external_post_ids":{"type":"array","items":{"type":"string"},"description":"Solo stories: una por media, en el orden del post."},"permalink":{"type":"string","nullable":true,"description":"URL pública del post. Puede llegar `null` si la red aún no la expone (video recién subido)."},"attempts":{"type":"integer"},"error":{"type":"object","nullable":true,"properties":{"code":{"type":"integer","description":"Código numérico del proveedor (Meta) o negativo si es del servicio. Ramifica por esto."},"message":{"type":"string","description":"Texto del proveedor, puede venir en cualquier idioma."}}},"started_at":{"type":"string","format":"date-time","nullable":true},"published_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time"}}}},"created_at":{"type":"string","format":"date-time"},"dispatched_at":{"type":"string","format":"date-time","nullable":true},"completed_at":{"type":"string","format":"date-time","nullable":true},"canceled_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time"}}}}}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de tu tenant. No se distingue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Jobs"],"summary":"Re-agendar","description":"Solo `scheduled_at` y solo mientras el job siga `scheduled`. Cambiar el contenido es otro job: cancela éste y crea uno.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Id del job"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["scheduled_at"],"properties":{"scheduled_at":{"type":"string","format":"date-time"}}}}}},"responses":{"200":{"description":"Re-agendado.","content":{"application/json":{"schema":{"type":"object","properties":{"job":{"type":"object","required":["id","external_ref","profile_id","status","scheduled_at","content_hash","payload","legs","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"external_ref":{"type":"string","description":"Tu id del post (p. ej. content_posts.id)."},"profile_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["scheduled","dispatching","published","partial","failed","canceled"],"description":"`partial` = algunos legs publicaron y otros fallaron."},"scheduled_at":{"type":"string","format":"date-time"},"content_hash":{"type":"string","description":"sha256 del contenido normalizado. Con `external_ref` hace único al job."},"payload":{"type":"object","properties":{"caption":{"type":"string"},"media":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"type":{"type":"string","enum":["image","video"]}}}},"link":{"type":"string","format":"uri"},"settings":{"type":"object"}}},"legs":{"type":"array","items":{"type":"object","required":["id","account_id","platform","post_type","status","external_post_id","permalink","attempts","error","published_at"],"properties":{"id":{"type":"string","format":"uuid"},"account_id":{"type":"string","format":"uuid"},"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"post_type":{"type":"string","enum":["text","image","carousel","video","reel","story"]},"status":{"type":"string","enum":["pending","in_flight","published","failed","canceled"]},"external_post_id":{"type":"string","nullable":true,"description":"Id del post en la red (media id de IG, post id de FB). En stories, la última publicada."},"external_post_ids":{"type":"array","items":{"type":"string"},"description":"Solo stories: una por media, en el orden del post."},"permalink":{"type":"string","nullable":true,"description":"URL pública del post. Puede llegar `null` si la red aún no la expone (video recién subido)."},"attempts":{"type":"integer"},"error":{"type":"object","nullable":true,"properties":{"code":{"type":"integer","description":"Código numérico del proveedor (Meta) o negativo si es del servicio. Ramifica por esto."},"message":{"type":"string","description":"Texto del proveedor, puede venir en cualquier idioma."}}},"started_at":{"type":"string","format":"date-time","nullable":true},"published_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time"}}}},"created_at":{"type":"string","format":"date-time"},"dispatched_at":{"type":"string","format":"date-time","nullable":true},"completed_at":{"type":"string","format":"date-time","nullable":true},"canceled_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time"}}}}}}}},"400":{"description":"`invalid_body`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de tu tenant. No se distingue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`job_not_reschedulable` — ya se está publicando o ya terminó.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Jobs"],"summary":"Cancelar (con confirmación)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Id del job"}],"responses":{"200":{"description":"Siempre dice qué pasó.","content":{"application/json":{"schema":{"type":"object","required":["result","job"],"properties":{"result":{"type":"string","enum":["confirmed","in_flight","already_published"],"description":"`confirmed` = no va a salir. `in_flight` = ya se está publicando, no se puede detener. `already_published` = ya salió."},"job":{"type":"object","required":["id","external_ref","profile_id","status","scheduled_at","content_hash","payload","legs","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"external_ref":{"type":"string","description":"Tu id del post (p. ej. content_posts.id)."},"profile_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["scheduled","dispatching","published","partial","failed","canceled"],"description":"`partial` = algunos legs publicaron y otros fallaron."},"scheduled_at":{"type":"string","format":"date-time"},"content_hash":{"type":"string","description":"sha256 del contenido normalizado. Con `external_ref` hace único al job."},"payload":{"type":"object","properties":{"caption":{"type":"string"},"media":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"type":{"type":"string","enum":["image","video"]}}}},"link":{"type":"string","format":"uri"},"settings":{"type":"object"}}},"legs":{"type":"array","items":{"type":"object","required":["id","account_id","platform","post_type","status","external_post_id","permalink","attempts","error","published_at"],"properties":{"id":{"type":"string","format":"uuid"},"account_id":{"type":"string","format":"uuid"},"platform":{"type":"string","enum":["facebook","instagram","tiktok"]},"post_type":{"type":"string","enum":["text","image","carousel","video","reel","story"]},"status":{"type":"string","enum":["pending","in_flight","published","failed","canceled"]},"external_post_id":{"type":"string","nullable":true,"description":"Id del post en la red (media id de IG, post id de FB). En stories, la última publicada."},"external_post_ids":{"type":"array","items":{"type":"string"},"description":"Solo stories: una por media, en el orden del post."},"permalink":{"type":"string","nullable":true,"description":"URL pública del post. Puede llegar `null` si la red aún no la expone (video recién subido)."},"attempts":{"type":"integer"},"error":{"type":"object","nullable":true,"properties":{"code":{"type":"integer","description":"Código numérico del proveedor (Meta) o negativo si es del servicio. Ramifica por esto."},"message":{"type":"string","description":"Texto del proveedor, puede venir en cualquier idioma."}}},"started_at":{"type":"string","format":"date-time","nullable":true},"published_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time"}}}},"created_at":{"type":"string","format":"date-time"},"dispatched_at":{"type":"string","format":"date-time","nullable":true},"completed_at":{"type":"string","format":"date-time","nullable":true},"canceled_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time"}}}}}}}},"401":{"description":"API key ausente, inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe, o no es de tu tenant. No se distingue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}