> ## Documentation Index
> Fetch the complete documentation index at: https://docs.jelou.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Mensajería

> Envía mensajes de WhatsApp desde tu función con ctx.jelou: texto, imágenes, botones, listas, carruseles, templates HSM y más.

## ¿Qué es `ctx.jelou`?

`ctx.jelou` es el cliente de mensajería integrado. Permite enviar mensajes de WhatsApp directamente desde tu función sin configurar APIs externas. Está disponible automáticamente cuando la empresa tiene credenciales de la API de Jelou configuradas.

## Verificar disponibilidad

```typescript theme={null}
handler: async (input, ctx) => {
  if (!ctx.jelou.available) {
    return { error: "Mensajería no configurada para esta empresa" };
  }

  await ctx.jelou.send({ type: "text", to: input.telefono, text: "Hola" });
  return { enviado: true };
}
```

<Warning>
  `ctx.jelou.available` es `false` cuando la empresa no tiene `clientId`/`clientSecret` configurados. Llamar a `send()` o `sendTemplate()` en un cliente no disponible lanza un `Error`.
</Warning>

## Enviar mensajes

### `ctx.jelou.send(options)`

Envía un mensaje individual. Retorna `{ messageId: string }`.

<Tabs>
  <Tab title="Texto">
    ```typescript theme={null}
    await ctx.jelou.send({
      type: "text",
      to: "+593987654321",
      text: "Tu pedido #1234 está en camino",
    });
    ```
  </Tab>

  <Tab title="Imagen">
    ```typescript theme={null}
    await ctx.jelou.send({
      type: "image",
      to: "+593987654321",
      mediaUrl: "https://cdn.example.com/producto.jpg",
      caption: "Producto: Laptop Dell XPS 15",
    });
    ```
  </Tab>

  <Tab title="Video">
    ```typescript theme={null}
    await ctx.jelou.send({
      type: "video",
      to: "+593987654321",
      mediaUrl: "https://cdn.example.com/tutorial.mp4",
      caption: "Tutorial de configuración",
    });
    ```
  </Tab>

  <Tab title="Archivo">
    ```typescript theme={null}
    await ctx.jelou.send({
      type: "file",
      to: "+593987654321",
      mediaUrl: "https://cdn.example.com/factura.pdf",
      filename: "factura-1234.pdf",
      caption: "Tu factura del mes de abril",
    });
    ```
  </Tab>

  <Tab title="Audio">
    ```typescript theme={null}
    await ctx.jelou.send({
      type: "audio",
      to: "+593987654321",
      mediaUrl: "https://cdn.example.com/mensaje.ogg",
    });
    ```
  </Tab>
</Tabs>

### Mensajes interactivos

<Tabs>
  <Tab title="Botones">
    ```typescript theme={null}
    await ctx.jelou.send({
      type: "buttons",
      to: "+593987654321",
      text: "¿Cómo deseas recibir tu pedido?",
      buttons: [
        { id: "delivery", title: "Envío a domicilio" },
        { id: "pickup", title: "Retiro en tienda" },
        { id: "express", title: "Envío express" },
      ],
    });
    ```
  </Tab>

  <Tab title="Lista">
    ```typescript theme={null}
    await ctx.jelou.send({
      type: "list",
      to: "+593987654321",
      text: "Selecciona una categoría de productos:",
      buttonText: "Ver categorías",
      sections: [
        {
          title: "Electrónica",
          rows: [
            { id: "laptops", title: "Laptops", description: "Portátiles y notebooks" },
            { id: "phones", title: "Teléfonos", description: "Smartphones y accesorios" },
          ],
        },
        {
          title: "Hogar",
          rows: [
            { id: "furniture", title: "Muebles" },
            { id: "kitchen", title: "Cocina" },
          ],
        },
      ],
    });
    ```
  </Tab>

  <Tab title="Quick Reply">
    ```typescript theme={null}
    await ctx.jelou.send({
      type: "quick_reply",
      to: "+593987654321",
      text: "¿Fue útil esta información?",
      replies: [
        { title: "Sí, gracias", payload: "helpful_yes" },
        { title: "No, necesito más ayuda", payload: "helpful_no" },
      ],
    });
    ```
  </Tab>

  <Tab title="CTA URL">
    ```typescript theme={null}
    await ctx.jelou.send({
      type: "cta_url",
      to: "+593987654321",
      text: "Completa tu compra en nuestra tienda:",
      displayText: "Ir a la tienda",
      url: "https://tienda.example.com/checkout/1234",
    });
    ```
  </Tab>
