developers
EN Crear cuenta

Guías

SDK de TypeScript

@heysteff/sdk envuelve la API v1 con tipos, reintentos, un receptor de webhooks listo y los eventos en vivo. No tiene dependencias: usa fetch, así corre en Node 20+, Deno, Bun y Cloudflare Workers.

Paquete: @heysteff/sdk en npm · código publicado · GitHub de heysteff · licencia MIT. El README del paquete trae un prompt listo para configurarlo con tu IA. Todos los paquetes y repos: Paquetes y repositorios.

npm
npm install @heysteff/sdk

Funciona con import (ESM) y con require (CommonJS), con los tipos incluidos. Esta página describe la versión 0.7.0.

JavaScript
import { Heysteff } from '@heysteff/sdk';        // ESM
const { Heysteff } = require('@heysteff/sdk');   // CommonJS

Mandar un mensaje#

Sin apiKey, el cliente lee HEYSTEFF_API_KEY. Con una key sk_test_ el mensaje sale por el sandbox.

TypeScript
import { Heysteff } from '@heysteff/sdk';

const heysteff = new Heysteff({ apiKey: process.env.HEYSTEFF_API_KEY });

const message = await heysteff.messages.send({
  channel: 'whatsapp_cloud',
  to: '+56912345678',
  content: 'Hola Ana, tu pedido ya va en camino',
});
console.log(message.id, message.status);

Botones y listas, estado de un mensaje, leído, reacciones, webhooks y plantillas (desde la 0.5.0):

TypeScript
await heysteff.messages.send({
  channel: 'whatsapp_cloud',
  to: '+56912345678',
  content_type: 'interactive',
  interactive: {
    type: 'buttons',
    body: '¿Confirmas tu hora?',
    buttons: [{ id: 'si', title: 'Sí' }, { id: 'no', title: 'No' }],
  },
});
const msg = await heysteff.messages.get(messageId);     // msg.status: queued | sent | delivered | read | failed
await heysteff.messages.markRead(inboundMessageId);       // tildes azules / "visto"
await heysteff.messages.react({ message_id: inboundMessageId, emoji: '👍' });
const { secret } = await heysteff.webhooks.create({ url: 'https://api.tutienda.com/heysteff', events: ['message.read'] });
await heysteff.templates.create({ name: 'pedido_despachado', language: 'es', category: 'UTILITY', body_text: 'Hola {{1}}', body_examples: ['Ana'] });

Archivos y plantillas#

media.upload acepta un Blob, un Buffer o un Uint8Array y devuelve el url para el mensaje. Los tipos y límites están en Tipos de mensaje.

TypeScript
import { readFile } from 'node:fs/promises';

const file = await heysteff.media.upload({
  file: await readFile('catalogo.pdf'),
  filename: 'catalogo.pdf',
  contentType: 'application/pdf',
});

await heysteff.messages.send({
  channel: 'whatsapp_cloud',
  to: '+56912345678',
  content_type: 'file',
  media: { url: file.url, filename: 'catalogo.pdf' },
  content: 'Te dejo el catálogo de noviembre',
});
TypeScript
const [template] = await heysteff.templates.list();

await heysteff.messages.send(
  {
    channel: 'whatsapp_cloud',
    to: '+56912345678',
    content_type: 'template',
    template: { name: 'pedido_despachado', language: 'es', body_params: ['Ana', '#1042'] },
  },
  { idempotencyKey: 'pedido-1042-despacho' },
);

Pasar una conversación a una persona#

Si tu propia IA contesta, estos métodos la pasan al equipo y se la devuelven (ver Conecta tu propia IA). Necesitan conversations:write; get, conversations:read.

TypeScript
await heysteff.conversations.get(id);
await heysteff.conversations.escalate(id, { reason: 'Pide un reembolso', urgency: 'high' });
await heysteff.conversations.pauseBot(id);
await heysteff.conversations.resumeBot(id);
await heysteff.conversations.resolve(id);

Reintentos e idempotencia#

Ante un 429, un 5xx o un corte de red, el SDK reintenta hasta dos veces con espera creciente y respeta Retry-After. En los endpoints que aceptan Idempotency-Key (mensajes, campañas, flujos, cobros y reembolsos) genera una clave por llamada y la repite en cada reintento: un mensaje o un cobro nunca salen dos veces. Pasa tu propia clave con idempotencyKey para que valga también entre ejecuciones de tu sistema. Un POST sin idempotencia, como crear un contacto, no se reintenta.

