---
name: heysteff
description: Integra un sistema con la API de heysteff (WhatsApp, Instagram, Messenger, SMS y correo con el mismo POST /v1/messages), su SDK de TypeScript, su CLI y su servidor MCP. Úsala cuando haya que mandar mensajes o plantillas de WhatsApp, leer conversaciones, subir archivos, crear contactos, recibir y verificar webhooks de heysteff, probarlos en local sin túnel (heysteff listen), probar con el sandbox (keys sk_test_) o conectar una IA propia que conteste por heysteff.
---

# heysteff para agentes de código

heysteff es una API de mensajería omnicanal: el mismo `POST /v1/messages` manda por WhatsApp (API oficial de Meta o por QR), Instagram, Messenger, SMS y correo. Esta skill resume cómo programar contra ella. Las fuentes de verdad, que ganan sobre esta skill si algo cambia, son:

- API completa en texto plano: https://app.heysteff.com/api/v1/llms-full.txt (léela antes de escribir código: campos, enums y scopes exactos)
- OpenAPI 3.1: https://app.heysteff.com/api/v1/openapi.json
- Docs: https://developers.heysteff.com/docs/ (inglés: https://developers.heysteff.com/en/docs/)
- Reglas cortas: https://developers.heysteff.com/AGENTS.md

No inventes endpoints: la API pública solo expone rutas bajo `https://app.heysteff.com/api/v1/`.

## Autenticación

- Cada solicitud lleva `Authorization: Bearer $HEYSTEFF_API_KEY`. La key se crea en la consola: https://developers.heysteff.com/console/ (sección API keys).
- Si trabajas en la máquina de una persona, no le pidas que cree y copie una key: corre `heysteff login`. La CLI abre su navegador en `https://developers.heysteff.com/console/device?code=…` y muestra un código en la terminal; la persona revisa que sea el mismo, elige el espacio, el modo (`sk_live_` o `sk_test_` del sandbox) y los permisos, y aprieta Autorizar. La key queda en `~/.config/heysteff/config.json` (permisos 600) y la usan los comandos `heysteff …`. Sin navegador (SSH, contenedor): `heysteff login --no-browser` y pásale la URL. En CI: `HEYSTEFF_API_KEY`. El SDK lee `HEYSTEFF_API_KEY`, no ese archivo.
- `sk_live_…` escribe a clientes reales. `sk_test_…` es del sandbox: solo manda por WhatsApp a teléfonos unidos al sandbox del espacio, sin plantillas, y solo tiene `messages:write`, `conversations:read` y `conversations:write`.
- Lee la key de la variable de entorno `HEYSTEFF_API_KEY`. Nunca la escribas en el código, en un commit, en un log, en un ejemplo ni en el frontend: da acceso al espacio completo y las llamadas van siempre desde un servidor.
- Cada operación exige un scope (`messages:write`, `contacts:read`, `webhooks:read`…). Un `403` casi siempre es un scope que falta: pídele a la persona que lo agregue a la key; no busques un rodeo.

## Endpoints clave