</Tabs>

### Mensajes avanzados

<Tabs>
  <Tab title="Ubicación">
    ```typescript theme={null}
    await ctx.jelou.send({
      type: "location",
      to: "+593987654321",
      lat: -2.1894,
      lng: -79.8891,
      name: "Tienda Guayaquil",
      address: "Av. 9 de Octubre 123, Guayaquil",
    });
    ```
  </Tab>

  <Tab title="Contactos">
    ```typescript theme={null}
    await ctx.jelou.send({
      type: "contacts",
      to: "+593987654321",
      contacts: [
        {
          name: { formatted_name: "Soporte Técnico", first_name: "Soporte" },
          phones: [{ phone: "+593912345678", type: "WORK" }],
        },
      ],
    });
    ```
  </Tab>

  <Tab title="Carrusel">
    ```typescript theme={null}
    await ctx.jelou.send({
      type: "carousel",
      to: "+593987654321",
      cards: [
        {
          mediaUrl: "https://cdn.example.com/laptop.jpg",
          body: "Laptop Dell XPS 15 — $1,299",
          buttons: [
            { id: "buy_laptop", title: "Comprar" },
            { id: "info_laptop", title: "Más info" },
          ],
        },
        {
          mediaUrl: "https://cdn.example.com/tablet.jpg",
          body: "iPad Pro 12.9\" — $999",
          buttons: [
            { id: "buy_tablet", title: "Comprar" },
            { id: "info_tablet", title: "Más info" },
          ],
        },
      ],
    });
    ```
  </Tab>

  <Tab title="Flow">
    ```typescript theme={null}
    await ctx.jelou.send({
      type: "flow",
      to: "+593987654321",
      text: "Completa tu registro:",
      flowId: "flow_abc123",
      flowCta: "Iniciar registro",
      flowAction: {
        screen: "REGISTER",
        data: { userId: "user-42" },
      },
    });
    ```
  </Tab>

  <Tab title="Sticker">
    ```typescript theme={null}
    await ctx.jelou.send({
      type: "sticker",
      to: "+593987654321",
      mediaUrl: "https://cdn.example.com/sticker.webp",
    });
    ```
  </Tab>
</Tabs>

## Enviar templates HSM

### `ctx.jelou.sendTemplate(options)`

Envía un template HSM aprobado por WhatsApp. Retorna `Array<{ id, destination }>`.

```typescript theme={null}
// Template básico
await ctx.jelou.sendTemplate({
  template: "confirmacion_pedido",
  to: "+593987654321",
  language: "es",
  params: ["María", "PED-1234", "$59.99"],
});
```

### Múltiples destinatarios

```typescript theme={null}
await ctx.jelou.sendTemplate({
  template: "promocion_mensual",
  to: ["+593987654321", "+593912345678", "+593998765432"],
  language: "es",
  params: ["20%", "30 de abril"],
});
```

### Template con media

```typescript theme={null}
await ctx.jelou.sendTemplate({
  template: "recibo_pago",
  to: "+593987654321",
  language: "es",
  params: ["$150.00"],
  mediaUrl: "https://cdn.example.com/recibo-1234.pdf",
  filename: "recibo.pdf",
});
```

## Validar plantillas antes de enviar

Un `sendTemplate` falla en silencio por tres motivos: el nombre está mal escrito, el idioma no está aprobado, o mandas más o menos parámetros de los que espera la plantilla. `ctx.templateRegistry` te da acceso al catálogo de plantillas aprobadas del canal para detectarlo antes de llamar a Meta.

```typescript theme={null}
handler: async (input, ctx) => {
  if (!ctx.templateRegistry.available) {
    return await ctx.jelou.send({ type: "text", to: input.telefono, text: "Tu pedido está listo" });
  }

  if (await ctx.templateRegistry.has("confirmacion_pedido", "es")) {
    await ctx.templateRegistry.validate({
      name: "confirmacion_pedido",
      language: "es",
      params: [input.nombre, input.pedidoId],
    });

    await ctx.jelou.sendTemplate({
      template: "confirmacion_pedido",
      to: input.telefono,
      language: "es",
      params: [input.nombre, input.pedidoId],
    });
  }

  return { enviado: true };
}
```

