Documentación · v1

La API de Helpeen. Sin sorpresas.

Once endpoints, JSON y una clave. Aquí está todo: cómo autenticarte, cada endpoint con su petición y su respuesta, los errores y los límites.

¿Lo implementa una IA?

Dale a Claude, Cursor o tu asistente la guía completa: reglas de seguridad, qué arquitectura elegir, código listo para adaptar y esta referencia entera, en un solo Markdown.

También en /llms.txt y /llms-full.txt, donde los asistentes la buscan solos.

Con la API, tu web o tu app muestra el contenido de tu centro de ayuda (artículos, FAQs y vídeos) con su propio diseño, y abre conversaciones con tu equipo en nombre de sus usuarios.

  • URL base: https://helpeen.com/api/v1
  • Formato: JSON en peticiones y respuestas, UTF-8.
  • Sin SDK: cualquier cliente HTTP vale (fetch, curl, axios, requests…).

Primeros pasos

  1. En el panel, entra en Integración y crea una clave pública (hp_pub_…).
  2. Pide todo el contenido de tu centro de ayuda en una llamada:
curl https://helpeen.com/api/v1/help \
  -H "Authorization: Bearer hp_pub_xxxxxxxxxxxxxxxx"
  1. Pinta la respuesta con tu diseño. Si además quieres que tus usuarios escriban al equipo, crea una clave secreta (hp_sec_…) y llama a /conversations desde tu servidor.

¿Prefieres no programar? Con el widget añades un chat flotante o un centro de ayuda completo con una línea de HTML.

Widget

Tres formas de poner Helpeen en una web sin llamar a la API:

Qué añade Cómo
Chat flotante Un botón que abre un panel para escribir al equipo y seguir las respuestas Una línea <script>
Centro de ayuda Un bloque dentro de la página con buscador, FAQs, artículos, vídeos y formulario de contacto Un <div> y la misma línea
React Los dos anteriores como componentes npm install @helpeen/react

El aspecto (color, posición, estilo del botón, tema y textos) se configura en el panel, en Widget, y llega a las webs que ya lo tienen sin tocar su código. Todo se sirve en iframes desde Helpeen, así que no choca con los estilos de tu web. Usa tu clave pública.

Chat flotante

Pega esta línea antes de </body> (o en el <head>):

<script src="https://helpeen.com/widget.js" data-key="hp_pub_xxxxxxxxxxxxxxxx" async></script>

El panel solo se descarga la primera vez que alguien pulsa el botón, así que no ralentiza la carga.

Centro de ayuda incrustado

Pon el <div> donde quieras que aparezca. Con data-launcher="off" no sale el botón flotante:

<div data-helpeen="help"></div>
<script src="https://helpeen.com/widget.js" data-key="hp_pub_xxxxxxxxxxxxxxxx" data-launcher="off" async></script>

Se ajusta en alto a su contenido. Puedes poner varios <div data-helpeen="help"> en la misma página.

React

npm install @helpeen/react
import { HelpeenChat, HelpeenHelpCenter } from "@helpeen/react";

// Chat flotante, una vez, en tu layout:
<HelpeenChat publicKey="hp_pub_xxxxxxxxxxxxxxxx" />

// Centro de ayuda, en tu página de ayuda:
<HelpeenHelpCenter publicKey="hp_pub_xxxxxxxxxxxxxxxx" />

Las dos aceptan locale. HelpeenHelpCenter acepta además className y style para el contenedor.

Atributos del script

Atributo
data-key obligatorio Tu clave pública
data-locale opcional Idioma (es, en, pt, fr, de, it). Por defecto, el <html lang> de la página
data-launcher opcional off para no mostrar el botón flotante
data-user-id, data-user-email, data-user-name, data-user-hash opcional Usuario identificado, ver abajo

Visitantes y usuarios identificados

Sin hacer nada más, quien escribe desde el widget es un visitante: deja su email, y el widget recuerda sus conversaciones en ese navegador. Las respuestas le llegan también por email.