| Qué | Método y ruta | Scope |
| --- | --- | --- |
| Mandar texto, archivo, plantilla, botones o lista (`interactive`), ubicación (`location`) o reacción (`reaction`) | `POST /v1/messages` | `messages:write` |
| Estado de un mensaje (`queued`, `sent`, `delivered`, `read`, `failed`) | `GET /v1/messages/{id}` | `conversations:read` |
| Marcar como leído (recibo al cliente) | `POST /v1/messages/{id}/read` | `messages:write` |
| Subir un archivo (multipart, campo `file`, hasta 15 MB) | `POST /v1/media` | `messages:write` |
| Plantillas aprobadas de WhatsApp | `GET /v1/templates` | `messages:write` |
| Crear y borrar plantillas de WhatsApp | `POST /v1/templates`, `DELETE /v1/templates/{name}` | `templates:write` |
| WhatsApp Flows: listar, crear, subir el Flow JSON, publicar, deprecar, borrar, vista previa y respuestas | `/v1/whatsapp-flows…` | `flows:read` / `flows:write` |
| Mandar un Flow, un botón de llamar o pedir permiso de llamada (solo WhatsApp Cloud) | `POST /v1/messages` con `content_type` `flow`, `voice_call` o `call_permission_request` | `messages:write` |
| Registro de llamadas de WhatsApp y ajustes de llamadas de un número | `GET /v1/calls`, `GET/PATCH /v1/whatsapp-numbers/{id}/calling` | `calls:read` / `calls:write` |
| Salud, verificación y perfil de empresa de un número | `GET /v1/whatsapp-numbers/{id}`, `GET/PATCH /v1/whatsapp-numbers/{id}/business-profile` | `numbers:read` / `numbers:write` |
| Conversaciones y sus mensajes | `GET /v1/conversations`, `GET /v1/conversations/{id}`, `GET /v1/conversations/{id}/messages` | `conversations:read` |
| Pasar a una persona, pausar o reanudar el bot, resolver | `POST /v1/conversations/{id}/escalate`, `/bot`, `/resolve` | `conversations:write` |
| Contactos | `GET /v1/contacts`, `POST /v1/contacts` | `contacts:read` / `contacts:write` |
| Webhooks y sus entregas | `GET /v1/webhooks`, `GET /v1/webhooks/{id}/deliveries`, `POST /v1/webhook-deliveries/{id}/retry` | `webhooks:read` / `webhooks:write` |
| Crear, editar y borrar webhooks (el secreto se devuelve una vez) | `POST /v1/webhooks`, `PATCH /v1/webhooks/{id}`, `DELETE /v1/webhooks/{id}` | `webhooks:write` |
| Números de WhatsApp de heysteff | `GET /v1/numbers`, `POST /v1/numbers`, `DELETE /v1/numbers/{id}`, `POST /v1/numbers/{id}/display-name` | `numbers:read` / `numbers:write` |
| Clientes de tu SaaS, sus canales, su uso (para cobrarles) y los links con su estado | `GET /v1/customers`, `GET /v1/customers/{id}`, `GET /v1/customers/{id}/channels`, `GET /v1/customers/{id}/usage`, `GET /v1/setup-links` | `customers:read` |
| Crear, editar y borrar clientes; link de conexión para que conecten su propio WhatsApp, Instagram, Messenger o Gmail | `POST /v1/customers`, `PATCH`/`DELETE /v1/customers/{id}`, `POST /v1/customers/{id}/setup-links` | `customers:write` |
| Ver el agente de IA (estado, cupo de prueba, keys por sus últimos 4) y sus custom tools | `GET /v1/agent`, `GET /v1/agent/tools`, `GET /v1/agent/tools/{id}` | `agent:read` |
| Configurar el agente (encendido por espacio, canal o cliente; proveedor, modelo, instrucciones), guardar o borrar tu key del proveedor y administrar custom tools | `PATCH /v1/agent`, `PUT /v1/agent/keys`, `DELETE /v1/agent/keys/{provider}`, `POST /v1/agent/tools`, `PATCH`/`DELETE /v1/agent/tools/{id}` | `agent:write` |
| Hallazgos: clientes frustrados, lo que el agente no supo responder, sin resolver | `GET /v1/insights` | `insights:read` |
| Bandeja embebible: token del iframe con las conversaciones del espacio, de un canal o de un cliente | `POST`/`GET /v1/inbox-embeds`, `GET`/`PATCH`/`DELETE /v1/inbox-embeds/{id}` | `inbox:embed` |

Confirma el scope exacto de cada operación en `llms-full.txt`. Los webhooks y las plantillas se pueden crear en la consola o por API. Botones y listas salen nativos en WhatsApp Cloud, como quick replies en Instagram y Messenger y como texto numerado en el resto (`degraded: true`); la respuesta del cliente llega en `message:received` con `interactive_reply: { id, title }`. Botones, listas y ubicaciones exigen la ventana de 24 h (409 `wa_window_closed` / `meta_window_closed`). Una key `sk_test_` no reacciona ni gestiona webhooks o plantillas (403 `test_key_not_allowed`).

`channel` es uno de `whatsapp_cloud`, `whatsapp_baileys`, `instagram`, `facebook`, `sms` o `gmail`. `to` es el teléfono en E.164 (`+56912345678`), el correo o el id del canal. Si el espacio tiene varios números del mismo canal, pasa `channel_connection_id`. Para mandar por cuenta de uno de tus clientes (el canal que conectó con su link), pasa `customer_id`: sale por su canal activo del tipo `channel` (422 `customer_channel_not_connected` si no tiene uno).