### Métodos

| Método                                                  | Descripción                                                                                 |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `templateRegistry.available`                            | `false` si la empresa no tiene credenciales configuradas                                    |
| `templateRegistry.has(nombre, idioma?)`                 | `true` si el canal actual tiene la plantilla. Nunca lanza error                             |
| `templateRegistry.hasAnywhere(nombre)`                  | `true` si cualquier canal de la empresa la tiene. Para auditorías, no para decidir un envío |
| `templateRegistry.get(nombre, idioma)`                  | Devuelve la plantilla o `null`. Lanza error si no hay canal asociado                        |
| `templateRegistry.list()`                               | Todas las plantillas aprobadas del canal actual                                             |
| `templateRegistry.validate({ name, language, params })` | Lanza error si el nombre, el idioma, el estado o la cantidad de parámetros no cuadran       |
| `templateRegistry.for(botId)`                           | Devuelve un catálogo enlazado a otro canal                                                  |

<Note>
  `has()` y `hasAnywhere()` son verificaciones booleanas: si no hay canal asociado o el catálogo no responde, registran una advertencia y devuelven `false` en lugar de lanzar error. Cuando necesitas una respuesta definitiva usa `get()`, `list()` o `validate()`, que lanzan `TemplateRegistryError` con un `code`: `unknown_template`, `language_not_approved`, `param_count_mismatch`, `template_paused`, `registry_unavailable` o `no_bot_context`.
</Note>

<Warning>
  `sendTemplate` **no** valida por su cuenta — el catálogo es de solo lectura. Llama a `validate()` explícitamente cuando quieras la verificación.
</Warning>

### En cron y ejecuciones diferidas

En un disparo de cron o de una [ejecución diferida](/guides/functions/diferidas) no hay canal resuelto, así que `has()` siempre devolvería `false`. Lleva el `botId` en tu propio payload y enlaza el catálogo explícitamente:

```typescript theme={null}
handler: async (input, ctx) => {
  // El botId viajó en el payload que agendaste antes
  const registro = ctx.templateRegistry.for(input.botId);

  if (await registro.has("recordatorio_cita", "es")) {
    await ctx.jelou.sendTemplate({
      template: "recordatorio_cita",
      to: input.telefono,
      language: "es",
      params: [input.fecha],
      botId: input.botId,
    });
  }
}
```

`for(botId)` es seguro en cualquier ruta: en una petición normal simplemente reenlaza al canal indicado.

<Tip>
  Los resultados se guardan en caché **90 segundos**. Una plantilla aprobada hace un momento puede no aparecer hasta que la caché expire.
</Tip>

## Override de canal

Todos los mensajes usan `ctx.bot.id` por defecto. Para enviar desde otro canal:

```typescript theme={null}
await ctx.jelou.send({
  type: "text",
  to: "+593987654321",
  text: "Mensaje desde otro canal",
  botId: "bot-notificaciones-456",
});
```

## Manejo de errores

```typescript theme={null}
import { JelouApiError } from "@jelou/functions";

try {
  await ctx.jelou.send({ type: "text", to: "+593987654321", text: "Hola" });
} catch (err) {
  if (err instanceof JelouApiError) {
    if (err.isRateLimit()) {
      ctx.log("Rate limit", { retryAfter: 2 });
      return { error: "rate_limit" };
    }
    if (err.isAuth()) {
      ctx.log("Credenciales inválidas", { status: err.status });
      return { error: "auth_error" };
    }
    if (err.isValidation()) {
      ctx.log("Datos inválidos", { status: err.status, code: err.code });
      return { error: "validation_error" };
    }
  }
  throw err;
}
```

## Testing

Usa `createMockJelouClient()` para testing sin enviar mensajes reales:

```typescript theme={null}
import { createMockContext, createMockJelouClient } from "@jelou/functions/testing";

const mockJelou = createMockJelouClient();
const ctx = createMockContext({ jelou: mockJelou });

await ctx.jelou.send({ type: "text", to: "+593987654321", text: "Test" });

mockJelou.calls.length;       // 1
mockJelou.calls[0].method;    // "send"
mockJelou.calls[0].args.text; // "Test"
mockJelou.reset();             // limpia las llamadas
```

<Card title="Testing completo" icon="flask-vial" href="/guides/functions/testing">
  Referencia de createMockJelouClient, createMockMemoryClient y más.
</Card>
