developers
EN Crear cuenta

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#

  1. Crea el cliente con POST /v1/customers, una vez por cada negocio de tu SaaS.
  2. Genera un link con POST /v1/customers/{id}/setup-links. Te devuelve url (https://developers.heysteff.com/connect/hsl_…).
  3. 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.
  4. Te enteras por dos lados: tu cliente vuelve a tu success_redirect_url con el id del canal en la URL, y a tu webhook llega customer.channel_connected.
  5. Mandas mensajes por cuenta de él con POST /v1/messages y customer_id.

1. Crea el cliente#

curl
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" }
  }'
Respuesta
{
  "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.

curl
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"
  }'
Respuesta
{
  "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"
  }
}
CampoQué hace
allowed_channelsLos canales que tu cliente puede elegir: whatsapp_cloud, instagram, facebook (Messenger) y gmail. Por defecto, los cuatro.
expires_in_hoursVigencia del link, de 1 a 2160 horas (90 días). Por defecto 720 (30 días).
locale, themeIdioma y colores de la página. Ver Personaliza la página.
whatsapp_modesCómo puede conectar WhatsApp: número propio, coexistencia con la app del celular o un número de heysteff. Ver WhatsApp.
reconnect_channel_idVuelve a conectar un canal caído del cliente. Ver Volver a conectar.
success_redirect_urlA dónde vuelve tu cliente al conectar. https, o http://localhost para probar en tu máquina.
failure_redirect_urlA dónde vuelve si algo falla. Sin ella, vuelve a success_redirect_url con status=error.
brand_name, brand_logo_urlLo 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):

Conectado
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_cloud
Error
https://tuapp.com/integraciones/heysteff/error?status=error&error=<código>&customer_id=6f1c2a8e-3b4d-4e5f-9a0b-1c2d3e4f5a6b

Sin 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:

JSON
{
  "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
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
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"
    }
  }'

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:

FormaPara quién
own_numberUn número que todavía no está en la app de WhatsApp Business. Se da de alta en la API oficial de Meta.
coexistenceEl 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_numberUn 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).

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).

Respuesta
{
  "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
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:

HTML
<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>

Con el SDK y el CLI#

TypeScript
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 iframe
Terminal
heysteff 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#

EndpointScopeQué hace
POST /v1/customerscustomers:writeCrea 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/customerscustomers:readTus 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:readUn cliente, con channels_count. 404 customer_not_found.
PATCH /v1/customers/{id}customers:writeCambia name, external_id o metadata (al menos uno).
DELETE /v1/customers/{id}customers:writeBorra el cliente. Sus canales siguen conectados en tu espacio, sin cliente: borrar no desconecta nada.
POST /v1/customers/{id}/setup-linkscustomers:writeCrea un link de conexión y devuelve url. Revoca los activos del mismo cliente y canal.
GET /v1/customers/{id}/setup-linkscustomers:readLos links del cliente con su state.
GET /v1/setup-linkscustomers:readTodos los links del espacio; filtra con status y customer_id.
POST /v1/setup-links/{id}/revokecustomers:writeRevoca un link sin usar.
GET /v1/customers/{id}/usagecustomers:readMensajes y conversaciones del cliente por canal y por día (from, to).
POST /v1/inbox-embedsinbox:embedCrea un token de la bandeja embebible; devuelve embed_url y snippet.
GET /v1/inbox-embeds, GET/PATCH/DELETE /v1/inbox-embeds/{id}inbox:embedLista, consulta, cambia (nombre, orígenes, tema, idioma) y revoca tokens.
GET /v1/customers/{id}/channelscustomers:readLos canales que conectó el cliente: id (tu channel_connection_id), channel_type, display_name, is_active.
POST /v1/messagesmessages:writeCon customer_id, manda por el canal de ese cliente.
GET /v1/conversations?customer_id=conversations:readSolo las conversaciones de los canales de ese cliente: una bandeja por cliente (la plantilla de bandeja lo hace con HEYSTEFF_CUSTOMER_ID).

Seguridad#

Cupos por plan#

PlanClientes
Developer Free1, para probar el flujo completo.
Developer10.
Developer PlatformIlimitados.

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)#