Si tu web tiene login, puedes identificar al usuario: el widget no le pide el email y le enseña sus conversaciones, las mismas que abras por la API con su user_id. Para que nadie pueda hacerse pasar por otro, tu servidor firma el id con el secreto de identidad del panel (Widget › Usuarios identificados):

// En tu servidor (Node.js). Nunca en el navegador.
import { createHmac } from "node:crypto";

const userHash = createHmac("sha256", process.env.HELPEEN_IDENTITY_SECRET).update(user.id).digest("hex");

Y se lo pasas al widget:

<script src="https://helpeen.com/widget.js" data-key="hp_pub_xxxxxxxxxxxxxxxx"
  data-user-id="u_123" data-user-email="ana@ejemplo.com" data-user-name="Ana"
  data-user-hash="9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" async></script>
<HelpeenChat publicKey="hp_pub_xxxxxxxxxxxxxxxx" user={{ id: "u_123", email: "ana@ejemplo.com", name: "Ana", hash: userHash }} />

Si la firma no es válida, el usuario entra como visitante. Si regeneras el secreto, todos los usuarios identificados pasan a visitantes hasta que tu servidor firme con el nuevo.

Controlarlo desde tu código

window.Helpeen("open");     // abre el chat
window.Helpeen("close");
window.Helpeen("toggle");
window.Helpeen("identify", { id: "u_123", email: "ana@ejemplo.com", name: "Ana", hash: "…" });
window.Helpeen("logout");   // al cerrar sesión en tu web
window.Helpeen("mount");    // rellena <div data-helpeen="help"> añadidos después de cargar

Las llamadas que hagas antes de que cargue el script se ejecutan al cargar si defines la cola: window.Helpeen = window.Helpeen || function () { (window.Helpeen.q = window.Helpeen.q || []).push(arguments); };. Con React, usa helpeen("open"), que importas de @helpeen/react.

Dónde funciona

El widget solo se deja incrustar en los dominios de Ajustes › Orígenes permitidos (en cualquiera si la lista está vacía). Para evitar abusos, cada visitante puede abrir 5 conversaciones por hora y cada empresa recibe como mucho 100 por hora desde el widget.

Para recordar la sesión, el widget guarda un identificador en el almacenamiento local del navegador (dentro del iframe de Helpeen). Es necesario para el servicio que pide el usuario, así que no requiere consentimiento, pero conviene mencionarlo en tu política de cookies.

Autenticación

Todas las peticiones llevan la clave en la cabecera Authorization:

Authorization: Bearer <clave>
Clave Prefijo Qué puede hacer Dónde se usa
Pública hp_pub_ Leer el contenido publicado Navegador, app móvil o servidor
Secreta hp_sec_ Todo, incluidas las conversaciones Solo en tu servidor