## Ejemplos

curl:

```sh
curl https://app.heysteff.com/api/v1/messages \
  -H "Authorization: Bearer $HEYSTEFF_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pedido-1042-en-camino" \
  -d '{ "channel": "whatsapp_cloud", "to": "+56912345678", "content": "Hola Ana, tu pedido ya va en camino" }'
```

Plantilla de WhatsApp (obligatoria fuera de la ventana de 24 horas):

```json
{
  "channel": "whatsapp_cloud",
  "to": "+56912345678",
  "content_type": "template",
  "template": { "name": "pedido_despachado", "language": "es", "body_params": ["Ana", "#1042"] }
}
```

Archivo: primero `POST /v1/media` (devuelve `data.url`), después `POST /v1/messages` con `content_type` `image`, `audio`, `video` o `file` y `media: { "url": "<data.url>", "filename": "catalogo.pdf" }`. También sirve una URL https pública en `media.url`.

SDK de TypeScript (`npm install @heysteff/sdk`, Node 20+, Deno, Bun, Workers; sin `apiKey` lee `HEYSTEFF_API_KEY`):

```ts
import { Heysteff, HeysteffError } from '@heysteff/sdk';

const heysteff = new Heysteff({ apiKey: process.env.HEYSTEFF_API_KEY });
try {
  const message = await heysteff.messages.send(
    { channel: 'whatsapp_cloud', to: '+56912345678', content: 'Hola Ana' },
    { idempotencyKey: 'pedido-1042-en-camino' },
  );
  console.log(message.id, message.status);
} catch (err) {
  if (err instanceof HeysteffError && err.code === 'wa_window_closed') {
    // Fuera de la ventana de 24 horas: manda una plantilla.
  }
  throw err;
}

for await (const conversation of heysteff.conversations.listAll({ status: 'open' })) {
  console.log(conversation.id);
}
```

CLI 0.7.0 (`curl -fsSL https://developers.heysteff.com/install.sh | bash` o `npm install -g @heysteff/cli`):

```sh
heysteff login                 # la persona autoriza en el navegador; la key queda guardada
heysteff login --no-browser    # solo imprime la URL (SSH, contenedor)
heysteff setup --api-key "$HEYSTEFF_API_KEY" --no-open --skip-test   # CI: guarda y prueba la key sin preguntas
heysteff whoami
heysteff messages send --channel whatsapp_cloud --to +56912345678 --text "Hola Ana"
heysteff messages send --channel whatsapp_cloud --to +56912345678 --template pedido_despachado --param Ana --param "#1042"
heysteff messages send --channel whatsapp_cloud --to +56912345678 --text "¿Confirmas tu hora?" --buttons "si:Sí,no:No"
heysteff messages get <message_id>
heysteff messages read <message_id>
heysteff webhooks create --url https://api.tutienda.com/heysteff --event message:received --event message.read
heysteff customers create --external-id acme-42 --name "Acme SpA"
heysteff customers link <customer_id> --channels whatsapp_cloud,instagram --redirect-url https://tuapp.com/listo
heysteff messages send --channel whatsapp_cloud --customer <customer_id> --to +56912345678 --text "Hola"
heysteff conversations list --status open --json
heysteff templates list
heysteff webhooks trigger http://localhost:3000/webhooks/heysteff --secret whsec_test_123
heysteff listen                                                       # eventos del espacio en vivo
heysteff listen --forward-to http://localhost:3000/webhooks/heysteff  # reenvía cada evento firmado a tu servidor local
heysteff listen --events message:received,message.read --json         # NDJSON para scripts
```

Todo comando acepta `--json`. Códigos de salida: 0 bien, 1 error de la API o de red, 2 uso incorrecto, 3 sin key o key rechazada, 4 firma de webhook inválida.

## Escrituras e idempotencia

- Manda `Idempotency-Key` en todo POST que la acepte, con una clave por operación lógica (`pedido-1042-en-camino`), no una por intento. En `/v1/charges` y `/v1/refunds` es obligatoria.
- Reintenta solo `429`, `409 idempotency_conflict` y `5xx`, con backoff exponencial y la misma clave. Respeta `Retry-After`. Un `400`, `402` o `422` no se arregla reintentando.
- Montos en unidades menores como enteros (`1990` = 19,90). Teléfonos en E.164.

## Webhooks

