Empezar
Clientes y links de conexión
Si tienes un SaaS, tus clientes pueden conectar sus propios WhatsApp, Instagram, Messenger o Gmail a tu integración sin entrar a heysteff. Creas un cliente, le generas un link de conexión, tu cliente lo abre y conecta su cuenta en una página de heysteff, y desde ahí mandas mensajes por cuenta de él con el mismo POST /v1/messages.
Qué es un cliente#
Un cliente es un negocio que usa tu producto: una tienda, una clínica, una agencia. Lo identificas con tu propio external_id (el id que ya tiene en tu base) y puedes guardarle un metadata libre. Los canales que conecta quedan en tu espacio de heysteff, marcados con ese cliente, y los usas con tu API key.
Un mismo link sirve para cualquiera de los cuatro canales: WhatsApp (Embedded Signup de Meta), Instagram, Messenger y Gmail. Tu cliente elige uno y lo conecta; para otro canal, genera otro link. Instagram se puede conectar de dos formas: con la página de Facebook a la que está vinculada la cuenta, o sin página de Facebook (inicio de sesión de Instagram). Las dos dejan el mismo canal instagram.
El flujo completo#
- Crea el cliente con
POST /v1/customers, una vez por cada negocio de tu SaaS. - Genera un link con
POST /v1/customers/{id}/setup-links. Te devuelveurl(https://developers.heysteff.com/connect/hsl_…). - Tu cliente lo abre, elige el canal y conecta su cuenta: entra con Facebook para WhatsApp, Instagram o Messenger, con Instagram si su cuenta no tiene página de Facebook, o con Google para Gmail. La página muestra tu nombre y tu logo, no los de heysteff.
- Te enteras por dos lados: tu cliente vuelve a tu
success_redirect_urlcon el id del canal en la URL, y a tu webhook llegacustomer.channel_connected. - Mandas mensajes por cuenta de él con
POST /v1/messagesycustomer_id.
1. Crea el cliente#
curl -X POST https://app.heysteff.com/api/v1/customers \
-H "Authorization: Bearer $HEYSTEFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_id": "acme-42",
"name": "Acme SpA",
"metadata": { "plan": "pro" }
}'{
"data": {
"id": "6f1c2a8e-3b4d-4e5f-9a0b-1c2d3e4f5a6b",
"external_id": "acme-42",
"name": "Acme SpA",
"metadata": { "plan": "pro" },
"channels_count": 0,
"created_at": "2026-11-13T12:00:00.000Z",
"updated_at": "2026-11-13T12:00:00.000Z"
}
}external_id es único en tu espacio: repetirlo responde 409 customer_external_id_taken. Para buscar a un cliente por tu id, usa GET /v1/customers?external_id=acme-42.
2. Genera el link de conexión#
curl -X POST https://app.heysteff.com/api/v1/customers/6f1c2a8e-3b4d-4e5f-9a0b-1c2d3e4f5a6b/setup-links \
-H "Authorization: Bearer $HEYSTEFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"allowed_channels": ["whatsapp_cloud", "instagram"],
"expires_in_hours": 72,
"success_redirect_url": "https://tuapp.com/integraciones/heysteff/listo",
"failure_redirect_url": "https://tuapp.com/integraciones/heysteff/error",
"brand_name": "TuApp",
"brand_logo_url": "https://tuapp.com/logo.png"
}'{
"data": {
"id": "0b8f3c2d-1e4a-4b5c-9d6e-7f8a9b0c1d2e",
"customer_id": "6f1c2a8e-3b4d-4e5f-9a0b-1c2d3e4f5a6b",
"url": "https://developers.heysteff.com/connect/hsl_…",
"allowed_channels": ["whatsapp_cloud", "instagram"],
"expires_at": "2026-11-16T12:00:00.000Z",
"used_at": null,
"channel_connection_id": null,
"success_redirect_url": "https://tuapp.com/integraciones/heysteff/listo",
"failure_redirect_url": "https://tuapp.com/integraciones/heysteff/error",
"brand_name": "TuApp",
"brand_logo_url": "https://tuapp.com/logo.png",
"created_at": "2026-11-13T12:00:00.000Z"
}
}| Campo | Qué hace |
|---|---|
allowed_channels | Los canales que tu cliente puede elegir: whatsapp_cloud, instagram, facebook (Messenger) y gmail. Por defecto, los cuatro. |
expires_in_hours | Vigencia del link, de 1 a 2160 horas (90 días). Por defecto 720 (30 días). |
locale, theme | Idioma y colores de la página. Ver Personaliza la página. |
whatsapp_modes | Cómo puede conectar WhatsApp: número propio, coexistencia con la app del celular o un número de heysteff. Ver WhatsApp. |
reconnect_channel_id | Vuelve a conectar un canal caído del cliente. Ver Volver a conectar. |
success_redirect_url | A dónde vuelve tu cliente al conectar. https, o http://localhost para probar en tu máquina. |
failure_redirect_url | A dónde vuelve si algo falla. Sin ella, vuelve a success_redirect_url con status=error. |
brand_name, brand_logo_url | Lo que ve tu cliente en la página. Sin ellos, el nombre de tu espacio. |
url solo viene en esta respuesta: heysteff guarda un hash del token y no lo puede volver a mostrar. Guárdalo o mándaselo a tu cliente en el momento. Si se pierde, genera otro.
3. La vuelta a tu sistema#
Cuando tu cliente termina, heysteff lo devuelve a tu URL con el resultado en la query (se respeta la query que ya traiga tu URL):
https://tuapp.com/integraciones/heysteff/listo?status=connected&channel_id=9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d&customer_id=6f1c2a8e-3b4d-4e5f-9a0b-1c2d3e4f5a6b&channel_type=whatsapp_cloudhttps://tuapp.com/integraciones/heysteff/error?status=error&error=<código>&customer_id=6f1c2a8e-3b4d-4e5f-9a0b-1c2d3e4f5a6bSin URLs, la página muestra «Listo» y tu cliente la puede cerrar. La query sirve para mostrarle a tu cliente que terminó, pero no la uses como prueba: cualquiera puede escribir esa URL a mano. Confírmalo con el webhook o con GET /v1/customers/{id}/channels.
4. El webhook customer.channel_connected#
Suscríbete a customer.channel_connected como a cualquier otro evento (ver Webhooks). Llega firmado, con el sobre de siempre, cada vez que un cliente termina de conectar un canal con un link:
{
"id": "5d1f0c7e-8a2b-4c3d-9e4f-0a1b2c3d4e5f",
"tenant_id": "2c7e9b1a-3d4f-4a5b-8c6d-7e8f9a0b1c2d",
"event": "customer.channel_connected",
"ts": "2026-11-13T12:07:41.000Z",
"data": {
"customer_id": "6f1c2a8e-3b4d-4e5f-9a0b-1c2d3e4f5a6b",
"external_id": "acme-42",
"channel_connection_id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"channel_type": "whatsapp_cloud",
"display_name": "Acme",
"setup_link_id": "0b8f3c2d-1e4a-4b5c-9d6e-7f8a9b0c1d2e"
}
}5. Manda mensajes por cuenta de tu cliente#
Agrega customer_id a POST /v1/messages. El mensaje sale por el canal activo de ese cliente del tipo channel (si tiene más de uno, el conectado más reciente). Si además mandas channel_connection_id, ese canal tiene que ser del cliente.
curl https://app.heysteff.com/api/v1/messages \
-H "Authorization: Bearer $HEYSTEFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel": "whatsapp_cloud",
"customer_id": "6f1c2a8e-3b4d-4e5f-9a0b-1c2d3e4f5a6b",
"to": "+56912345678",
"content": "Hola Ana, tu hora quedó confirmada para mañana a las 10:00"
}'Errores propios: 404 customer_not_found si el cliente no existe y 422 customer_channel_not_connected si no tiene un canal activo de ese tipo. Las respuestas de los clientes finales llegan a tu webhook message:received con su channel_connection_id, así sabes de qué cliente es cada conversación.
Personaliza la página del link#
La página que ve tu cliente puede verse como tu producto: tu nombre, tu logo, tus colores y su idioma.
curl -X POST https://app.heysteff.com/api/v1/customers/6f1c2a8e-3b4d-4e5f-9a0b-1c2d3e4f5a6b/setup-links \
-H "Authorization: Bearer $HEYSTEFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"allowed_channels": ["whatsapp_cloud"],
"whatsapp_modes": ["coexistence", "own_number"],
"locale": "es",
"brand_name": "TuApp",
"brand_logo_url": "https://tuapp.com/logo.png",
"theme": {
"primary_color": "#4f46e5",
"background_color": "#f8fafc",
"text_color": "#0f172a",
"border_color": "#e2e8f0"
}
}'theme: colores en#RRGGBBpara el botón principal, el fondo, el texto y los bordes. Los que no mandes usan los de heysteff.locale:es,enoptfija el idioma (la URL lleva?lang=);auto, el valor por defecto, usa el del navegador de tu cliente.
WhatsApp: número propio, coexistencia o número de heysteff#
Con whatsapp_modes eliges cómo puede conectar WhatsApp tu cliente. Si permites más de una forma, la página le pregunta cuál quiere:
| Forma | Para quién |
|---|---|
own_number | Un número que todavía no está en la app de WhatsApp Business. Se da de alta en la API oficial de Meta. |
coexistence | El número que tu cliente ya usa en la app de WhatsApp Business del celular. Sigue ahí: su equipo responde desde la app y tú recibes los mismos chats por la API, con sus contactos y hasta 6 meses de historial. |
heysteff_number | Un número de heysteff ya verificado en Meta, del cupo de números de tu plan. Si tu plan no incluye números, crear el link responde 402 plan_limit. |
Por defecto, own_number y coexistence.
Volver a conectar un canal caído#
Si un canal de tu cliente se desconectó (cambió la contraseña de Facebook, venció el acceso de Google), genera un link con reconnect_channel_id: el id del canal de GET /v1/customers/{id}/channels. El link solo permite ese tipo de canal y la página le dice a tu cliente qué cuenta reconectar. El webhook customer.channel_connected trae reconnect_channel_id y reconnected (true si volvió el mismo canal; con otra cuenta queda un canal nuevo).
Un solo link activo por cliente y canal#
Generar un link nuevo revoca los anteriores del mismo cliente que permiten alguno de los mismos canales: así un link viejo reenviado no conecta nada. GET /v1/setup-links lista todos los links de tu espacio con su state (active, used, expired, revoked), y POST /v1/setup-links/{id}/revoke revoca uno a mano.
Uso por cliente#
GET /v1/customers/{id}/usage?from=2026-11-01&to=2026-11-30 te da lo que pasó por los canales de un cliente, para cobrarle: mensajes totales, entrantes, salientes (con los fallidos) y fallidos, más las conversaciones con mensajes, en total, por canal y por día. Los días son UTC y el rango incluye las dos fechas; sin fechas, los últimos 30 días (hasta 366).
{
"data": {
"customer_id": "6f1c2a8e-3b4d-4e5f-9a0b-1c2d3e4f5a6b",
"from": "2026-11-01",
"to": "2026-11-30",
"totals": { "messages": 1840, "inbound": 1012, "outbound": 828, "failed": 6, "conversations": 214 },
"by_channel": [
{ "channel": "whatsapp_cloud", "messages": 1500, "inbound": 820, "outbound": 680, "failed": 5, "conversations": 170 },
{ "channel": "instagram", "messages": 340, "inbound": 192, "outbound": 148, "failed": 1, "conversations": 44 }
],
"by_day": [
{ "date": "2026-11-01", "messages": 52, "inbound": 30, "outbound": 22, "failed": 0 }
]
}
}Bandeja embebible#
Mete la bandeja de heysteff en tu producto: tus usuarios ven y responden WhatsApp, Instagram, Messenger, SMS y Gmail sin salir de tu app. La vista muestra la lista de conversaciones con el ícono de cada canal, el hilo, responder con texto o archivos (y con una plantilla aprobada cuando la ventana de 24 h de WhatsApp está cerrada), marcar como leída o no leída y tomar la conversación o devolverla al bot, en tiempo real.
curl -X POST https://app.heysteff.com/api/v1/inbox-embeds \
-H "Authorization: Bearer $HEYSTEFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Bandeja de Acme",
"scope": "customer",
"customer_id": "6f1c2a8e-3b4d-4e5f-9a0b-1c2d3e4f5a6b",
"allowed_origins": ["https://app.tuapp.com"],
"theme": "system",
"expires_in_seconds": 3600
}'La respuesta trae token, embed_url y snippet (el iframe listo) solo esa vez. Pega el iframe en tu producto:
<iframe
src="https://developers.heysteff.com/embed/inbox?token=hse_…&status=open&unread=1"
style="width:100%;height:640px;border:0;border-radius:12px"
title="Inbox"></iframe>- Alcance.
scope:all(todo tu espacio),channel(un canal, conchannel_connection_id) ocustomer(los canales de un cliente, concustomer_id). La vista nunca muestra una conversación fuera de su alcance, ni en la lista, ni en el hilo, ni en el tiempo real. - Quién la puede mostrar.
allowed_origins(1 a 20):https://app.tuapp.com, con comodín de subdominiohttps://*.tuapp.com, ohttp://localhost:3000para probar. Van al CSPframe-ancestors: otro sitio no puede enmarcarla. - Filtros por la URL.
status(open,closed,snoozed,all),search,unread=1ychannel; tambiénthemeylang. - Tema e idioma.
theme:system,lightodark;locale:auto,es,enopt. - Tokens por sesión. Quien tiene el token puede leer y responder dentro del alcance. Créalo desde tu backend para cada usuario con
expires_in_secondscorto, nunca lo pongas sin vencimiento en una página pública.DELETE /v1/inbox-embeds/{id}lo revoca y la vista deja de cargar. - Scope de la key.
inbox:embed. Las keys de prueba no crean tokens.
Con el SDK y el CLI#
import { Heysteff } from '@heysteff/sdk';
const heysteff = new Heysteff({ apiKey: process.env.HEYSTEFF_API_KEY });
const customer = await heysteff.customers.create({ external_id: 'acme-42', name: 'Acme SpA' });
const link = await heysteff.customers.createSetupLink(customer.id, {
allowed_channels: ['whatsapp_cloud', 'instagram'],
success_redirect_url: 'https://tuapp.com/integraciones/heysteff/listo',
});
console.log(`Mándale a tu cliente: ${link.url}`);
// Cuando llega customer.channel_connected:
const channels = await heysteff.customers.listChannels(customer.id);
await heysteff.messages.send({
channel: 'whatsapp_cloud',
customer_id: customer.id,
to: '+56912345678',
content: 'Hola Ana, tu hora quedó confirmada',
});
// Uso del mes para cobrarle a tu cliente
const usage = await heysteff.customers.usage(customer.id, { from: '2026-11-01', to: '2026-11-30' });
// Bandeja embebible: un token por sesión de tu usuario
const embed = await heysteff.inboxEmbeds.create({
name: 'Bandeja de Acme',
scope: 'customer',
customer_id: customer.id,
allowed_origins: ['https://app.tuapp.com'],
expires_in_seconds: 3600,
});
// embed.embed_url va en el src de tu iframeheysteff customers create --external-id acme-42 --name "Acme SpA"
heysteff customers link 6f1c2a8e-3b4d-4e5f-9a0b-1c2d3e4f5a6b --channels whatsapp_cloud,instagram --redirect-url https://tuapp.com/listo
heysteff customers channels 6f1c2a8e-3b4d-4e5f-9a0b-1c2d3e4f5a6b
heysteff messages send --channel whatsapp_cloud --customer 6f1c2a8e-3b4d-4e5f-9a0b-1c2d3e4f5a6b --to +56912345678 --text "Hola"Endpoints y scopes#
| Endpoint | Scope | Qué hace |
|---|---|---|
POST /v1/customers | customers:write | Crea un cliente (external_id, name, metadata opcional de hasta 16 KB). 409 customer_external_id_taken; 402 plan_limit si llegaste al cupo de tu plan. |
GET /v1/customers | customers:read | Tus clientes. Filtra con external_id; pagina con limit (1 a 100, por defecto 50) y cursor (next_cursor de la respuesta). |
GET /v1/customers/{id} | customers:read | Un cliente, con channels_count. 404 customer_not_found. |
PATCH /v1/customers/{id} | customers:write | Cambia name, external_id o metadata (al menos uno). |
DELETE /v1/customers/{id} | customers:write | Borra el cliente. Sus canales siguen conectados en tu espacio, sin cliente: borrar no desconecta nada. |
POST /v1/customers/{id}/setup-links | customers:write | Crea un link de conexión y devuelve url. Revoca los activos del mismo cliente y canal. |
GET /v1/customers/{id}/setup-links | customers:read | Los links del cliente con su state. |
GET /v1/setup-links | customers:read | Todos los links del espacio; filtra con status y customer_id. |
POST /v1/setup-links/{id}/revoke | customers:write | Revoca un link sin usar. |
GET /v1/customers/{id}/usage | customers:read | Mensajes y conversaciones del cliente por canal y por día (from, to). |
POST /v1/inbox-embeds | inbox:embed | Crea un token de la bandeja embebible; devuelve embed_url y snippet. |
GET /v1/inbox-embeds, GET/PATCH/DELETE /v1/inbox-embeds/{id} | inbox:embed | Lista, consulta, cambia (nombre, orígenes, tema, idioma) y revoca tokens. |
GET /v1/customers/{id}/channels | customers:read | Los canales que conectó el cliente: id (tu channel_connection_id), channel_type, display_name, is_active. |
POST /v1/messages | messages:write | Con customer_id, manda por el canal de ese cliente. |
GET /v1/conversations?customer_id= | conversations:read | Solo las conversaciones de los canales de ese cliente: una bandeja por cliente (la plantilla de bandeja lo hace con HEYSTEFF_CUSTOMER_ID). |
Seguridad#
- Un uso. Cada link conecta un solo canal. Al terminar queda usado y ya no abre la conexión.
- Vence. Un link sin usar deja de servir a las
expires_in_hours(30 días por defecto). - Uno activo. Un link nuevo para el mismo cliente y canal revoca los anteriores.
- Sin secretos a la vista. heysteff guarda solo un hash del token. Tu cliente nunca ve tu API key, y tú nunca ves sus credenciales: el acceso que entrega Meta o Google queda cifrado en heysteff y no sale por la API.
- Redirect controlado. Las URLs de vuelta son https (o
http://localhost), sin usuario ni contraseña en la URL. - Solo keys reales. Las keys de prueba (
sk_test_) no crean links: responden 403test_key_not_allowed. El resto de los endpoints de clientes sí funciona con ellas.
Cupos por plan#
| Plan | Clientes |
|---|---|
| Developer Free | 1, para probar el flujo completo. |
| Developer | 10. |
| Developer Platform | Ilimitados. |
Al pasar el cupo, POST /v1/customers responde 402 plan_limit con details.resource = "developer_customers". Detalle en Precios.
En la consola#
En developers.heysteff.com/console, la sección Clientes lista tus clientes con sus canales, su uso y sus links, te deja crear un cliente y generar un link con todas sus opciones sin escribir código, y muestra todos los links del espacio con su estado. La sección Bandeja embebible crea tokens y te da el iframe para copiar.
Desde tu IA (MCP)#
list_developer_customersyget_developer_customer: tus clientes, o uno.create_developer_customer,update_developer_customerydelete_developer_customer: crear, cambiar y borrar.create_customer_setup_link: genera un link de conexión y devuelve la URL.list_customer_channels: los canales que conectó un cliente.list_setup_linksyrevoke_customer_setup_link: los links con su estado, y revocar uno.get_customer_usage: el uso de un cliente por canal y por día.list_inbox_embeds,get_inbox_embed,create_inbox_embed,update_inbox_embedyrevoke_inbox_embed: los tokens de la bandeja embebible.