Reglas:

  • La clave secreta nunca va en un navegador ni en una app. Una petición con clave secreta que lleve cabecera Origin (es decir, que sale de un navegador) se rechaza con 403. Si una clave secreta acaba en una página, revócala en Integración y crea otra.
  • Orígenes permitidos. Con la clave pública desde un navegador, el origen de la página tiene que estar en Ajustes › Orígenes permitidos (por ejemplo https://tuapp.com). Si la lista está vacía, se acepta cualquier origen. Las peticiones sin Origin (servidor, app móvil) no se ven afectadas.
  • CORS está activado para GET y POST, con las cabeceras Authorization y Content-Type.
  • Una clave revocada deja de funcionar al momento (401).

Convenciones

  • Ids: UUID en texto.
  • Fechas: ISO 8601 en UTC, por ejemplo 2026-10-01T10:00:00.000Z.
  • Campos vacíos: un campo opcional sin valor viene como null, nunca se omite (salvo body y body_html en los listados de artículos, ver abajo).
  • Orden: el contenido viene en el orden que le has dado en el panel (position) y, a igualdad, por título.
  • Solo lo publicado: la API nunca devuelve borradores, sea cual sea la clave.

Idioma

Los endpoints de contenido aceptan ?locale= con un código de dos letras (es, en, fr…). Sin él, o si no es válido, se usa el idioma por defecto de tu empresa. Solo se devuelve contenido de ese idioma: un artículo escrito en es no aparece al pedir locale=en.

Paginación

/articles y /faqs admiten limit (de 1 a 100, 50 por defecto) y offset (desde 0). La respuesta repite los valores aplicados:

{ "data": [], "limit": 50, "offset": 0 }

Si data trae menos elementos que limit, no hay más páginas.

Errores

Todos los errores tienen la misma forma:

{
  "error": {
    "code": "not_found",
    "message": "No article `conectar-un-banco`."
  }
}
code HTTP Cuándo
invalid_request 400 Falta un campo, sobra longitud o el cuerpo no es un objeto JSON
unauthorized 401 No hay clave, tiene un formato raro, no existe o está revocada
forbidden 403 Clave pública en un endpoint de conversaciones, clave secreta desde un navegador u origen no permitido
not_found 404 El artículo, la categoría o la conversación no existen (o no son de ese usuario)
internal 500 Error nuestro. Reintenta más tarde

message está en inglés y sirve para depurar; no lo enseñes tal cual a tus usuarios.


Contenido

Se puede llamar con la clave pública o con la secreta.

GET /help

Todo lo que necesita una página de soporte, en una sola llamada: categorías, FAQs (con su respuesta, hasta 100) y colecciones de vídeos.

Parámetro Tipo
locale query, opcional Idioma del contenido
curl "https://helpeen.com/api/v1/help?locale=es" \
  -H "Authorization: Bearer hp_pub_xxxxxxxxxxxxxxxx"

200 OK

{
  "organization": { "name": "Monestic", "slug": "monestic" },
  "locale": "es",
  "categories": [
    {
      "id": "6f1c2a8e-4b0d-4e8a-9a51-2f7d3c9b1e20",
      "slug": "bancos",
      "name": "Bancos",
      "description": "Conectar, sincronizar y desconectar cuentas.",
      "position": 0
    }
  ],
  "faqs": [
    {
      "id": "b2e4d6f8-1a3c-4e5f-8a9b-0c1d2e3f4a5b",
      "slug": "es-seguro",
      "kind": "faq",
      "title": "¿Es seguro conectar mi banco?",
      "excerpt": null,
      "category": { "slug": "bancos", "name": "Bancos" },
      "locale": "es",
      "position": 0,
      "published_at": "2026-09-12T08:30:00.000Z",
      "updated_at": "2026-09-20T16:02:11.000Z",
      "body": "Sí. Nunca vemos tus claves: la conexión se hace a través de tu banco.",
      "body_html": "<p>Sí. Nunca vemos tus claves: la conexión se hace a través de tu banco.</p>\n"
    }
  ],
  "video_collections": [
    {
      "id": "0d9e8f7a-6b5c-4d3e-2f1a-0b9c8d7e6f5a",
      "slug": "primeros-pasos",
      "title": "Primeros pasos",
      "description": null,
      "locale": "es",
      "position": 0,
      "videos": [
        {
          "id": "9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
          "title": "Conecta tu banco",
          "description": "En menos de dos minutos.",
          "url": "https://youtu.be/dQw4w9WgXcQ",
          "provider": "youtube",
          "embed_url": "https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ",
          "thumbnail_url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg",
          "position": 0
        }
      ]
    }
  ]
}

GET /categories

Las categorías, para montar la navegación de tu centro de ayuda.

curl https://helpeen.com/api/v1/categories \
  -H "Authorization: Bearer hp_pub_xxxxxxxxxxxxxxxx"

200 OK

{
  "data": [
    {
      "id": "6f1c2a8e-4b0d-4e8a-9a51-2f7d3c9b1e20",
      "slug": "bancos",
      "name": "Bancos",
      "description": "Conectar, sincronizar y desconectar cuentas.",
      "position": 0
    },
    {
      "id": "7a2d3b9f-5c1e-4f9b-8b62-3a8e4d0c2f31",
      "slug": "facturacion",
      "name": "Facturación",
      "description": null,
      "position": 1
    }
  ]
}

Las categorías no dependen del idioma: se devuelven todas.

GET /articles

Artículos y FAQs publicados, con filtros y búsqueda.

Parámetro Tipo
kind query, opcional article o faq. Sin él, los dos
category query, opcional Slug de categoría. 404 si no existe
q query, opcional Texto a buscar en título, extracto y cuerpo (hasta 100 caracteres, sin distinguir mayúsculas)
locale query, opcional Idioma del contenido
limit, offset query, opcional Paginación

En el listado, los artículos van sin body ni body_html (pide cada uno con /articles/{slug}), y las FAQs van con ellos, porque son cortas y se leen en el sitio.

curl "https://helpeen.com/api/v1/articles?category=bancos&q=sincroniz&limit=10" \
  -H "Authorization: Bearer hp_pub_xxxxxxxxxxxxxxxx"

200 OK

{
  "data": [
    {
      "id": "c3f5e7a9-2b4d-4f6a-9b8c-1d2e3f4a5b6c",
      "slug": "conectar-un-banco",
      "kind": "article",
      "title": "Cómo conectar un banco",
      "excerpt": "Paso a paso, y qué hacer si la sincronización se para.",
      "category": { "slug": "bancos", "name": "Bancos" },
      "locale": "es",
      "position": 0,
      "published_at": "2026-09-10T09:00:00.000Z",
      "updated_at": "2026-09-28T11:45:00.000Z"
    }
  ],
  "limit": 10,
  "offset": 0
}

GET /articles/{slug}

Un artículo o una FAQ, siempre con body (Markdown) y body_html.

Parámetro Tipo
slug ruta Slug del artículo
locale query, opcional Idioma. El mismo slug puede existir en varios idiomas
curl https://helpeen.com/api/v1/articles/conectar-un-banco \
  -H "Authorization: Bearer hp_pub_xxxxxxxxxxxxxxxx"

200 OK

{
  "data": {
    "id": "c3f5e7a9-2b4d-4f6a-9b8c-1d2e3f4a5b6c",
    "slug": "conectar-un-banco",
    "kind": "article",
    "title": "Cómo conectar un banco",
    "excerpt": "Paso a paso, y qué hacer si la sincronización se para.",
    "category": { "slug": "bancos", "name": "Bancos" },
    "locale": "es",
    "position": 0,
    "published_at": "2026-09-10T09:00:00.000Z",
    "updated_at": "2026-09-28T11:45:00.000Z",
    "body": "## Antes de empezar\n\nTen a mano el acceso a tu banca online.\n\n1. Ve a **Cuentas**.\n2. Pulsa **Añadir banco**.",
    "body_html": "<h2>Antes de empezar</h2>\n<p>Ten a mano el acceso a tu banca online.</p>\n<ol>\n<li>Ve a <strong>Cuentas</strong>.</li>\n<li>Pulsa <strong>Añadir banco</strong>.</li>\n</ol>\n"
  }
}

404 Not Found si no hay un artículo publicado con ese slug en ese idioma:

{ "error": { "code": "not_found", "message": "No article `conectar-un-banco`." } }

body_html ya viene saneado: el HTML que se hubiera escrito a mano en el Markdown sale escapado, y los enlaces o imágenes con esquemas peligrosos (javascript:, data:) pierden la URL. Se puede insertar tal cual (innerHTML, dangerouslySetInnerHTML, v-html).

GET /faqs

Atajo de /articles?kind=faq. Mismos parámetros salvo kind; las FAQs siempre traen su respuesta.

curl "https://helpeen.com/api/v1/faqs?category=facturacion" \
  -H "Authorization: Bearer hp_pub_xxxxxxxxxxxxxxxx"

200 OK

{
  "data": [
    {
      "id": "d4a6b8c0-3e5f-4a7b-8c9d-2e3f4a5b6c7d",
      "slug": "cambiar-tarjeta",
      "kind": "faq",
      "title": "¿Cómo cambio la tarjeta de pago?",
      "excerpt": null,
      "category": { "slug": "facturacion", "name": "Facturación" },
      "locale": "es",
      "position": 0,
      "published_at": "2026-09-15T10:00:00.000Z",
      "updated_at": "2026-09-15T10:00:00.000Z",
      "body": "En **Ajustes › Suscripción**, pulsa *Cambiar tarjeta*.",
      "body_html": "<p>En <strong>Ajustes › Suscripción</strong>, pulsa <em>Cambiar tarjeta</em>.</p>\n"
    }
  ],
  "limit": 50,
  "offset": 0
}

GET /videos

Colecciones de vídeos publicadas, con sus vídeos ordenados. Helpeen no aloja vídeo: guarda el enlace de YouTube o Vimeo y te da la URL para incrustarlo.

Parámetro Tipo
locale query, opcional Idioma de las colecciones
curl https://helpeen.com/api/v1/videos \
  -H "Authorization: Bearer hp_pub_xxxxxxxxxxxxxxxx"

200 OK

{
  "data": [
    {
      "id": "0d9e8f7a-6b5c-4d3e-2f1a-0b9c8d7e6f5a",
      "slug": "primeros-pasos",
      "title": "Primeros pasos",
      "description": null,
      "locale": "es",
      "position": 0,
      "videos": [
        {
          "id": "9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
          "title": "Conecta tu banco",
          "description": "En menos de dos minutos.",
          "url": "https://youtu.be/dQw4w9WgXcQ",
          "provider": "youtube",
          "embed_url": "https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ",
          "thumbnail_url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg",
          "position": 0
        },
        {
          "id": "8b7c6d5e-4f3a-4b2c-1d0e-9f8a7b6c5d4e",
          "title": "Exporta tus datos",
          "description": null,
          "url": "https://vimeo.com/123456789",
          "provider": "vimeo",
          "embed_url": "https://player.vimeo.com/video/123456789",
          "thumbnail_url": null,
          "position": 1
        }
      ]
    }
  ]
}

Para incrustarlo: <iframe src="{embed_url}" allowfullscreen></iframe>. thumbnail_url es null en Vimeo.


Conversaciones

Solo con clave secreta y desde tu servidor (desde el navegador, usa el widget). Helpeen no tiene cuentas de usuarios finales: tu app ya sabe quién es su usuario y lo dice con user_id, el id que tenga en tu base de datos (texto, hasta 200 caracteres). Una conversación solo se puede leer o contestar con el mismo user_id que la abrió; con cualquier otro, la respuesta es 404.

El user_id tiene que salir de la sesión de tu servidor, nunca de un parámetro que mande el navegador: si no, cualquiera podría leer las conversaciones de otro usuario.

Estados

Estado Significa Pasa a este estado cuando…
open Le toca responder al equipo El usuario abre la conversación o escribe en ella, aunque estuviera cerrada
pending Le toca al usuario El equipo responde desde el panel
closed Resuelta El equipo la cierra desde el panel

unread es true cuando lo último es una respuesta del equipo y el usuario aún no la ha visto (no se ha llamado a /read ni ha escrito después). Úsalo para el globo de "tienes respuesta".

Avisos por email

Helpeen los manda solo, no hace falta que lo hagas tú:

  • Cuando un usuario abre una conversación o escribe en ella → email al correo de avisos de tu empresa (Ajustes).
  • Cuando el equipo responde → email al usuario, a la dirección con la que abrió la conversación.

GET /conversations

Las conversaciones de un usuario, la de actividad más reciente primero (hasta 100).

Parámetro Tipo
user_id query, obligatorio Id del usuario en tu app
curl "https://helpeen.com/api/v1/conversations?user_id=u_123" \
  -H "Authorization: Bearer hp_sec_xxxxxxxxxxxxxxxx"

200 OK

{
  "data": [
    {
      "id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
      "subject": "No me sincroniza el banco",
      "status": "pending",
      "metadata": { "plan": "optimum", "page": "/bancos" },
      "created_at": "2026-10-01T09:12:00.000Z",
      "last_message_at": "2026-10-01T10:03:27.000Z",
      "unread": true
    }
  ]
}

POST /conversations

Abre una conversación con el primer mensaje del usuario.

Campo Tipo
user.id texto, obligatorio Id del usuario en tu app (hasta 200)
user.email texto, obligatorio Donde le llegarán las respuestas
user.name texto, opcional Nombre para el equipo (hasta 120)
subject texto, obligatorio Asunto (hasta 200)
message texto, obligatorio Primer mensaje (hasta 10.000)
metadata objeto, opcional Lo que quieras que vea el equipo junto a la conversación: plan, versión de la app, página… (hasta 4 KB)
curl https://helpeen.com/api/v1/conversations \
  -H "Authorization: Bearer hp_sec_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "user": { "id": "u_123", "email": "ana@ejemplo.com", "name": "Ana" },
    "subject": "No me sincroniza el banco",
    "message": "Desde ayer no veo movimientos nuevos.",
    "metadata": { "plan": "optimum", "page": "/bancos" }
  }'