Se crean en la consola (Webhooks) con una URL HTTPS; al crearlo se muestra el secreto una sola vez. Cada entrega es un POST JSON con el sobre `{ id, tenant_id, event, ts, data }` y estos headers: `X-Heysteff-Event`, `X-Heysteff-Timestamp`, `X-Heysteff-Signature`, `X-Heysteff-Delivery-Id`. Eventos frecuentes: `message:received`, `message:sent`, `message.failed`, `conversation.escalated`, `conversation.urgent`, `channel.connected` (un canal quedó conectado o reconectado en el espacio, por cualquier camino), `customer.channel_connected` (un cliente terminó de conectar un canal con su link), `webhook.test`.

Verificación de la firma:

1. Lee el cuerpo crudo, sin parsearlo ni volver a serializarlo.
2. Rechaza si `X-Heysteff-Timestamp` difiere en más de 300 segundos de tu reloj.
3. Calcula `v1=` + HMAC-SHA256 en hex con el secreto sobre `${timestamp}.${body}` y compáralo con `X-Heysteff-Signature` en tiempo constante.
4. Responde 2xx rápido, procesa después y deduplica por `X-Heysteff-Idempotency-Key` (el `id` del evento: se repite en los reintentos y es el mismo en todos tus webhooks).

Con el SDK 0.7.0, el receptor completo en una línea (verifica firma y antigüedad; responde 200, 400 si la firma no es válida o 500 si `onEvent` lanza, y heysteff reintenta):

```ts
// Next.js App Router: app/webhooks/heysteff/route.ts
import { createWebhookHandler } from '@heysteff/sdk';

export const POST = createWebhookHandler({
  secret: process.env.HEYSTEFF_WEBHOOK_SECRET!,
  onEvent: async (event) => {
    // WebhookEvent es una unión tipada por event.event
    if (event.event === 'message:received') console.log(event.data.content);
  },
});
// Hono: app.post('/webhooks/heysteff', (c) => handler(c.req.raw))
// Express: app.post('/webhooks/heysteff', express.raw({ type: 'application/json' }), handler.toExpress())
```

Para probarlo en local sin túnel ni URL pública: `heysteff listen --forward-to http://localhost:3000/webhooks/heysteff` reenvía los eventos reales del espacio con el mismo sobre y las mismas cabeceras firmadas. En local firma con un secreto `whsec_local_…` que muestra al iniciar: úsalo como `HEYSTEFF_WEBHOOK_SECRET` local, o pasa el tuyo con `--secret`. Cada línea muestra el status que respondió tu servidor (`→ 200 OK (12 ms)`). Sin la CLI, `heysteff.events.stream()` del SDK o `GET /v1/events/stream` (Server-Sent Events, scope `events:read`) dan los mismos eventos en vivo.

Si armas tu propio receptor, `verifyWebhook` hace solo la verificación:

```ts
import { verifyWebhook, WebhookVerificationError } from '@heysteff/sdk';

app.post('/webhooks/heysteff', express.raw({ type: 'application/json' }), async (req, res) => {
  try {
    const event = await verifyWebhook({ payload: req.body, headers: req.headers, secret: process.env.HEYSTEFF_WEBHOOK_SECRET! });
    res.sendStatus(200);
    // procesa event.event y event.data
  } catch (err) {
    if (err instanceof WebhookVerificationError) return res.sendStatus(400);
    throw err;
  }
});
```

Vector de prueba: secreto `whsec_test_123`, timestamp `1730000000`, cuerpo `{"event":"message:received","data":{"id":"msg_1"}}`, firma `v1=7fc6ea897401575af79e1b73b304bd864c05f1fef25dca2a0e8ffbf9b0d18ab4`.

Para contestar con tu propia IA: en `message:received`, si `data.bot_paused` es `true` no contestes (una persona o un flujo tiene la conversación); si no, responde con `POST /v1/messages` usando `data.channel` como `channel` y `data.external_user_id` como `to`. Cuando tu IA no sepa qué decir, `POST /v1/conversations/{id}/escalate`. Guía: https://developers.heysteff.com/docs/conecta-tu-ia

## Agente de IA