OpciónDefault
apiKeyHEYSTEFF_API_KEY
maxRetries2
timeoutMs30000 por intento
baseUrlhttps://app.heysteff.com/api
fetchel global

Listas y páginas#

list devuelve una página con su cursor; listAll las recorre todas:

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

Errores#

Toda respuesta con error llega como HeysteffError, con status, code, message, requestId y, cuando la API lo sabe, fix. Además trae category, para decidir sin memorizar cada código, y retryable, que dice si tiene sentido volver a intentarlo más tarde (el SDK ya reintenta solo los casos seguros, ver Reintentos). Los códigos están en Errores y límites.

categoryCuándo
authLa key falta, está mal copiada, revocada o vencida (401).
permissionA la key le falta un scope, la IP no está permitida o la operación no se puede con una sk_test_ (403).
validationEl cuerpo o los parámetros no son válidos (400, 422 y el resto de los 4xx).
rate_limitPasaste el límite de solicitudes (429): respeta Retry-After.
quotaSe acabó el cupo del plan o un tope diario (402, o un 403 o 429 con un código de cupo).
conflictChoque de estado o de idempotencia (409), por ejemplo wa_window_closed o idempotency_conflict.
not_foundEl recurso no existe o es de otro espacio (404).
providerFalló el proveedor del canal: Meta, el operador de SMS o Google (424).
serverError de heysteff (5xx).
networkNo hubo respuesta: corte de red o timeout.
TypeScript
import { HeysteffError } from '@heysteff/sdk';

try {
  await heysteff.messages.send({ channel: 'whatsapp_cloud', to: '+56912345678', content: 'Hola' });
} catch (err) {
  if (!(err instanceof HeysteffError)) throw err;
  if (err.code === 'wa_window_closed') {
    // Fuera de la ventana de 24 horas: manda una plantilla.
  } else if (err.category === 'permission') {
    console.error('A la key le falta un permiso:', err.message, err.fix);
  } else if (err.retryable) {
    // Vuelve a encolar el trabajo: el SDK ya agotó sus reintentos.
  } else {
    throw err;
  }
}

Recibir webhooks#

createWebhookHandler arma el receptor completo en una línea: lee el cuerpo crudo, verifica la firma X-Heysteff-Signature y que X-Heysteff-Timestamp tenga menos de 5 minutos, y le pasa el evento tipado a tu onEvent. Responde 200 si todo salió bien, 400 si la firma no es válida y 500 si tu onEvent lanza un error; en ese caso heysteff reintenta la entrega (ver Reintentos). Recibe un Request estándar y devuelve un Response, así que sirve tal cual en Next.js, Hono, Deno, Bun y Cloudflare Workers.

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) => {
    if (event.event === 'message:received') {
      console.log(event.data.external_user_id, event.data.content);
    }
  },
});
Hono
import { Hono } from 'hono';
import { createWebhookHandler } from '@heysteff/sdk';

const handler = createWebhookHandler({ secret: process.env.HEYSTEFF_WEBHOOK_SECRET!, onEvent });
const app = new Hono();
app.post('/webhooks/heysteff', (c) => handler(c.req.raw));
Express
import express from 'express';
import { createWebhookHandler } from '@heysteff/sdk';

const handler = createWebhookHandler({ secret: process.env.HEYSTEFF_WEBHOOK_SECRET!, onEvent });
const app = express();
// express.raw: la firma se calcula sobre el cuerpo crudo.
app.post('/webhooks/heysteff', express.raw({ type: 'application/json' }), handler.toExpress());

Si ya tienes un cliente, heysteff.webhooks.handler({ secret, onEvent }) hace lo mismo. heysteff espera la respuesta hasta 10 segundos: lo pesado, encólalo y procésalo después. Una entrega puede llegar más de una vez, así que procesa cada evento de forma idempotente.

Para probarlo en tu máquina con eventos reales, sin túnel, usa heysteff listen --forward-to http://localhost:3000/webhooks/heysteff. En local la CLI firma con el secreto whsec_local_… que muestra al iniciar: ese es el HEYSTEFF_WEBHOOK_SECRET de tu entorno local, o pásale el tuyo con --secret.

Eventos tipados#

WebhookEvent es una unión discriminada por event: dentro de cada case, TypeScript sabe qué trae data. WebhookEnvelope es el sobre genérico ({ id, tenant_id, event, ts, data }) y WEBHOOK_EVENT_NAMES lista todos los nombres de evento.