201 Created

{
  "data": {
    "id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
    "subject": "No me sincroniza el banco",
    "status": "open",
    "metadata": { "plan": "optimum", "page": "/bancos" },
    "created_at": "2026-10-01T09:12:00.000Z",
    "last_message_at": "2026-10-01T09:12:00.000Z",
    "unread": false
  }
}

400 Bad Request si falta algo o no cumple los límites:

{ "error": { "code": "invalid_request", "message": "`user.email` is not an email." } }

Guarda el id: lo necesitas para leer la conversación y contestar en ella.

GET /conversations/{id}

Una conversación con todos sus mensajes, del más antiguo al más nuevo.

Parámetro Tipo
id ruta Id de la conversación
user_id query, obligatorio Tiene que ser el usuario que la abrió
curl "https://helpeen.com/api/v1/conversations/5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9?user_id=u_123" \
  -H "Authorization: Bearer hp_sec_xxxxxxxxxxxxxxxx"

200 OK

{
  "data": {
    "id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
    "subject": "No me sincroniza el banco",
    "status": "pending",
    "metadata": { "plan": "optimum", "page": "/bancos" },
    "created_at": "2026-10-01T09:12:00.000Z",
    "last_message_at": "2026-10-01T10:03:27.000Z",
    "unread": true,
    "messages": [
      {
        "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
        "author": "user",
        "author_name": "Ana",
        "body": "Desde ayer no veo movimientos nuevos.",
        "created_at": "2026-10-01T09:12:00.000Z"
      },
      {
        "id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
        "author": "agent",
        "author_name": "Iñaki",
        "body": "Hola, Ana. Tu banco pidió renovar el permiso: entra en Cuentas y pulsa «Reconectar».",
        "created_at": "2026-10-01T10:03:27.000Z"
      }
    ]
  }
}