Los espacios developer traen el agente de heysteff, apagado al crear el espacio. Si la persona ya tiene su propio modelo, déjalo apagado y usa `message:received` + `POST /v1/messages`; si quiere el agente, enciéndelo con `PATCH /v1/agent` (`enabled`, `channels`, `customers`). Paga los tokens con su propia key (`PUT /v1/agent/keys`, nunca la escribas en el código: léela de una variable de entorno) o con el cupo de prueba del plan. Sin key ni cupo no responde y llega `agent.failed` con `ai_quota_exhausted`; no lo reintentes. Webhooks: `agent.replied`, `agent.handoff`, `agent.failed`, `insight.detected`. Detalle: https://developers.heysteff.com/docs/agente

## Sandbox

Para probar sin número propio: en la consola une tu WhatsApp al sandbox (código `DEV-…`), crea una key `sk_test_` y manda con el mismo `POST /v1/messages` y `channel: "whatsapp_cloud"`, sin `channel_connection_id`. Límites: solo WhatsApp, solo a teléfonos unidos (hasta 5), solo dentro de las 24 horas del último mensaje de ese teléfono, sin plantillas y con un tope diario. Detalle: https://developers.heysteff.com/docs/sandbox

## Código hecho para la Cloud API de Meta

Si el proyecto ya llama a `graph.facebook.com/<versión>/{phone_number_id}/messages`, no lo reescribas: cambia la URL base a `https://app.heysteff.com/api/meta/whatsapp/v21.0` (cualquier versión) y el token a la API key de heysteff. Mismo `phone_number_id`, mismo cuerpo y respuestas y errores con la forma de Meta (más `error.heysteff_code`). `messages[0].id` es el id de heysteff y los estados traen ese mismo id. Para el receptor de webhooks, crea el webhook con `format: "meta"`: llega el payload de la Cloud API firmado con `X-Hub-Signature-256` (`sha256=` + HMAC-SHA256 del cuerpo) con el secreto del webhook. Guía: https://developers.heysteff.com/docs/meta-compatible

## Servidor MCP

El MCP de heysteff configura el espacio (agente, canales, flujos, campañas, bandeja) en nombre de una persona, con OAuth, nunca con una API key. Úsalo para configurar; para integrar un sistema usa la API. La dirección es fija; al autorizar eliges el espacio y el permiso queda ligado a él (autoriza una cuenta con rol Dueño).

```sh
claude mcp add --transport http heysteff "https://mcp.heysteff.com/mcp"
```

Codex y Cursor: https://developers.heysteff.com/docs/agentes-de-codigo. Cualquier tool que manda un mensaje le llega a una persona real: confirma destinatario y texto con quien te pidió el cambio antes de llamarla.

## Errores comunes

Todo error tiene la forma `{ "error": { "code", "message", "details"? } }`. Decide por `code`; muestra `message` en logs. En el SDK, `HeysteffError` trae además `category` (`auth`, `permission`, `validation`, `rate_limit`, `quota`, `conflict`, `not_found`, `provider`, `server`, `network`) y `retryable`.

| code | Qué hacer |
| --- | --- |
| `unauthorized` (401) | Key mal copiada, revocada o vencida. |
| `forbidden` (403) | Falta un scope o la IP no está permitida. |
| `test_key_not_allowed` (403) | Esa operación no se puede con una `sk_test_`. |
| `wa_window_closed` | Fuera de la ventana de 24 h de WhatsApp: manda una plantilla. |
| `template_not_found`, `template_param_mismatch` | Revisa nombre, idioma y cantidad de `body_params` con `GET /v1/templates`. |
| `media_fetch_failed`, `media_not_found`, `invalid_media` | La URL no se pudo descargar, el archivo ya no existe o es de otro espacio: vuelve a subirlo. |
| `sandbox_not_a_member`, `sandbox_window_closed`, `sandbox_template_not_allowed` | Une el teléfono al sandbox, escríbele desde ese teléfono o no uses plantillas con `sk_test_`. |
| `message_quota_exceeded` (402) | Se acabó el cupo del plan Developer Free. |
| `ai_quota_exhausted` (402 o en `agent.failed`) | Sin key propia del proveedor y sin cupo de prueba del agente: guarda la key o sube de plan. |
| `idempotency_conflict` (409) | Misma `Idempotency-Key` con otro cuerpo: usa una clave nueva por operación. |
| `rate_limited` (429) | Espera `Retry-After` y reintenta. |

Lista completa: https://developers.heysteff.com/docs/errores-y-limites
