# heysteff API > API REST de heysteff (versión 1.0.0). Base: https://app.heysteff.com/api. Todas las rutas empiezan con /v1. Especificación OpenAPI 3.1: https://app.heysteff.com/api/v1/openapi.json. Documentación: https://developers.heysteff.com/docs/ ## Autenticación Cada solicitud lleva la API key del workspace en un header: Authorization: Bearer sk_live_ La key se crea en el panel de heysteff, Configuración → Desarrolladores → API Keys, y se muestra una sola vez. Cada key tiene scopes (permisos) y opcionalmente una lista de IPs permitidas. Una operación sin el scope requerido responde 403. Nunca expongas la key en código del navegador, apps móviles ni repositorios: úsala solo desde tu servidor y guárdala en una variable de entorno. ## Idempotencia Las operaciones que crean algo aceptan el header `Idempotency-Key` (en /v1/charges y /v1/refunds es obligatorio). Si repites una solicitud con la misma clave, recibes exactamente la misma respuesta que la primera vez, sin efectos duplicados. Si la primera todavía está en proceso, la API responde 409 `idempotency_conflict`: reintenta en unos segundos. Usa una clave por operación lógica (por ejemplo, el id del pedido más la acción). En /v1/events la clave va en el cuerpo (`idempotency_key`) y en /v1/conversions la da `external_ref`. ## Límites Cada API key tiene un tope de solicitudes por ventana de 60 segundos que depende del plan del espacio: developer_free 100, developer 2000, developer_platform 4000. Cada respuesta trae `X-RateLimit-Limit` (el tope), `X-RateLimit-Remaining` (lo que te queda en la ventana) y `X-RateLimit-Reset` (segundos unix en que empieza la siguiente). Al pasarte, la API responde 429 `rate_limited` con `Retry-After`: espera ese tiempo y reintenta con backoff exponencial. Los límites del plan (por ejemplo, mensajes al mes) responden 402 `plan_limit_exceeded`. ## Errores Todo error usa el mismo formato: { "error": { "code": "validation_error", "message": "Request validation failed", "details": { ... } } } - 400: JSON inválido (`invalid_json`) o validación fallida (`validation_error`, con el detalle por campo en `details`). - 401: falta la API key, es inválida o fue revocada (`unauthorized`), o venció (`api_key_expired`, con `details.expired_at`). Una key puede tener fecha de vencimiento (`expires_at`); 7 días antes avisamos por correo a los owners y admins. - 402: límite del plan alcanzado (`plan_limit_exceeded`). - 403: falta el scope, la IP no está permitida o el workspace está deshabilitado. - 404: el recurso no existe en este workspace (`not_found`). - 409: conflicto, por ejemplo una `Idempotency-Key` en proceso. - 422: la solicitud es válida pero no se puede procesar (por ejemplo, `no_channel_connection`; al mandar un SMS, `invalid_sms_message` o `sms_sender_mismatch`; un Flow o un botón de llamar fuera de WhatsApp Cloud, `whatsapp_cloud_only`). - 424: falló un proveedor externo, por ejemplo Meta (`meta_flow_error`, `meta_error`). - 429: límite de solicitudes de la key (`rate_limited`). ## Paginación Los listados por cursor devuelven `{ data: [...], cursor: { next, has_more } }`: pasa `cursor=` en la siguiente llamada mientras `has_more` sea true. `limit` va de 1 a 200. /v1/reviews pagina con `limit` y `offset`; /v1/events con `after_seq`. ## Webhooks salientes Los webhooks se configuran con POST /v1/webhooks (scope `webhooks:write`; el secreto `secret` se devuelve una sola vez), PATCH /v1/webhooks/{id} (eventos o pausa) y DELETE /v1/webhooks/{id}, o en Configuración → Desarrolladores → Webhooks salientes, eligiendo los eventos (por ejemplo `message:received`, `message:sent`, `message.delivered`, `message.read`, `message.failed`, `conversation.escalated`, `flow.completed`, `payment.paid`); sin eventos elegidos recibe todos. heysteff hace un POST JSON a tu URL con el sobre `{ id, tenant_id, event, ts, data }` y estos headers: - X-Heysteff-Event: nombre del evento. - X-Heysteff-Timestamp: segundos unix del momento de la firma. - X-Heysteff-Signature: `v1=`, HMAC-SHA256 con el secreto del webhook sobre el string `${timestamp}.${body}` (body = el cuerpo crudo, sin re-serializar). - X-Heysteff-Delivery-Id: id de la entrega a ESTE webhook (el mismo en sus reintentos). - X-Heysteff-Idempotency-Key: id estable del evento (igual al `id` del sobre): el mismo en cada reintento y en cada webhook que recibe el mismo evento. Úsalo para deduplicar. Para verificar: calcula el HMAC sobre `${timestamp}.${body}`, compáralo en tiempo constante con lo que viene después de `v1=` y rechaza timestamps con más de 300 segundos de diferencia con tu reloj. Responde 2xx rápido. Una entrega que falla (sin 2xx) se reintenta hasta 6 intentos con espera creciente; después queda marcada como fallida (dead letter). Deduplica por X-Heysteff-Idempotency-Key: un reintento puede llegar más de una vez. Eventos del ciclo de vida: `conversation.created`, `conversation.inactive`, `conversation.ended`, `conversation.assigned`, `conversation.bot_toggled`, `contact.updated`, `contact.opted_out`, `contact.opted_in`, `channel.disconnected`, `channel.reconnected`, `channel.restricted`, `webhook.paused`. Salen en segundos por cualquier camino (panel, API, MCP, app, campañas). `conversation.inactive` sale una vez por inactividad cuando una conversación abierta lleva `inactivity_hours` sin mensajes (por webhook, 1–720, por defecto 24). `conversation.ended` trae `close_kind` (resolved, archived, auto_archived). `channel.error` sigue saliendo cuando un canal falla. Agrupar mensajes rápidos: con `batch: { wait_seconds, max_size }` en POST/PATCH /v1/webhooks (1–60 s, por defecto 5; 1–100 eventos, por defecto 50), los `message:received` de una conversación que llegan dentro de la espera salen en un solo POST `{ id, tenant_id, event, ts, batch: true, events: [sobre, ...] }` con `X-Heysteff-Batch: true` y `X-Heysteff-Batch-Size`. `batch: null` lo apaga. Pausa automática: si en 15 minutos un webhook tuvo 40 entregas o más y fallaron 85 % o más, queda en pausa (`is_active: false`, `paused_at`, `paused_reason: failure_rate`), avisamos por correo a owners y admins y los demás webhooks reciben `webhook.paused`. Los eventos de mientras quedan en cola y salen al reactivarlo con PATCH /v1/webhooks/{id} `{ "is_active": true }` o desde la consola. Eventos de estado de un mensaje saliente, uno por transición: `message.delivered` (llegó al teléfono), `message.read` (el cliente lo leyó) y `message.failed` (el proveedor lo rechazó, con `error: { code, message }`). Cada uno trae `message_id`, `conversation_id`, `channel_type`, `external_message_id`, `status` y `timestamp`. Un recibo repetido del proveedor no vuelve a avisar. `message:received` trae `interactive_reply: { id, title }` cuando el cliente toca un botón o una opción de lista (o `null`) y `flow_response: { flow_token, data }` cuando envía un WhatsApp Flow (o `null`). Llamadas de WhatsApp: `call.started` (empezó a sonar o la contestaron), `call.ended` (terminó después de contestar, o falló) y `call.missed` (nadie contestó, o la persona la rechazó), uno por transición. Cada uno trae `call_id` (el id de GET /v1/calls), `wa_call_id`, `channel_connection_id`, `conversation_id`, `contact_id`, `direction` (inbound, outbound), `status` (ringing, answered, completed, missed, rejected, failed), `customer` (el teléfono del cliente), `started_at`, `ended_at`, `duration_seconds` y `error`. Para que lleguen, la app de Meta del número debe tener suscrito el campo de webhook `calls`. Registro de entregas: GET /v1/webhooks lista los webhooks y GET /v1/webhooks/{id}/deliveries sus entregas (scope `webhooks:read`), con `state` (pending, retrying, succeeded, failed), `http_status`, `duration_ms`, `error` y `response_body_excerpt`; filtra con `status=failed`, `event`, `since` y `until`. Cuando arregles tu servidor, POST /v1/webhook-deliveries/{id}/retry (scope `webhooks:write`) vuelve a mandar una entrega fallida con el mismo X-Heysteff-Delivery-Id. Formato Meta: un webhook creado con `format: "meta"` recibe, en vez del sobre, el payload crudo de la Cloud API de Meta (`object: "whatsapp_business_account"`, `entry[].changes[].value` con `messages` o `statuses`) firmado con `X-Hub-Signature-256: sha256=` (HMAC-SHA256 del cuerpo crudo con el secreto del webhook), solo para los eventos de mensajes de WhatsApp Cloud (`message:received`, `message:sent`, `message.delivered`, `message.read`, `message.failed`). Así un receptor hecho para Meta funciona sin cambios. ## API compatible con la Cloud API de Meta Para código que ya existe contra `graph.facebook.com`: la base `https://app.heysteff.com/api/meta/whatsapp/` (cualquier `v.`) con `Authorization: Bearer sk_live_…` acepta los cuerpos de la Cloud API y responde con su forma: POST /{phone_number_id}/messages (texto, media por `id` o `link`, sticker, ubicación, contactos, reacción, interactivos, plantillas y `status: "read"` con `typing_indicator`), POST /{phone_number_id}/media, GET y DELETE /{media_id} (y GET /{media_id}/download), GET /{phone_number_id}, GET y POST /{phone_number_id}/whatsapp_business_profile, GET /{waba_id}/phone_numbers y GET, POST y DELETE /{waba_id}/message_templates. `{phone_number_id}` es el de un número de WhatsApp Cloud del espacio (con una key `sk_test_`, el del sandbox). Todo pasa por el mismo envío de POST /v1/messages. `messages[0].id` es el id de heysteff y los estados en formato Meta traen ese mismo id. Errores con la forma de Graph (`error.code`, `error_subcode`, `fbtrace_id`) más `error.heysteff_code`. Estas rutas no están en este documento porque su contrato es el de Meta: guía en https://developers.heysteff.com/docs/meta-compatible. ## Números de WhatsApp de heysteff Un workspace sin número para la Cloud API puede usar uno de EE. UU. (+1) que heysteff ya verificó en Meta (Developer Free y Developer incluyen 1). POST /v1/numbers (scope `numbers:write`) reserva uno por 30 minutos y devuelve `connect_url`: la API NO puede dejar el número en la cuenta de WhatsApp del cliente, eso solo pasa dentro del Embedded Signup de Meta, así que una persona abre el link en un navegador, entra con Facebook y elige el número. El link es de un solo uso; pedirlo de nuevo reusa la reserva con plazo renovado. GET /v1/numbers (scope `numbers:read`) muestra cada número con `state` (reserved, connected, disconnected), `channel_connection_id` y el estado del nombre visible, más `plan.limit`/`plan.used` y `offer.available`. DELETE /v1/numbers/{id} suelta una reserva sin conectar; un número conectado responde 409 `wa_pool_number_connected`. POST /v1/numbers/{id}/display-name pide a Meta otro nombre visible (424 si Meta lo rechaza). Meta cobra los mensajes a la tarjeta de la cuenta de WhatsApp del cliente. Para cualquier número de WhatsApp Cloud del workspace (de heysteff o propio), con `{id}` = `channel_connection_id` y solo con keys `sk_live_`: GET /v1/whatsapp-numbers/{id} (scope `numbers:read`) devuelve en vivo desde Meta la calidad (`quality_rating`), el límite de mensajería, el estado del nombre visible y la verificación de la empresa con `verify_url`; GET y PATCH /v1/whatsapp-numbers/{id}/business-profile (scopes `numbers:read` y `numbers:write`) leen y cambian el perfil de empresa (`about`, `description`, `address`, `email`, `websites`, `vertical` y la foto en `profile_picture_url`). ## WhatsApp Flows Formularios que se abren dentro del chat de WhatsApp. Solo WhatsApp Cloud y solo con keys `sk_live_` (una key de prueba responde 403 `test_key_not_allowed`). POST /v1/whatsapp-flows (scope `flows:write`) crea un borrador con `name`, `categories` y `flow_json` opcional (objeto o texto, hasta 1 MB); PUT /v1/whatsapp-flows/{id}/json sube o reemplaza el Flow JSON de un borrador y devuelve `validation_errors` de Meta (vacío = listo); POST /v1/whatsapp-flows/{id}/publish lo publica (desde ahí no cambia), POST /v1/whatsapp-flows/{id}/deprecate lo retira y DELETE borra un borrador (uno publicado responde 409 `flow_not_draft`). GET /v1/whatsapp-flows/{id}/preview da un link para probarlo en el navegador. Si creaste Flows en WhatsApp Manager, POST /v1/whatsapp-flows/sync los trae. Para mandarlo: POST /v1/messages con `channel: "whatsapp_cloud"`, `content_type: "flow"` y `flow: { flow_id, cta, body, flow_token? }` (con la ventana de 24 h abierta; fuera de ella, una plantilla con botón FLOW). La respuesta llega en `message:received` con `flow_response: { flow_token, data }` y queda en GET /v1/whatsapp-flows/responses (scope `flows:read`); con `save_to_contact: true` en el Flow, cada campo simple se guarda en el campo del contacto con la misma clave. Errores: 422 `flow_json_invalid` (con `details.validation_errors`), 422 `no_whatsapp_cloud_channel`, 422 `whatsapp_cloud_only` (al mandarlo por otro canal), 424 `meta_flow_error`. heysteff no aloja el endpoint de datos cifrado de un Flow (`data_exchange`): si tu Flow lo usa, configúralo en Meta con tu propio servidor. ## Llamadas de WhatsApp PATCH /v1/whatsapp-numbers/{id}/calling (scope `calls:write`; `{id}` = `channel_connection_id`) enciende las llamadas del número (`enabled`), el botón de llamar (`call_icon_visible`), el permiso de devolución (`callback_permission`) y el horario (`call_hours`). GET /v1/calls (scope `calls:read`) es el registro, con `status`, `direction` y `before`/`next_before` para paginar; cada llamada avisa con los webhooks `call.started`, `call.ended` y `call.missed`. POST /v1/messages con `content_type: "voice_call"` manda un botón para que el cliente llame, y `content_type: "call_permission_request"` le pide permiso para llamarlo. heysteff registra las llamadas pero todavía no las contesta ni lleva el audio: para eso, conecta tu propio servidor SIP en Meta (con SIP encendido, Meta deja de mandar los webhooks de llamadas) o usa WebRTC con la Calling API de Meta y un servidor de medios propio. ## Clientes y links de conexión Para un SaaS cuyos clientes conectan SU propio canal. POST /v1/customers (scope `customers:write`) crea un cliente con tu `external_id` (único en el espacio; 409 `customer_external_id_taken`), `name` y `metadata` opcional (objeto de hasta 16 KB); el cupo lo da el plan (Developer Free 1, Developer 10, Developer Platform ilimitados; al pasarlo, 402 `plan_limit` con `details.resource = developer_customers`). GET /v1/customers (scope `customers:read`) los lista con `external_id`, `limit` (1-100) y `cursor` (`next_cursor`); GET, PATCH y DELETE /v1/customers/{id} consultan, cambian y borran uno (borrar no desconecta sus canales). POST /v1/customers/{id}/setup-links (scope `customers:write`) crea un link de conexión con `allowed_channels` (whatsapp_cloud, instagram, facebook, gmail; por defecto los cuatro), `expires_in_hours` (1-2160, por defecto 720 = 30 días), `success_redirect_url`/`failure_redirect_url` (https o http://localhost), `brand_name`, `brand_logo_url`, `theme` ({primary_color, background_color, text_color, border_color} en #RRGGBB), `locale` (auto, es, en, pt), `whatsapp_modes` (own_number, coexistence: el número sigue en la app del celular, heysteff_number: un número de heysteff si el plan los incluye; por defecto los dos primeros) y `reconnect_channel_id` (vuelve a conectar un canal caído del cliente; el link solo permite ese tipo), y devuelve `url` (`https://developers.heysteff.com/connect/hsl_…`) solo en esa respuesta. El cliente final abre el link en un navegador y conecta UN canal (Embedded Signup de Meta para WhatsApp, Facebook para Instagram y Messenger, Instagram Login para una cuenta de Instagram sin página de Facebook, Google para Gmail); el link queda usado y vence a las `expires_in_hours`. Al terminar vuelve a `success_redirect_url?status=connected&channel_id=…&customer_id=…&channel_type=…` (si falla, a `failure_redirect_url` con `status=error&error=&customer_id=…`) y llega el webhook `customer.channel_connected` con `{ customer_id, external_id, channel_connection_id, channel_type, display_name, setup_link_id }` (además del evento general `channel.connected`, que sale con cualquier conexión o reconexión del espacio). GET /v1/customers/{id}/channels lista sus canales. POST /v1/messages acepta `customer_id`: sale por el canal activo de ese cliente del tipo `channel` (el conectado más reciente); 404 `customer_not_found`, 422 `customer_channel_not_connected`. Las keys `sk_test_` no crean links (403 `test_key_not_allowed`). Un solo link activo por cliente y canal: crear uno revoca los anteriores que se cruzan. GET /v1/setup-links (y GET /v1/customers/{id}/setup-links) lista los links con `state` (active, used, expired, revoked); POST /v1/setup-links/{id}/revoke revoca uno sin usar. GET /v1/customers/{id}/usage?from=YYYY-MM-DD&to=YYYY-MM-DD (scope `customers:read`) da mensajes (`messages`, `inbound`, `outbound`, `failed`) y `conversations` del cliente en total, por canal y por día (UTC; por defecto los últimos 30 días, hasta 366), para cobrarle a tus clientes. ## Bandeja embebible Mete la bandeja de heysteff en tu producto con un iframe. POST /v1/inbox-embeds (scope `inbox:embed`) crea un token con `name`, `scope` (all, channel con `channel_connection_id`, customer con `customer_id`), `allowed_origins` (1-20, p. ej. https://app.acme.com o https://*.acme.com; van al CSP `frame-ancestors`), `theme` (system, light, dark), `locale` (auto, es, en, pt) y `expires_in_seconds` opcional (300 a 31536000), y devuelve `token` (hse_…), `embed_url` (`https://developers.heysteff.com/embed/inbox?token=…`) y `snippet` (el iframe) solo en esa respuesta. La vista muestra las conversaciones del alcance de todos los canales, el hilo, responder (texto, archivos y plantillas de WhatsApp con la ventana de 24 h cerrada), marcar leído y tomar o soltar el bot, en tiempo real; nunca ve nada fuera del alcance. Filtros por la URL: `status` (open, closed, snoozed, all), `search`, `unread=1`, `channel`. Crea tokens por sesión de tu usuario con un vencimiento corto desde tu backend en vez de poner uno sin vencimiento en una página pública. GET /v1/inbox-embeds, GET/PATCH/DELETE /v1/inbox-embeds/{id} los listan, cambian (nombre, orígenes, tema, idioma) y revocan. Las keys `sk_test_` no crean tokens. ## Agente de IA (workspaces developer) Los workspaces developer traen el agente de IA de heysteff (apagado al crear el espacio). GET /v1/agent (scope `agent:read`) devuelve `enabled`, `instructions` (system prompt), `provider` (anthropic, openai, deepseek u openrouter; null = el modelo por defecto), `model`, `temperature` (0 preciso a 1 creativo), `keys` (solo proveedor y `last4`), `quota` ({ period, used, limit, remaining } del mes UTC), los interruptores por canal y por cliente y `models` (modelos sugeridos con su precio de lista en USD por millón de tokens). PATCH /v1/agent (scope `agent:write`) cambia lo que mandes: `channels: [{ id: , enabled }]` y `customers: [{ id: , enabled }]` con true (encender), false (apagar) o null (volver a heredar); el interruptor del espacio manda sobre todos. Por conversación, POST /v1/conversations/{id}/bot con { paused } pausa o reanuda el bot. Costo: PUT /v1/agent/keys { provider, api_key } guarda cifrada tu key del proveedor (BYOK; nunca se devuelve) y DELETE /v1/agent/keys/{provider} la borra. Sin key, cada respuesta descuenta del cupo de prueba del plan con la key de heysteff (Developer Free 200, Developer 2.000, Developer Platform 20.000 respuestas al mes, compartido con el asistente de la consola). Sin key y sin cupo el agente no responde, nunca cambia de proveedor, y llega el webhook `agent.failed` con `code: "ai_quota_exhausted"`; el asistente de la consola responde 402 `ai_quota_exhausted` con `fix`. Custom tools: GET/POST /v1/agent/tools y GET/PATCH/DELETE /v1/agent/tools/{id} definen llamadas HTTP a tu backend que el agente hace en la conversación (`spec.url_template` con `{{args.x}}` declarados en `spec.parameters_schema`; `spec` se reemplaza completa al editar; la credencial hacia tu backend se carga en la consola). Cupo de custom tools: Developer Free 2, Developer 10, Developer Platform ilimitadas (402 `plan_limit`). Webhooks del agente (solo workspaces developer): `agent.replied` { conversation_id, trigger_message_id, message_ids, mode: sent|suggestion, text, channel_type, channel_connection_id, provider, model, credential: byok|platform }, `agent.handoff` { conversation_id, escalation_id, reason, by, bot_paused } y `agent.failed` { conversation_id, trigger_message_id, code: ai_quota_exhausted|provider_error, message, provider }. Hallazgos (Developer y Developer Platform): GET /v1/insights?days=30&limit=10 (scope `insights:read`) devuelve `included`, `enabled`, `range` y tres listas { total, items }: `frustrated`, `agent_gaps` (lo que el agente no supo o respondió mal) y `unresolved`; el análisis semanal manda `insight.detected` { report_id, funnel_id, week_start, week_end, unresolved, knowledge_gaps, top_gaps }. Por MCP: get_developer_agent, update_developer_agent, delete_developer_ai_key y get_developer_insights (la key no se guarda por MCP). Las rutas /v1/agent responden 403 `developer_workspace_only` en un workspace que no es developer. ## Operaciones ## Mensajes ### POST /v1/messages — Enviar un mensaje (scope: messages:write) operationId: createMessage Encola un mensaje a un contacto por el canal indicado. Además de texto, media y plantillas: `content_type: "interactive"` (botones o lista en `interactive`; nativos en WhatsApp Cloud, quick replies en Instagram y Messenger, texto con opciones numeradas en SMS, email y WhatsApp por QR, con `degraded: true` en la respuesta; la respuesta del cliente llega en `message:received` con `interactive_reply: { id, title }`), `content_type: "location"` (nativa en WhatsApp Cloud; en el resto, texto con un enlace de mapa) y `content_type: "reaction"` (un emoji sobre `reaction.message_id`, en WhatsApp e Instagram, responde 202; Instagram solo acepta ❤️ y cualquier otro emoji responde 422 `reaction_emoji_unsupported`; Messenger responde 422 `reactions_unsupported`; una key de prueba responde 403 `test_key_not_allowed`). Botones, listas y ubicaciones exigen la ventana de 24 h abierta en WhatsApp Cloud, Instagram y Messenger (409 `wa_window_closed` / `meta_window_closed`). Un texto más largo que el canal (Instagram 1.000, Messenger 2.000) sale en piezas (`parts`). Fuera de la ventana de 24 h de WhatsApp Cloud solo se pueden mandar plantillas aprobadas (`content_type: "template"`). Para mandar una imagen, audio, video o documento, sube el archivo con POST /v1/media y manda su `url` en `media.url` con `content_type` `image`, `audio`, `video` o `file`; `content` va como pie (opcional). Las plantillas disponibles salen de GET /v1/templates. Sin `channel_connection_id` sale por el canal predeterminado de la API del workspace. Con `customer_id` (un cliente de POST /v1/customers) sale por el canal de ese cliente del tipo `channel`, el conectado más reciente con su link de conexión; si no tiene uno activo responde 422 `customer_channel_not_connected`. Con `channel: "sms"` solo se admite texto (`content_type: "text"`, hasta 1.600 caracteres) a un teléfono en formato internacional; si no, responde 422 `invalid_sms_message`, y un `channel_connection_id` que no es de un número SMS responde 422 `sms_sender_mismatch`. El SMS se cobra por segmento y cada mensaje admite hasta 3. Solo en WhatsApp Cloud (`channel: "whatsapp_cloud"`; en otro canal, o si el remitente no es un número de WhatsApp Cloud, responde 422 `whatsapp_cloud_only`, sin degradarse a texto ni a un link) y solo con una key `sk_live_` (una key de prueba responde 403 `test_key_not_allowed`): `content_type: "flow"` manda un WhatsApp Flow (formulario dentro del chat) con `flow: { flow_id, cta, body, header?, footer?, flow_token?, mode?, action?, screen?, data? }`; `flow_id` es el id de Meta que da GET /v1/whatsapp-flows; si falta `flow_token`, heysteff genera uno `hs_…`; `mode: "draft"` sirve para probar un borrador. La respuesta del cliente llega en `message:received` con `flow_response: { flow_token, data }`. `content_type: "voice_call"` manda un botón para llamar al negocio por WhatsApp (`voice_call: { body, display_text?, ttl_minutes?, payload? }`; el número necesita las llamadas encendidas con PATCH /v1/whatsapp-numbers/{id}/calling) y `content_type: "call_permission_request"` le pide al cliente permiso para llamarlo (`call_permission: { body }`). Los tres son mensajes de sesión: fuera de la ventana de 24 h responden 409 `wa_window_closed` (fuera de la ventana, usa una plantilla con botón FLOW o de llamada). Si Meta rechaza uno al enviarlo, el mensaje queda `failed` (webhook `message.failed`) y no se reemplaza por texto. Parámetros: - Idempotency-Key (header)?: string Cuerpo JSON: - to?: string - contact_ref?: string - channel: "instagram" | "facebook" | "whatsapp_cloud" | "whatsapp_baileys" | "gmail" | "sms" | "shopify" - channel_connection_id?: string - customer_id?: string - content?: string - content_type?: "text" | "image" | "audio" | "video" | "file" | "template" | "interactive" | "reaction" | "location" | "flow" | "voice_call" | "call_permission_request" - template?: object - name: string - language: string - body_params?: string[] - components?: object[] - media?: object - url: string - filename?: string - interactive?: object - variante 1: object - type: "buttons" - body: string - buttons: object[] - id: string - title: string - variante 2: object - type: "list" - body: string - button: string - sections: object[] - title?: string - rows: object[] - reaction?: object - message_id: string - emoji: string - location?: object - latitude: number - longitude: number - name?: string - address?: string - flow?: object - flow_id: string - cta: string - body: string - header?: string - footer?: string - flow_token?: string - mode?: "draft" | "published" - action?: "navigate" | "data_exchange" - screen?: string - data?: object - voice_call?: object - body: string - display_text?: string - ttl_minutes?: integer - payload?: string - call_permission?: object - body: string Respuestas: 201, 202, 400, 401, 402, 403, 409, 422, 429 ### GET /v1/messages/{id} — Estado de un mensaje (scope: conversations:read) operationId: getMessage Un mensaje del workspace con su estado (con una key de prueba, solo los del sandbox) (`queued`, `sent`, `delivered`, `read`, `failed`; `received` si lo mandó el cliente), el canal, los momentos de envío, entrega y lectura, y el error del proveedor si falló; un entrante que responde a un WhatsApp Flow trae `flow_response`. Los cambios de estado también llegan por webhook (`message.delivered`, `message.read`, `message.failed`). Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 429 ### POST /v1/messages/{id}/read — Marcar como leído (scope: messages:write) operationId: markMessageRead Marca como leído un mensaje que mandó el cliente: la conversación queda leída en heysteff y, donde el canal lo permite, el cliente lo ve (tildes azules en WhatsApp Cloud; "visto" en Instagram y Messenger, donde Meta marca la conversación entera). SMS, email y WhatsApp por QR no tienen recibo: responde `supported: false` sin mandar nada. `receipt_sent` dice si el proveedor lo aceptó. Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 422, 429 ### POST /v1/media — Subir un archivo (scope: messages:write) operationId: uploadMedia Sube un archivo (multipart, campo `file`) para mandarlo después en un mensaje: imágenes (JPEG, PNG, WebP, GIF), audio (MP3, M4A, OGG, WAV, WebM), video (MP4, MOV), PDF y documentos de oficina, hasta 15 MB. La `url` que devuelve va en `media.url` de POST /v1/messages. Hasta 30 subidas por minuto y 1.000 por día por workspace. Respuestas: 201, 400, 401, 403, 413, 415, 429 ### GET /v1/templates — Listar plantillas de WhatsApp (scope: messages:write) operationId: listTemplates Plantillas de WhatsApp Cloud de los números conectados del workspace, con lo necesario para mandarlas por POST /v1/messages (`content_type: "template"`). Por defecto solo las aprobadas; `status=all` trae todas. Con una key de prueba la lista viene vacía: el sandbox no manda plantillas. Parámetros: - channel_connection_id (query)?: string - status (query)?: "APPROVED" | "PENDING" | "REJECTED" | "PAUSED" | "DISABLED" | "all" Respuestas: 200, 400, 401, 403, 429 ### POST /v1/templates — Crear una plantilla de WhatsApp (scope: templates:write) operationId: createTemplate Crea una plantilla de WhatsApp Cloud y la manda a revisión de Meta. Responde con la plantilla, casi siempre en `PENDING`; el estado cambia solo cuando Meta la aprueba o rechaza (míralo con GET /v1/templates?status=all). Sin `channel_connection_id` usa el número de WhatsApp Cloud por defecto de la API. El nombre va en minúsculas con guiones bajos; cada variable `{{n}}` del cuerpo necesita su ejemplo en `body_examples`. Los encabezados con imagen, video o documento se crean desde el panel. Hasta 20 por hora. Cuerpo JSON: - channel_connection_id?: string - name: string - language: string - category: "MARKETING" | "UTILITY" | "AUTHENTICATION" - header_text?: string - header_example?: string - header_media_format?: "IMAGE" | "VIDEO" | "GIF" | "DOCUMENT" - header_media_handle?: string - body_text: string - body_examples?: string[] - footer_text?: string - buttons?: object[] - variante 1: object - type: "QUICK_REPLY" - text: string - variante 2: object - type: "URL" - text: string - url: string - url_example?: string - variante 3: object - type: "PHONE_NUMBER" - text: string - phone_number: string - variante 4: object - type: "FLOW" - text: string - flow_id: string - flow_action?: "navigate" | "data_exchange" - navigate_screen?: string - variante 5: object - type: "VOICE_CALL" - text: string - ttl_minutes?: integer - marketing?: object - ttl_seconds?: integer - creative_features?: object - image_brightness_and_contrast?: "OPT_IN" | "OPT_OUT" - image_touchups?: "OPT_IN" | "OPT_OUT" - add_text_overlay?: "OPT_IN" | "OPT_OUT" - image_animation?: "OPT_IN" | "OPT_OUT" - image_background_gen?: "OPT_IN" | "OPT_OUT" - auto_promotion_tag?: "OPT_IN" | "OPT_OUT" - text_extraction_for_headline?: "OPT_IN" | "OPT_OUT" - text_extraction_for_tap_target?: "OPT_IN" | "OPT_OUT" - product_extensions?: "OPT_IN" | "OPT_OUT" - text_formatting_optimization?: "OPT_IN" | "OPT_OUT" - link_tracking_opted_out?: boolean - max_price?: object - bid_amount: integer - bid_strategy?: "LOWEST_COST_WITH_BID_CAP" - bid_country_multiplier_overrides?: object Respuestas: 201, 400, 401, 403, 404, 422, 424, 429 ### DELETE /v1/templates/{name} — Borrar una plantilla de WhatsApp (scope: templates:write) operationId: deleteTemplate Borra una plantilla en Meta y en heysteff. Sin `language` borra todas sus traducciones; con `channel_connection_id`, solo la de ese número. Meta no deja reusar el nombre de una plantilla borrada por 30 días. Parámetros: - name (path): string - language (query)?: string - channel_connection_id (query)?: string Respuestas: 200, 400, 401, 403, 404, 424, 429 ## Conversaciones ### GET /v1/conversations — Listar conversaciones (scope: conversations:read) operationId: listConversations Conversaciones del workspace, de la más reciente a la más antigua, paginadas por cursor. Parámetros: - cursor (query)?: string - limit (query)?: integer - status (query)?: "open" | "closed" | "snoozed" - channel (query)?: "instagram" | "facebook" | "whatsapp_cloud" | "whatsapp_baileys" | "gmail" | "sms" | "shopify" | "mercadolibre" - customer_id (query)?: string Respuestas: 200, 400, 401, 403, 429 ### GET /v1/conversations/{id}/messages — Listar mensajes de una conversación (scope: conversations:read) operationId: listConversationMessages Mensajes de una conversación, paginados por cursor. Parámetros: - id (path): string - cursor (query)?: string - limit (query)?: integer Respuestas: 200, 400, 401, 403, 429 ### GET /v1/conversations/{id} — Leer una conversación (scope: conversations:read) operationId: getConversation Una conversación con lo necesario para contestarle (`channel`, `external_user_id` y `channel_connection_id` van tal cual a POST /v1/messages) y su estado: si el bot está pausado y por quién, la escalación abierta y la asignación. Con una key de prueba solo existen las conversaciones del sandbox. Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 429 ### POST /v1/conversations/{id}/escalate — Pasar a una persona (scope: conversations:write) operationId: escalateConversation Tu IA no sabe qué responder: pasa la conversación a una persona del equipo en la bandeja de heysteff. Abre una escalación, pausa el bot (`bot_paused_by` = `api_escalation`), la reabre si estaba archivada, avisa al equipo y calcula un resumen con su urgencia. Idempotente: si ya hay una escalación abierta no abre otra (`already_open: true`, status 200), pero el `reason` nuevo se suma al caso. En un espacio personal no hay equipo: responde 422 `personal_workspace_no_escalation`. Parámetros: - id (path): string Cuerpo JSON: - reason?: string - urgency?: "high" | "medium" | "low" Respuestas: 200, 201, 400, 401, 403, 404, 422, 429 ### POST /v1/conversations/{id}/bot — Pausar o reanudar el bot (scope: conversations:write) operationId: setConversationBot `{"paused": true}` pausa el bot sin fecha de vuelta (`bot_paused_by` = `api`), corta el flujo que llevaba la conversación y no pisa una pausa que ya existía. `{"paused": false}` lo reanuda y cierra la escalación abierta, como "Reactivar bot" en la bandeja; la pausa de un flujo que sigue corriendo no se levanta (`kept_flow_pause: true`). Parámetros: - id (path): string Cuerpo JSON: - paused: boolean Respuestas: 200, 400, 401, 403, 404, 429 ### POST /v1/conversations/{id}/resolve — Resolver una conversación (scope: conversations:write) operationId: resolveConversation El "Resolver" de la bandeja: cierra las escalaciones abiertas, reactiva el bot, archiva la conversación y la mueve a la columna de resolución del embudo si hay una. Idempotente (`changed: false` si ya estaba resuelta). El próximo mensaje del cliente la vuelve a abrir. Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 429 ## Contactos ### GET /v1/contacts — Listar contactos (scope: contacts:read) operationId: listContacts Contactos del workspace, paginados por cursor. `q` busca por nombre, correo o teléfono. Parámetros: - cursor (query)?: string - limit (query)?: integer - q (query)?: string Respuestas: 200, 400, 401, 403, 429 ### POST /v1/contacts — Crear un contacto (scope: contacts:write) operationId: createContact Crea un contacto. Necesita al menos un dato que lo identifique (teléfono, correo o nombre). El teléfono se normaliza a E.164. Cuerpo JSON: - name?: string - email?: string - phone?: string - phone_country?: string - avatar_url?: string - external_ids?: object - tags?: string[] - metadata?: object - birth_month?: integer - birth_day?: integer - birth_year?: integer Respuestas: 201, 400, 401, 403, 409, 422, 429 ## Listas de contactos ### GET /v1/contact-lists — Listar listas de contactos (scope: contacts:read) operationId: listContactLists Respuestas: 200, 401, 403, 429 ### POST /v1/contact-lists/{id}/members — Agregar contactos a una lista (scope: contacts:write) operationId: addContactListMembers Agrega contactos a una lista sin pasar por una planilla. Envía exactamente uno de `rows` (filas crudas, con el mismo mapeo de columnas del import) o `members` (contactos estructurados). Con `preview_only: true` devuelve el resumen sin guardar. Si una campaña activa usa la lista, los contactos nuevos reciben su mensaje. Parámetros: - id (path): string Cuerpo JSON: - rows?: object[] - members?: object[] - phone: string - first_name?: string - last_name?: string - email?: string - variables?: object - column_mapping?: object - preview_only?: boolean Respuestas: 200, 400, 401, 402, 403, 404, 422, 429 ## Campañas ### POST /v1/campaigns — Crear una campaña (scope: campaigns:write) operationId: createCampaign Crea una campaña. Con `trigger_type: "api"` y `trigger: true` la corrida se dispara en la misma solicitud. Parámetros: - Idempotency-Key (header)?: string Cuerpo JSON: - name: string - channel: "instagram" | "facebook" | "whatsapp_cloud" | "whatsapp_baileys" | "gmail" | "sms" | "shopify" - trigger_type: "scheduled" | "event" | "api" - message_mode: "template" | "free_text" | "ai_generated" | "email_template" | "saved_message" - trigger_config?: object - audience_source: object - kind: "contact_list" | "custom_tool" | "internal_query" | "event" - ref?: object - message_config: object - suppression_config?: object - trigger?: boolean Respuestas: 201, 400, 401, 402, 403, 409, 422, 429 ### GET /v1/campaigns/{id} — Obtener una campaña (scope: campaigns:read) operationId: getCampaign Estado de la campaña y conteo de destinatarios por estado. Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 429 ## Eventos ### GET /v1/events — Leer el stream de eventos (scope: events:read) operationId: listEvents Stream ordenado de eventos del workspace, para consumirlo por polling como complemento de los webhooks. Guarda el `next_after_seq` y pásalo en la siguiente llamada. Parámetros: - after_seq (query)?: string - limit (query)?: integer Respuestas: 200, 400, 401, 403, 429 ### POST /v1/events — Registrar un evento externo (scope: events:write) operationId: createEvent Publica un evento de tu sistema (por ejemplo, de tu ecommerce) en el stream del workspace, donde lo pueden usar campañas y flujos. `type` debe estar en la lista de eventos externos permitidos. `idempotency_key` deduplica: repetir la misma clave devuelve la respuesta original. Cuerpo JSON: - type: string - contact_ref: string - payload?: object - occurred_at?: string - idempotency_key: string Respuestas: 201, 400, 401, 403, 409, 422, 429 ### GET /v1/events/stream — Recibir los eventos en vivo (Server-Sent Events) (scope: events:read) operationId: streamEvents Los mismos eventos y el mismo sobre que los webhooks (`{ id, tenant_id, event, ts, data }`), en vivo por Server-Sent Events: lo que usa `heysteff listen`. Al abrir llega `retry: 3000` y un `event: ready` con `{ tenant_id, mode }`; después, por evento, `id`, `event` (el nombre) y `data` (el sobre en JSON). Un comentario `: ping` cada 25 s. El servidor corta a la hora: reconecta con la cabecera `Last-Event-ID` y recibes lo que te perdiste (se guardan hasta 1.000 eventos durante 10 minutos mientras alguien escucha). Una key `sk_test_` entra con `conversations:read` y solo recibe los eventos de las conversaciones del sandbox. Parámetros: - events (query)?: string Respuestas: 200, 401, 403, 422, 429 ## Conversiones ### POST /v1/conversions — Reportar una conversión (scope: conversions:write) operationId: createConversion Reporta una acción de valor (compra, reserva, registro) para atribuirla a la campaña que la generó. `value` va en unidades menores (centavos), entero. `external_ref` es el id de la orden en tu sistema y deduplica: repetirlo devuelve la respuesta original. Cuerpo JSON: - contact_ref: string - type: "purchase" | "booking" | "signup" | "lead" | "custom" - value?: integer - currency?: string - occurred_at?: string - external_ref: string Respuestas: 201, 400, 401, 403, 409, 429 ## Reseñas ### GET /v1/reviews — Listar reseñas (scope: reviews:read) operationId: listReviews Reseñas de productos del workspace, de la más reciente a la más antigua, paginadas por `limit` y `offset`. Parámetros: - status (query)?: "pending" | "approved" | "rejected" | "archived" - product_id (query)?: string - min_rating (query)?: integer - limit (query)?: integer - offset (query)?: integer Respuestas: 200, 400, 401, 403, 429 ### POST /v1/reviews — Pedir una reseña (scope: reviews:write) operationId: createReviewRequest Le manda a un contacto la solicitud de reseña de un pedido y deja abierta la captura de su respuesta. `external_ref` es la referencia del pedido en tu sistema: si ya hay una solicitud abierta o una reseña guardada para ese pedido, responde 200 sin reenviar. Cuerpo JSON: - contact_ref: string - external_ref: string - contact_name?: string - product?: string - channel?: string Respuestas: 200, 201, 400, 401, 402, 403, 422, 429 ## Cobros ### POST /v1/charges — Cobrarle a un contacto (scope: charges:write) operationId: createCharge Genera un link de pago con el proveedor de cobros del workspace y se lo manda al contacto por el canal indicado. `amount` va en unidades menores (centavos), entero positivo. El header `Idempotency-Key` es obligatorio. Fuera de la ventana de 24 h de WhatsApp Cloud usa `template_name`. Parámetros: - Idempotency-Key (header): string Cuerpo JSON: - contact_ref: string - channel: "instagram" | "facebook" | "whatsapp_cloud" | "whatsapp_baileys" | "gmail" | "sms" | "shopify" - amount: integer - currency: string - description: string - metadata?: object - template_name?: string - template_language?: string - template_components?: object[] Respuestas: 201, 400, 401, 402, 403, 409, 422, 429 ### POST /v1/refunds — Reembolsar un pago (scope: refunds:write) operationId: createRefund Reembolsa un link de pago pagado, total o parcial. Sin `amount` el reembolso es total (el saldo restante). Todo reembolso queda auditado y, por defecto, espera la aprobación de una persona. El header `Idempotency-Key` es obligatorio. Parámetros: - Idempotency-Key (header): string Cuerpo JSON: - payment_link_id: string - amount?: integer - reason?: string Respuestas: 201, 400, 401, 403, 404, 409, 422, 429 ### GET /v1/sales/funnel — Embudo de ventas (scope: sales:read) operationId: getSalesFunnel Ventas por etapa, ingresos pagados y reembolsos por moneda en la ventana `[since, until)` sobre la fecha de creación de cada venta. Montos en unidades menores. Parámetros: - since (query)?: string - until (query)?: string Respuestas: 200, 400, 401, 403, 429 ## Flujos ### POST /v1/automation-flows/{id}/trigger — Disparar un flujo (scope: automations:trigger) operationId: triggerAutomationFlow Arranca una corrida de un flujo activo para un teléfono. Los campos extra del cuerpo quedan como variables de la corrida (hasta 32 KB). La corrida se deduplica por `dedup_key`, el header `Idempotency-Key` o, si no mandas ninguno, por flujo + teléfono + `order_number`. Parámetros: - id (path): string - Idempotency-Key (header)?: string Cuerpo JSON: - phone: string - order_number?: string - dedup_key?: string Respuestas: 200, 202, 400, 401, 403, 404, 409, 429 ## Espacio personal ### GET /v1/owner/sleep — Estado del modo sueño (scope: owner:write) operationId: getOwnerSleep Si el dueño de un espacio personal está durmiendo. Mientras duerme, lo que el agente iba a mandar queda como sugerencia. Respuestas: 200, 401, 403, 422, 429 ### POST /v1/owner/sleep — Avisar que el dueño se durmió o despertó (scope: owner:write) operationId: setOwnerSleep Pensado para un Atajo de iOS con una automatización de Enfoque: `{"state":"asleep"}` al activar «Dormir» y `{"state":"awake"}` al desactivarlo. El aviso vale 12 horas. Cuerpo JSON: - state: "asleep" | "awake" Respuestas: 200, 400, 401, 403, 422, 429 ## Webhooks ### GET /v1/webhooks — Listar webhooks (scope: webhooks:read) operationId: listWebhooks Los webhooks salientes del workspace, con sus eventos y cómo les va (último 2xx, último fallo, fallos seguidos). Se crean y editan con POST/PATCH/DELETE /v1/webhooks, en Configuración → Desarrolladores o por MCP. Respuestas: 200, 401, 403, 429 ### POST /v1/webhooks — Crear un webhook (scope: webhooks:write) operationId: createWebhook Crea un webhook saliente: `url` (https) y `events` (vacío = todos los eventos). Opcional: `batch` para agrupar los `message:received` rápidos de una conversación en un solo POST (por defecto 5 s y hasta 50 eventos) e `inactivity_hours` para `conversation.inactive` (por defecto 24). La respuesta trae el secreto de firma en `secret` UNA sola vez: guárdalo para verificar `X-Heysteff-Signature`. Hasta 20 por hora por workspace. Cuerpo JSON: - url: string - events?: string[] - format?: "heysteff" | "meta" - batch?: object - wait_seconds?: integer - max_size?: integer - inactivity_hours?: integer Respuestas: 201, 400, 401, 402, 403, 429 ### PATCH /v1/webhooks/{id} — Editar un webhook (scope: webhooks:write) operationId: updateWebhook Cambia los eventos (`events`), lo pausa y reactiva (`is_active`), la agrupación (`batch`, `null` la apaga) o las horas de inactividad (`inactivity_hours`). Reactivar un webhook que se pausó solo manda lo que quedó en cola. La URL y el secreto no cambian: para otra URL crea un webhook nuevo y borra este. Parámetros: - id (path): string Cuerpo JSON: - events?: string[] - is_active?: boolean - format?: "heysteff" | "meta" - batch?: object - wait_seconds?: integer - max_size?: integer - inactivity_hours?: integer Respuestas: 200, 400, 401, 403, 404, 429 ### DELETE /v1/webhooks/{id} — Borrar un webhook (scope: webhooks:write) operationId: deleteWebhook Borra el webhook: deja de recibir eventos. Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 429 ### GET /v1/webhooks/{id}/deliveries — Registro de entregas de un webhook (scope: webhooks:read) operationId: listWebhookDeliveries Entregas de un webhook, de la más nueva a la más vieja, paginadas por cursor. Cada una dice qué evento era, cómo le fue al último intento (status HTTP, duración, motivo del fallo) y un extracto de lo que respondió tu servidor. Una entrega que falla se reintenta sola con espera creciente (1, 2, 4, 8 y 16 minutos) hasta 6 intentos. Parámetros: - id (path): string - status (query)?: "succeeded" | "failed" | "pending" - event (query)?: string - since (query)?: string - until (query)?: string - cursor (query)?: string - limit (query)?: integer Respuestas: 200, 400, 401, 403, 404, 429 ### POST /v1/webhook-deliveries/{id}/retry — Reintentar una entrega (scope: webhooks:write) operationId: retryWebhookDelivery Vuelve a mandar ahora una entrega que falló (`state` = `failed` o `retrying`): mismo cuerpo y mismo `X-Heysteff-Delivery-Id`, con firma nueva. No reinicia el contador de intentos. Repetir la llamada mientras la entrega espera su turno no la duplica. Hasta 120 reintentos por hora por workspace. Parámetros: - id (path): string Respuestas: 202, 400, 401, 402, 403, 404, 409, 429 ## Números de WhatsApp ### GET /v1/numbers — Listar los números de heysteff (scope: numbers:read) operationId: listNumbers Los números de WhatsApp que heysteff le dio al workspace (conectados y la reserva viva), con el estado de su nombre visible, el cupo del plan y si se puede pedir otro. Respuestas: 200, 401, 403, 429 ### POST /v1/numbers — Reservar un número y obtener el link de conexión (scope: numbers:write) operationId: reserveNumber Reserva un número verificado del pool de heysteff por 30 minutos y devuelve `connect_url`. Meta solo deja un número en la cuenta de WhatsApp de un negocio dentro de su Embedded Signup: alguien tiene que abrir `connect_url` en un navegador, entrar con Facebook y elegir el número. La API reserva el número y arma ese link; no puede saltarse el paso de Meta. Si ya hay una reserva viva se reusa: el plazo se renueva y el link es nuevo (responde 200 en vez de 201). El número cuenta contra `provisioned_numbers` del plan. Los mensajes los paga tu cuenta de WhatsApp a Meta. Respuestas: 200, 201, 401, 402, 403, 409, 429 ### GET /v1/numbers/{id} — Ver un número (scope: numbers:read) operationId: getNumber Un número de heysteff del workspace. Una reserva vencida ya no aparece (404). Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 429 ### DELETE /v1/numbers/{id} — Soltar una reserva (scope: numbers:write) operationId: releaseNumber Devuelve al pool una reserva que nadie terminó de conectar. Un número ya conectado no se suelta por API: está en tu cuenta de WhatsApp y heysteff lo sigue pagando mientras exista el canal (409 `wa_pool_number_connected`). Tampoco una reserva retenida porque un alta terminó en Meta sin confirmar qué número quedó (409 `wa_pool_number_on_hold`). Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 409, 429 ### POST /v1/numbers/{id}/display-name — Pedir un nombre visible (scope: numbers:write) operationId: requestNumberDisplayName Pide a Meta un nombre visible nuevo (el nombre del negocio que ven tus clientes) para un número conectado. Meta lo revisa y el resultado llega en `display_name.status`. De 3 a 50 caracteres, sin emojis ni links. Al conectar un número, heysteff ya pide el nombre de tu negocio. Parámetros: - id (path): string Cuerpo JSON: - display_name: string Respuestas: 202, 400, 401, 403, 404, 409, 424, 429 ### GET /v1/whatsapp-numbers/{id} — Salud y verificación de un número (scope: numbers:read) operationId: getWhatsAppNumberStatus Un número de WhatsApp Cloud del workspace (cualquiera, no solo los de heysteff), en vivo desde Meta: calidad (`quality_rating`), límite de mensajería, estado del nombre visible y verificación de la empresa con el link para verificarla (`verify_url`). Si una parte falla, el resto igual responde y el motivo queda en `errors`. Solo con una key `sk_live_`: una key de prueba responde 403 `test_key_not_allowed` (el número compartido del sandbox no es tuyo). Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 429 ### GET /v1/whatsapp-numbers/{id}/business-profile — Ver el perfil de empresa (scope: numbers:read) operationId: getWhatsAppBusinessProfile El perfil de empresa que ven tus clientes en WhatsApp: info, descripción, dirección, correo, sitios web, rubro y foto. Solo con una key `sk_live_`: una key de prueba responde 403 `test_key_not_allowed` (el número compartido del sandbox no es tuyo). Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 424, 429 ### PATCH /v1/whatsapp-numbers/{id}/business-profile — Cambiar el perfil de empresa (scope: numbers:write) operationId: updateWhatsAppBusinessProfile Cambia los campos que mandes (al menos uno). La foto va en `profile_picture_url`: una URL https pública de un JPG o PNG (cuadrado, 192×192 px o más, hasta 5 MB) o la `url` de POST /v1/media; heysteff la descarga y la sube a Meta. Responde el perfil actualizado. El nombre visible no se cambia aquí: POST /v1/numbers/{id}/display-name (números de heysteff) o WhatsApp Manager. Solo con una key `sk_live_`: una key de prueba responde 403 `test_key_not_allowed` (el número compartido del sandbox no es tuyo). Parámetros: - id (path): string Cuerpo JSON: - about?: string - address?: string - description?: string - email?: object - variante 1: string - variante 2: "" - websites?: string[] - vertical?: "UNDEFINED" | "OTHER" | "AUTO" | "BEAUTY" | "APPAREL" | "EDU" | "ENTERTAIN" | "EVENT_PLAN" | "FINANCE" | "GROCERY" | "GOVT" | "HOTEL" | "HEALTH" | "NONPROFIT" | "PROF_SERVICES" | "RETAIL" | "TRAVEL" | "RESTAURANT" | "ALCOHOL" | "ONLINE_GAMBLING" | "PHYSICAL_GAMBLING" | "OTC_DRUGS" | "NOT_A_BIZ" - profile_picture_url?: string Respuestas: 200, 400, 401, 403, 404, 413, 422, 424, 429 ## Clientes y links de conexión ### GET /v1/customers — Listar clientes (scope: customers:read) operationId: listCustomers Los clientes del workspace, del más nuevo al más viejo. Pagina con `limit` y `next_cursor`. Parámetros: - external_id (query)?: string - q (query)?: string - limit (query)?: integer - cursor (query)?: string Respuestas: 200, 400, 401, 403, 429 ### POST /v1/customers — Crear un cliente (scope: customers:write) operationId: createCustomer Crea un cliente: el cliente final de tu SaaS que va a conectar sus propios canales. Developer Free admite 1 cliente, Developer 10 y Developer Platform ilimitados (402 `plan_limit` al pasarse). Cuerpo JSON: - external_id: string - name: string - metadata?: object Respuestas: 201, 400, 401, 402, 403, 409, 429 ### GET /v1/customers/{id} — Ver un cliente (scope: customers:read) operationId: getCustomer Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 429 ### PATCH /v1/customers/{id} — Editar un cliente (scope: customers:write) operationId: updateCustomer Cambia `name`, `external_id` o `metadata` (manda al menos uno). Parámetros: - id (path): string Cuerpo JSON: - external_id?: string - name?: string - metadata?: object Respuestas: 200, 400, 401, 403, 404, 409, 429 ### DELETE /v1/customers/{id} — Borrar un cliente (scope: customers:write) operationId: deleteCustomer Borra el cliente y sus links. Sus canales siguen conectados en el workspace, sin cliente: si ya no los usas, desconéctalos aparte. Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 429 ### GET /v1/customers/{id}/setup-links — Links de un cliente (scope: customers:read) operationId: listCustomerSetupLinks Los últimos 50 links del cliente con su estado (`active`, `used`, `expired`, `revoked`). Sin la URL: solo existe al crear el link. Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 429 ### POST /v1/customers/{id}/setup-links — Crear un link de conexión (scope: customers:write) operationId: createCustomerSetupLink Devuelve `url`, una página de heysteff donde tu cliente conecta UNO de sus canales (WhatsApp por el Embedded Signup de Meta, Instagram con o sin página de Facebook, Messenger o Gmail) a tu workspace, sin cuenta de heysteff. El link vence a las `expires_in_hours` (720 por defecto, máximo 2160) y queda usado al conectar un canal. Un solo link activo por cliente y canal: crear uno revoca los anteriores que permiten alguno de los mismos canales. Configurable: idioma (`locale`), colores (`theme`), logo y nombre, cómo conectar WhatsApp (`whatsapp_modes`: número propio, coexistencia con la app del celular o un número de heysteff) y `reconnect_channel_id` para volver a conectar un canal caído del cliente. Al terminar, la página vuelve a `success_redirect_url` con `?status=connected&channel_id=…&customer_id=…&channel_type=…` (o a `failure_redirect_url` con `?status=error&error=…`) y tu webhook recibe `customer.channel_connected`. La URL solo viene en esta respuesta. Parámetros: - id (path): string Cuerpo JSON: - allowed_channels?: "whatsapp_cloud" | "instagram" | "facebook" | "gmail"[] - expires_in_hours?: integer - success_redirect_url?: string - failure_redirect_url?: string - brand_name?: string - brand_logo_url?: string - locale?: "auto" | "es" | "en" | "pt" - theme?: object - primary_color?: string - background_color?: string - text_color?: string - border_color?: string - whatsapp_modes?: "own_number" | "coexistence" | "heysteff_number"[] - reconnect_channel_id?: string Respuestas: 201, 400, 401, 402, 403, 404, 422, 429 ### GET /v1/setup-links — Links de conexión del workspace (scope: customers:read) operationId: listSetupLinks Todos los links del workspace, del más nuevo al más viejo, con su estado y el cliente. Filtra por `status` y `customer_id`. Parámetros: - status (query)?: "active" | "used" | "expired" | "revoked" - customer_id (query)?: string - limit (query)?: integer Respuestas: 200, 400, 401, 403, 429 ### POST /v1/setup-links/{id}/revoke — Revocar un link de conexión (scope: customers:write) operationId: revokeSetupLink La página del link deja de funcionar. Revocar dos veces no hace nada; un link ya usado responde 409 `setup_link_used`. Parámetros: - id (path): string Respuestas: 200, 401, 403, 404, 409, 429 ### GET /v1/customers/{id}/usage — Uso de un cliente (scope: customers:read) operationId: getCustomerUsage Mensajes (totales, entrantes, salientes y fallidos) y conversaciones de los canales que conectó el cliente, en total, por canal y por día (UTC). Sin fechas, los últimos 30 días; el rango puede abarcar hasta 366 días. Sirve para cobrarle a tus clientes. Parámetros: - id (path): string - from (query)?: string - to (query)?: string Respuestas: 200, 400, 401, 403, 404, 429 ### GET /v1/customers/{id}/channels — Canales de un cliente (scope: customers:read) operationId: listCustomerChannels Los canales que conectó el cliente con sus links, activos y desconectados. Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 429 ## WhatsApp Flows ### GET /v1/whatsapp-flows — Listar los WhatsApp Flows (scope: flows:read) operationId: listWhatsAppFlows Los Flows de las cuentas de WhatsApp Cloud del espacio, del más reciente al más viejo (hasta 500), desde el espejo de heysteff. Si creaste Flows en WhatsApp Manager, primero POST /v1/whatsapp-flows/sync. Solo WhatsApp Cloud. Solo con una key `sk_live_`: una key de prueba responde 403 `test_key_not_allowed` (el número compartido del sandbox no tiene Flows propios). Parámetros: - channel_connection_id (query)?: string - status (query)?: string Respuestas: 200, 400, 401, 403, 429 ### POST /v1/whatsapp-flows — Crear un Flow (scope: flows:write) operationId: createWhatsAppFlow Crea un Flow en borrador en la cuenta de WhatsApp del número (`channel_connection_id`, o el número de WhatsApp Cloud conectado más reciente). `flow_json` es opcional (objeto o texto, hasta 1 MB): se puede subir después con PUT /v1/whatsapp-flows/{id}/json. Con `publish: true` Meta lo publica al crearlo si el Flow JSON no tiene errores. Responde 201 con el Flow y sus `validation_errors`. `save_to_contact: true` guarda cada campo simple de las respuestas en el campo del contacto con la misma clave. Solo con una key `sk_live_`: una key de prueba responde 403 `test_key_not_allowed` (el número compartido del sandbox no tiene Flows propios). Cuerpo JSON: - channel_connection_id?: string - name: string - categories: "SIGN_UP" | "SIGN_IN" | "APPOINTMENT_BOOKING" | "LEAD_GENERATION" | "CONTACT_US" | "CUSTOMER_SUPPORT" | "SURVEY" | "OTHER"[] - flow_json?: object - variante 1: string - variante 2: object - publish?: boolean - save_to_contact?: boolean Respuestas: 201, 400, 401, 403, 404, 422, 424, 429 ### POST /v1/whatsapp-flows/sync — Sincronizar los Flows desde Meta (scope: flows:write) operationId: syncWhatsAppFlows Trae de Meta los Flows de la cuenta de WhatsApp de un número (`channel_connection_id`, o el más reciente), los crea o actualiza en heysteff y borra del espejo los que ya no existen en esa cuenta. Úsalo después de crear o editar Flows en WhatsApp Manager. Solo con una key `sk_live_`: una key de prueba responde 403 `test_key_not_allowed` (el número compartido del sandbox no tiene Flows propios). Cuerpo JSON: - channel_connection_id?: string Respuestas: 200, 400, 401, 403, 404, 422, 424, 429 ### GET /v1/whatsapp-flows/responses — Listar las respuestas de los Flows (scope: flows:read) operationId: listWhatsAppFlowResponses Las respuestas que enviaron los clientes, la más nueva primero. `flow_id` (el id heysteff del Flow) filtra; para la página siguiente, manda `before` con el `next_before` de la respuesta (null = no hay más). Cada respuesta también llega al momento en el webhook `message:received` con `flow_response`. Solo con una key `sk_live_`: una key de prueba responde 403 `test_key_not_allowed` (el número compartido del sandbox no tiene Flows propios). Parámetros: - flow_id (query)?: string - limit (query)?: integer - before (query)?: string Respuestas: 200, 400, 401, 403, 429 ### GET /v1/whatsapp-flows/{id} — Ver un Flow (scope: flows:read) operationId: getWhatsAppFlow Un Flow con su estado, sus errores de validación y el último Flow JSON subido desde heysteff (texto). Solo con una key `sk_live_`: una key de prueba responde 403 `test_key_not_allowed` (el número compartido del sandbox no tiene Flows propios). Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 429 ### PATCH /v1/whatsapp-flows/{id} — Cambiar un Flow (scope: flows:write) operationId: updateWhatsAppFlow Cambia el nombre o las categorías (en Meta) y `save_to_contact` (en heysteff). Para cambiar las pantallas, sube otro Flow JSON con PUT /v1/whatsapp-flows/{id}/json. Solo con una key `sk_live_`: una key de prueba responde 403 `test_key_not_allowed` (el número compartido del sandbox no tiene Flows propios). Parámetros: - id (path): string Cuerpo JSON: - name?: string - categories?: "SIGN_UP" | "SIGN_IN" | "APPOINTMENT_BOOKING" | "LEAD_GENERATION" | "CONTACT_US" | "CUSTOMER_SUPPORT" | "SURVEY" | "OTHER"[] - save_to_contact?: boolean Respuestas: 200, 400, 401, 403, 404, 424, 429 ### DELETE /v1/whatsapp-flows/{id} — Borrar un borrador (scope: flows:write) operationId: deleteWhatsAppFlow Borra un Flow en borrador en Meta y en heysteff. Meta solo borra borradores: uno publicado responde 409 `flow_not_draft` (deprécalo con POST /v1/whatsapp-flows/{id}/deprecate). Solo con una key `sk_live_`: una key de prueba responde 403 `test_key_not_allowed` (el número compartido del sandbox no tiene Flows propios). Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 409, 424, 429 ### GET /v1/whatsapp-flows/{id}/json — Descargar el Flow JSON (scope: flows:read) operationId: getWhatsAppFlowJson El Flow JSON como objeto: el último subido desde heysteff o, si no hay (Flow creado en WhatsApp Manager), el que tiene Meta. Solo con una key `sk_live_`: una key de prueba responde 403 `test_key_not_allowed` (el número compartido del sandbox no tiene Flows propios). Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 424, 429 ### PUT /v1/whatsapp-flows/{id}/json — Subir el Flow JSON (scope: flows:write) operationId: uploadWhatsAppFlowJson Sube (reemplaza) el Flow JSON de un borrador: objeto o texto con un objeto JSON, hasta 1 MB. Responde el Flow con los `validation_errors` de Meta; vacío = listo para publicar. Un Flow publicado no cambia (409 `flow_not_draft`): crea otro. Solo con una key `sk_live_`: una key de prueba responde 403 `test_key_not_allowed` (el número compartido del sandbox no tiene Flows propios). Parámetros: - id (path): string Cuerpo JSON: - flow_json: object - variante 1: string - variante 2: object Respuestas: 200, 400, 401, 403, 404, 409, 422, 424, 429 ### POST /v1/whatsapp-flows/{id}/publish — Publicar un Flow (scope: flows:write) operationId: publishWhatsAppFlow Publica un borrador: desde ahí se puede mandar a cualquier cliente y ya no cambia. Meta lo rechaza si el Flow JSON tiene errores (424 `meta_flow_error`, revisa `validation_errors` con GET /v1/whatsapp-flows/{id}). Solo con una key `sk_live_`: una key de prueba responde 403 `test_key_not_allowed` (el número compartido del sandbox no tiene Flows propios). Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 409, 424, 429 ### POST /v1/whatsapp-flows/{id}/deprecate — Deprecar un Flow (scope: flows:write) operationId: deprecateWhatsAppFlow Deja de usar un Flow publicado: ya no se puede mandar ni abrir. No se puede deshacer. Solo con una key `sk_live_`: una key de prueba responde 403 `test_key_not_allowed` (el número compartido del sandbox no tiene Flows propios). Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 409, 424, 429 ### GET /v1/whatsapp-flows/{id}/preview — Link de vista previa (scope: flows:read) operationId: getWhatsAppFlowPreview El link de vista previa de Meta: abre el formulario en el navegador para probarlo sin mandarlo. Vence a los 30 días; `invalidate=true` pide uno nuevo. Solo con una key `sk_live_`: una key de prueba responde 403 `test_key_not_allowed` (el número compartido del sandbox no tiene Flows propios). Parámetros: - id (path): string - invalidate (query)?: "true" | "false" Respuestas: 200, 400, 401, 403, 404, 424, 429 ## Llamadas de WhatsApp ### GET /v1/calls — Registro de llamadas (scope: calls:read) operationId: listCalls Las llamadas de WhatsApp de tus números de WhatsApp Cloud, la más nueva primero. Filtros: `channel_connection_id`, `status` y `direction`; para la página siguiente, manda `before` con el `next_before` de la respuesta (null = no hay más). Cada llamada también deja una nota en la conversación y avisa por los webhooks `call.started`, `call.ended` y `call.missed`. heysteff registra las llamadas pero no las contesta: no hay audio en esta versión. Solo con una key `sk_live_`: una key de prueba responde 403 `test_key_not_allowed` (el número compartido del sandbox no es tuyo). Parámetros: - channel_connection_id (query)?: string - status (query)?: "ringing" | "answered" | "completed" | "missed" | "rejected" | "failed" - direction (query)?: "inbound" | "outbound" - limit (query)?: integer - before (query)?: string Respuestas: 200, 400, 401, 403, 429 ### POST /v1/calls — Que el agente IA llame a un cliente (scope: calls:write) operationId: startAgentCall El agente IA llama al cliente de una conversación de WhatsApp Cloud y habla con la voz, el conocimiento y las herramientas del espacio; la llamada queda en la conversación con su transcripción y se cobra por minuto igual que una entrante. Exige las llamadas del agente encendidas (módulo Llamadas). `reason` es lo que el agente dice al saludar ("te llamo porque…"); `at` (ISO 8601 con zona, hasta 30 días) la programa. WhatsApp exige el permiso del cliente: si no lo dio, le llega un mensaje para permitir la llamada (solo dentro de la ventana de 24 h) y la llamada sale sola cuando acepta. `action`: `calling`, `scheduled`, `permission_requested` o `already_pending` (ya había una pendiente). Errores: 409 `voice_outbound_unavailable` o `voice_call_permission_window_closed`, 422 `voice_call_whatsapp_cloud_only`, 429 `voice_call_permission_limit` o `voice_call_limit` (topes de WhatsApp, con `details.until`), 424 `voice_permission_check_failed`. Solo con una key `sk_live_`: una key de prueba responde 403 `test_key_not_allowed` (el número compartido del sandbox no es tuyo). Cuerpo JSON: - conversation_id: string - reason: string - at?: string Respuestas: 201, 400, 401, 403, 429 ### GET /v1/whatsapp-numbers/{id}/calling — Ver los ajustes de llamadas (scope: calls:read) operationId: getWhatsAppCallingSettings Los ajustes de llamadas del número, leídos en vivo de Meta: si están encendidas, el botón de llamar, el permiso de devolución y el horario. Solo con una key `sk_live_`: una key de prueba responde 403 `test_key_not_allowed` (el número compartido del sandbox no es tuyo). Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 424, 429 ### PATCH /v1/whatsapp-numbers/{id}/calling — Encender las llamadas y cambiar su horario (scope: calls:write) operationId: updateWhatsAppCallingSettings Enciende o apaga las llamadas del número (`enabled`) y cambia el botón de llamar del chat (`call_icon_visible`), el permiso de devolución de llamada (`callback_permission`) y el horario (`call_hours` con `timezone_id`, `weekly` de hasta 2 tramos por día en HHMM y `holidays`; `null` lo quita). Lo que no mandes queda como está en Meta. Meta decide si el número es elegible (424 `meta_error` si no). Para recibir los eventos, la app de Meta tiene que tener suscrito el campo de webhook `calls`. Solo con una key `sk_live_`: una key de prueba responde 403 `test_key_not_allowed` (el número compartido del sandbox no es tuyo). Parámetros: - id (path): string Cuerpo JSON: - enabled: boolean - call_icon_visible?: boolean - callback_permission?: boolean - call_hours?: object - timezone_id: string - weekly: object[] - day: "MONDAY" | "TUESDAY" | "WEDNESDAY" | "THURSDAY" | "FRIDAY" | "SATURDAY" | "SUNDAY" - open: string - close: string - holidays?: object[] - date: string - start: string - end: string Respuestas: 200, 400, 401, 403, 404, 424, 429 ## Agente de IA ### GET /v1/agent — Ver el agente de IA (scope: agent:read) operationId: getAgent Encendido, instrucciones, proveedor, modelo, temperatura, keys propias (solo los últimos 4), cupo de prueba del mes, interruptores por canal y por cliente y los modelos sugeridos con su precio. Respuestas: 200, 401, 403, 429 ### PATCH /v1/agent — Configurar el agente de IA (scope: agent:write) operationId: updateAgent Todos los campos son opcionales (al menos uno); `provider` va siempre con `model` (422 `model_required`). `channels` y `customers` encienden (`true`), apagan (`false`) o vuelven a heredar (`null`) el agente en un canal o en todos los canales de un cliente. Las respuestas se pagan con tu key del proveedor si la guardaste (PUT /v1/agent/keys); si no, descuentan del cupo de prueba del plan (Developer Free 200, Developer 2.000, Developer Platform 20.000 al mes). Sin key ni cupo el agente no responde y llega el webhook `agent.failed` con `ai_quota_exhausted`: nunca otro proveedor. Cuerpo JSON: - enabled?: boolean - instructions?: string - provider?: "anthropic" | "openai" | "deepseek" | "openrouter" - model?: string - temperature?: number - channels?: object[] - id: string - enabled: boolean - customers?: object[] - id: string - enabled: boolean Respuestas: 200, 400, 401, 403, 404, 429 ### PUT /v1/agent/keys — Guardar tu key del proveedor (BYOK) (scope: agent:write) operationId: setAgentKey Guarda cifrada (o reemplaza) tu key de Anthropic, OpenAI, DeepSeek u OpenRouter. Nunca se devuelve: la respuesta trae el proveedor y los últimos 4. Cuerpo JSON: - provider: "anthropic" | "openai" | "deepseek" | "openrouter" - api_key: string Respuestas: 200, 400, 401, 403, 429 ### DELETE /v1/agent/keys/{provider} — Borrar tu key del proveedor (scope: agent:write) operationId: deleteAgentKey Parámetros: - provider (path): "anthropic" | "openai" | "deepseek" | "openrouter" Respuestas: 200, 400, 401, 403, 429 ### GET /v1/agent/tools — Listar custom tools (scope: agent:read) operationId: listAgentTools Las llamadas HTTP a tu backend que el agente puede hacer durante una conversación. Respuestas: 200, 401, 403, 429 ### POST /v1/agent/tools — Crear una custom tool (scope: agent:write) operationId: createAgentTool La auth hacia tu backend (bearer, HMAC) se configura en la consola: por la API no viajan secretos de terceros. Cupo del plan: Developer Free 2, Developer 10, Developer Platform ilimitadas. Cuerpo JSON: - slug: string - name: string - description: string - mode?: "simple" | "advanced" - spec: object - method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" - url_template: string - headers?: object - query_template?: object - body_template?: object - parameters_schema: object - type: "object" - properties: object - required?: string[] - additionalProperties?: boolean - response_path?: string - timeout_ms?: integer - allow_hosts?: string[] - auth_kind?: "none" | "bearer" | "header" | "basic" - product_catalog?: boolean Respuestas: 201, 400, 401, 402, 403, 429 ### GET /v1/agent/tools/{id} — Ver una custom tool (scope: agent:read) operationId: getAgentTool Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 429 ### PATCH /v1/agent/tools/{id} — Editar una custom tool (scope: agent:write) operationId: updateAgentTool `spec` se reemplaza completa: manda la spec entera con tu cambio. Parámetros: - id (path): string Cuerpo JSON: - name?: string - description?: string - mode?: "simple" | "advanced" - spec?: object - method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" - url_template: string - headers?: object - query_template?: object - body_template?: object - parameters_schema: object - type: "object" - properties: object - required?: string[] - additionalProperties?: boolean - response_path?: string - timeout_ms?: integer - allow_hosts?: string[] - auth_kind?: "none" | "bearer" | "header" | "basic" - product_catalog?: boolean - is_active?: boolean Respuestas: 200, 400, 401, 403, 404, 429 ### DELETE /v1/agent/tools/{id} — Borrar una custom tool (scope: agent:write) operationId: deleteAgentTool Parámetros: - id (path): string Respuestas: 200, 400, 401, 403, 404, 429 ### GET /v1/insights — Hallazgos de tus conversaciones (scope: insights:read) operationId: getInsights De la analítica de conversaciones de los últimos `days` días (30 por defecto): clientes frustrados (`frustrated`), lo que el agente no supo responder o respondió mal (`agent_gaps`) y conversaciones sin resolver (`unresolved`). `included=false` si el plan no trae analítica (Developer Free). Cuando un análisis encuentra algo llega el webhook `insight.detected`. Parámetros: - days (query)?: integer - limit (query)?: integer Respuestas: 200, 400, 401, 403, 429 ## Bandeja embebible ### GET /v1/inbox-embeds — Listar tokens de la bandeja embebible (scope: inbox:embed) operationId: listInboxEmbeds Los tokens del workspace con su estado. El token en sí nunca se lista. Respuestas: 200, 401, 403, 429 ### POST /v1/inbox-embeds — Crear un token de la bandeja embebible (scope: inbox:embed) operationId: createInboxEmbed Crea un token para meter la bandeja de heysteff (WhatsApp, Instagram, Messenger, SMS y Gmail) en tu producto con un iframe a `developers.heysteff.com/embed/inbox?token=…`: lista de conversaciones, hilo, responder (texto, archivos y plantillas de WhatsApp con la ventana cerrada), marcar leído y tomar o soltar el bot, en tiempo real. La vista nunca muestra nada fuera del alcance. Filtros por la URL del iframe: `status`, `search`, `unread`, `channel`. Una key de prueba no puede crear tokens. Cuerpo JSON: - name: string - scope?: "all" | "channel" | "customer" - channel_connection_id?: string - customer_id?: string - allowed_origins: string[] - theme?: "system" | "light" | "dark" - locale?: "auto" | "es" | "en" | "pt" - expires_in_seconds?: integer Respuestas: 201, 400, 401, 403, 409, 422, 429 ### GET /v1/inbox-embeds/{id} — Ver un token de la bandeja embebible (scope: inbox:embed) operationId: getInboxEmbed Parámetros: - id (path): string Respuestas: 200, 401, 403, 404, 429 ### PATCH /v1/inbox-embeds/{id} — Editar un token de la bandeja embebible (scope: inbox:embed) operationId: updateInboxEmbed Cambia `name`, `allowed_origins`, `theme` o `locale`. El alcance no cambia: para otro alcance, crea otro token. Parámetros: - id (path): string Cuerpo JSON: - name?: string - allowed_origins?: string[] - theme?: "system" | "light" | "dark" - locale?: "auto" | "es" | "en" | "pt" Respuestas: 200, 400, 401, 403, 404, 429 ### DELETE /v1/inbox-embeds/{id} — Revocar un token de la bandeja embebible (scope: inbox:embed) operationId: revokeInboxEmbed La vista deja de cargar y su tiempo real se corta. Revocar dos veces no hace nada. Parámetros: - id (path): string Respuestas: 200, 401, 403, 404, 429 ## Logs ### GET /v1/logs — Logs unificados (scope: logs:read) operationId: listLogs Llamadas a la API, entregas de webhooks y mensajes que el proveedor rechazó, en una sola línea de tiempo (de lo más nuevo a lo más viejo), con el conteo del período por fuente en `counts`. Es lo mismo que la página Logs de la consola. Las llamadas a la API se guardan 14 días. Parámetros: - sources (query)?: string - errors_only (query)?: "true" | "false" - q (query)?: string - range (query)?: "1h" | "24h" | "7d" | "14d" - since (query)?: string - until (query)?: string - before (query)?: string - before_id (query)?: string - limit (query)?: integer Respuestas: 200, 400, 401, 403, 429