body es texto plano: escápalo al pintarlo y respeta los saltos de línea (por ejemplo con white-space: pre-wrap). Leer la conversación no la marca como leída: para eso está /read.

POST /conversations/{id}/messages

El usuario escribe en una conversación. La deja en open (aunque estuviera cerrada), cuenta como leída y avisa al equipo por email.

Campo Tipo
user_id texto, obligatorio El usuario que la abrió
body texto, obligatorio El mensaje (hasta 10.000)
curl https://helpeen.com/api/v1/conversations/5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9/messages \
  -H "Authorization: Bearer hp_sec_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "user_id": "u_123", "body": "Gracias, ya funciona." }'

201 Created

{
  "data": {
    "id": "3c4d5e6f-7a8b-4c9d-0e1f-2a3b4c5d6e7f",
    "author": "user",
    "author_name": "Ana",
    "body": "Gracias, ya funciona.",
    "created_at": "2026-10-01T10:20:41.000Z"
  }
}

POST /conversations/{id}/read

Marca como vistas las respuestas del equipo: unread pasa a false. Llámalo cuando el usuario abra la conversación en tu app.

Campo Tipo
user_id texto, obligatorio El usuario que la abrió
curl https://helpeen.com/api/v1/conversations/5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9/read \
  -H "Authorization: Bearer hp_sec_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "user_id": "u_123" }'

