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 install @heysteff/sdkFunciona con import (ESM) y con require (CommonJS), con los tipos incluidos. Esta página describe la versión 0.7.0.
import { Heysteff } from '@heysteff/sdk'; // ESM
const { Heysteff } = require('@heysteff/sdk'); // CommonJSMandar un mensaje#
Sin apiKey, el cliente lee HEYSTEFF_API_KEY. Con una key sk_test_ el mensaje sale por el sandbox.
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):
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.
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',
});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.
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ón | Default |
|---|---|
apiKey | HEYSTEFF_API_KEY |
maxRetries | 2 |
timeoutMs | 30000 por intento |
baseUrl | https://app.heysteff.com/api |
fetch | el global |
Listas y páginas#
list devuelve una página con su cursor; listAll las recorre todas:
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.
| category | Cuándo |
|---|---|
auth | La key falta, está mal copiada, revocada o vencida (401). |
permission | A la key le falta un scope, la IP no está permitida o la operación no se puede con una sk_test_ (403). |
validation | El cuerpo o los parámetros no son válidos (400, 422 y el resto de los 4xx). |
rate_limit | Pasaste el límite de solicitudes (429): respeta Retry-After. |
quota | Se acabó el cupo del plan o un tope diario (402, o un 403 o 429 con un código de cupo). |
conflict | Choque de estado o de idempotencia (409), por ejemplo wa_window_closed o idempotency_conflict. |
not_found | El recurso no existe o es de otro espacio (404). |
provider | Falló el proveedor del canal: Meta, el operador de SMS o Google (424). |
server | Error de heysteff (5xx). |
network | No hubo respuesta: corte de red o timeout. |
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.
// 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);
}
},
});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));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.
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:
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.
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.
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á conectadoClientes 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.
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.
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.