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
- En el panel, entra en Integración y crea una clave pública (
hp_pub_…). - 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"
- 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/conversationsdesde 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 con403. 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 sinOrigin(servidor, app móvil) no se ven afectadas. - CORS está activado para
GETyPOST, con las cabecerasAuthorizationyContent-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 (salvobodyybody_htmlen 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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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>