200 OK

{ "data": { "id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9", "unread": false } }

Objetos

Category

Campo Tipo
id uuid
slug texto Único en tu empresa
name texto
description texto o null
position entero Orden en el panel

Article

Campo Tipo
id uuid
slug texto Único por idioma
kind article o faq En una FAQ, title es la pregunta y body la respuesta
title texto
excerpt texto o null Resumen corto para listados
category { slug, name } o null
locale texto Código de dos letras
position entero
published_at fecha
updated_at fecha
body texto Markdown. Solo en el detalle y en las FAQs
body_html texto HTML saneado. Solo en el detalle y en las FAQs

Collection

Campo Tipo
id uuid
slug texto
title texto
description texto o null
locale texto
position entero
videos [Video] Ordenados por position

Video

Campo Tipo
id uuid
title texto
description texto o null
url texto El enlace original
provider youtube o vimeo
embed_url texto Para un <iframe>. YouTube va en su versión sin cookies
thumbnail_url texto o null null en Vimeo
position entero

Conversation

Campo Tipo
id uuid
subject texto
status open, pending o closed Ver Estados
metadata objeto El que mandaste al abrirla ({} si ninguno)
created_at fecha
last_message_at fecha
unread booleano Hay respuesta del equipo sin ver
messages [Message] Solo en GET /conversations/{id}