TypeScript
import { WEBHOOK_EVENT_NAMES, type WebhookEvent } from '@heysteff/sdk';

async function onEvent(event: WebhookEvent) {
  switch (event.event) {
    case 'message:received':
      if (!event.data.bot_paused) console.log(event.data.channel, event.data.content);
      break;
    case 'message.failed':
      console.log(event.data.message_id, event.data.error.code);
      break;
    case 'conversation.escalated':
      console.log('Pasó a una persona:', event.data.conversation_id);
      break;
  }
}

console.log(WEBHOOK_EVENT_NAMES); // ['message:received', 'message:sent', …]

Verificar a mano#

Si armas tu propio receptor, verifyWebhook hace solo la verificación: comprueba la firma X-Heysteff-Signature y que X-Heysteff-Timestamp tenga menos de 5 minutos, y devuelve el evento. Pásale el cuerpo crudo, antes de parsear el JSON:

TypeScript
import express from 'express';
import { verifyWebhook, WebhookVerificationError } from '@heysteff/sdk';

const app = express();

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

Eventos en vivo#

heysteff.events.stream() recibe en vivo los mismos eventos que un webhook, sin exponer una URL: abre GET /v1/events/stream (Server-Sent Events) y devuelve un iterable asíncrono de WebhookEvent. Si se corta la conexión, se reconecta solo con Last-Event-ID y recupera lo que pasó entretanto, hasta 10 minutos atrás. Necesita el scope events:read; con una key sk_test_ llegan solo los eventos del sandbox. Es lo mismo que usa heysteff listen.

TypeScript
const controller = new AbortController();
process.on('SIGINT', () => controller.abort());

for await (const event of heysteff.events.stream({
  events: ['message:received', 'message.read'],
  signal: controller.signal,
})) {
  if (event.event === 'message:received') console.log(event.data.content);
}

Sirve para scripts, pruebas y procesos sin URL pública. En producción conviene un webhook: heysteff lo reintenta durante horas si tu servidor está caído, mientras que el stream guarda solo los últimos 10 minutos.

Números de WhatsApp de heysteff#

heysteff.numbers reserva un número del pool de heysteff y devuelve el link de la ventana de Meta donde se termina el alta. Ver Obtén un número.

TypeScript
const { number, connect_url, connect_url_expires_at } = await heysteff.numbers.reserve();
// Alguien abre connect_url en un navegador y elige el número en Meta.
const { data, plan, offer } = await heysteff.numbers.list();
await heysteff.numbers.requestDisplayName(number.id, 'Tienda Aurora');
await heysteff.numbers.release(number.id); // 409 wa_pool_number_connected si ya está conectado

Clientes y links de conexión#

heysteff.customers crea clientes de tu SaaS y les genera un link para que conecten su propio canal; después mandas por cuenta de ellos con customer_id. Ver Clientes y links de conexión.

TypeScript
const customer = await heysteff.customers.create({ external_id: 'acme-42', name: 'Acme SpA' });
const { url } = await heysteff.customers.createSetupLink(customer.id, { allowed_channels: ['whatsapp_cloud'] });
// Tu cliente abre url y conecta su WhatsApp.
const channels = await heysteff.customers.listChannels(customer.id);
await heysteff.messages.send({ channel: 'whatsapp_cloud', customer_id: customer.id, to: '+56912345678', content: 'Hola' });
for await (const c of heysteff.customers.listAll()) console.log(c.external_id, c.channels_count);

Agente de IA#

heysteff.agent configura el agente de IA del espacio, guarda tu key del proveedor y administra custom tools; heysteff.insights lee los hallazgos. Ver Agente de IA.

TypeScript
const agent = await heysteff.agent.get(); // estado, cupo de prueba y keys (solo sus últimos 4)
await heysteff.agent.setKey({ provider: 'anthropic', api_key: process.env.ANTHROPIC_API_KEY! });
await heysteff.agent.update({ enabled: true, provider: 'anthropic', model: 'claude-haiku-5-5', channels: [{ id: channelId, enabled: false }] });
await heysteff.agent.tools.create({ slug: 'estado_pedido', name: 'Estado de un pedido', description: 'Busca un pedido por su número.', spec });
const { agent_gaps } = await heysteff.insights.get({ days: 7 });

Todo lo demás#

Cada endpoint de la referencia tiene su método (contacts, contactLists, campaigns, events, conversions, charges, refunds, reviews, sales, automationFlows). heysteff.call(operationId, …) llama a cualquiera por su operationId del OpenAPI.