Message

Campo Tipo
id uuid
author user o agent agent es alguien de tu equipo
author_name texto o null
body texto Texto plano
created_at fecha

Límites

user_id 200 caracteres
user.email 320 caracteres
user.name 120 caracteres
subject 200 caracteres
message, body 10.000 caracteres
metadata 4 KB en JSON
q 100 caracteres (se recorta)
limit 1–100
Conversaciones por usuario en el listado 100
FAQs en /help 100

Ejemplo: página de soporte en Next.js

Contenido leído en el servidor y cacheado cinco minutos, y un formulario de contacto que abre la conversación desde una server action:

// lib/helpeen.ts
const HELPEEN = "https://helpeen.com/api/v1";

async function helpeen<T>(path: string, init: RequestInit & { next?: { revalidate?: number } } = {}) {
  const res = await fetch(`${HELPEEN}${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${process.env.HELPEEN_SECRET_KEY}`,
      "Content-Type": "application/json",
      ...init.headers,
    },
  });
  const json = await res.json();
  if (!res.ok) throw new Error(`Helpeen ${res.status}: ${json.error?.message}`);
  return json as T;
}

export const getHelp = (locale: string) =>
  helpeen<HelpResponse>(`/help?locale=${locale}`, { next: { revalidate: 300 } });

export const openConversation = (input: {
  user: { id: string; email: string; name?: string };
  subject: string;
  message: string;
  metadata?: Record<string, unknown>;
}) => helpeen<{ data: Conversation }>("/conversations", { method: "POST", body: JSON.stringify(input) });
// app/soporte/actions.ts
"use server";

export async function contact(formData: FormData) {
  const user = await requireUser(); // tu sesión: el user_id sale de aquí, no del formulario
  await openConversation({
    user: { id: user.id, email: user.email, name: user.name },
    subject: String(formData.get("subject")),
    message: String(formData.get("message")),
    metadata: { plan: user.plan },
  });
}

Ejemplo: FAQs en una web estática

Con la clave pública, directamente desde el navegador (añade el dominio en Orígenes permitidos):

<div id="faqs"></div>
<script>
  // `title` is plain text: escape it. `body_html` is already safe.
  const escape = (text) =>
    text.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);

  fetch("https://helpeen.com/api/v1/faqs?locale=es", {
    headers: { Authorization: "Bearer hp_pub_xxxxxxxxxxxxxxxx" },
  })
    .then((r) => r.json())
    .then(({ data }) => {
      document.getElementById("faqs").innerHTML = data
        .map((faq) => `<details><summary>${escape(faq.title)}</summary>${faq.body_html}</details>`)
        .join("");
    });
</script>