Koru Contact Form
Formularios de contacto dinámicos con notificación por email y autorespuesta. Diseñás campos, estilo y avisos desde el panel de Koru — en el sitio se instala una sola vez.
Koru Contact Form permite crear formularios de contacto (campos, validación de obligatoriedad, ancho de columna, textos y colores) y recibir los envíos en un panel administrativo con listado completo, más notificación por email al administrador y autorespuesta opcional al usuario que completó el formulario. Incluye protección anti-spam por honeypot y guarda cada envío con un snapshot de la configuración vigente al momento del envío.
Todo el contenido y comportamiento —campos, diseño, emails— se configura desde el panel de Koru, sin volver a tocar el código del sitio. En el sitio se instala una sola vez.
Vas a necesitar tu Website ID required, tu App ID required, el Form ID required y la App Manager URL required. Los cuatro vienen resueltos en el código que emite el dashboard de Koru Contact Form → tu formulario → botón Copiar Código (modal Código de Instalación). No hace falta buscarlos uno por uno.
Qué no hace Koru Contact Form
No notifica por WhatsApp ni SMS: el único canal de salida es el email. No hereda los estilos de tu tienda (solo el color de acento es configurable). No admite más de un formulario por página. No expone un archivo de override CSS. Su única protección anti-spam es el honeypot; no incluye captcha.
Antes de empezar
Responsables recomendados
| Tarea | Responsable habitual |
|---|---|
| Crear el formulario y definir los campos | Marketing, ecommerce o dueño del sitio |
| Pegar el snippet en el sitio | Desarrollador o agencia |
| Cargar los destinatarios y el texto de los emails | Quien va a atender las consultas |
| Revisar los envíos en el panel | Atención al cliente o ventas |
En un equipo chico una misma persona puede cubrir todos los roles.
Credenciales requeridas
| Credencial | Atributo en el snippet | Estado | De dónde se saca |
|---|---|---|---|
| Website ID | data-website-id | REQUIRED | Modal Código de Instalación del dashboard de la app. Es el ID del sitio al que está asignado el formulario. |
| App ID | data-app-id | REQUIRED | Mismo modal. Es el ID de la app Koru Contact Form, igual para todos los formularios de la cuenta. |
| Form ID (slug) | data-custom-data | REQUIRED | Se autogenera al crear el formulario (formato koru-xxxxxxxxx) y se muestra en el header del builder como ID: koru-…. También lo emite el modal. |
| App Manager URL | data-app-manager-url | REQUIRED | Mismo modal. Es el host contra el que el widget valida su autorización (<APP-MANAGER-URL>/api/auth/widget). Copialo tal cual lo emite el panel. |
| API URL | data-api-url | Opcional (no se lee) | El panel lo emite, pero ni el SDK ni el widget lo leen: la URL del backend está fija en el build del widget. No afecta el funcionamiento. |
Los cuatro obligatorios salen del mismo lugar y ya vienen resueltos en el snippet, así que en la práctica no se cargan a mano: se copia el bloque completo.
El modal Código de Instalación solo se abre si Koru Suite valida que tu usuario tiene permisos sobre ese formulario y ese sitio. Si ves "Acceso denegado", el problema es de permisos en Koru, no del código.
Orden de implementación
Este es el recorrido completo, de punta a punta. Seguilo en orden para no dejar el formulario publicado a medias — sobre todo para que no haya envíos que no le lleguen a nadie.
Activá la app para el sitio en Koru
En el panel de Koru, la app tiene que estar activa para ese website. Si no lo está,
el widget no se monta: el SDK loguea Widget not authorized y la página no muestra nada.
Creá el formulario y definí los campos
El Form ID se genera en este paso, así que no se puede instalar antes de crear el
formulario. Armá los campos ahora y, si querés autorespuesta, incluí un campo de tipo
email desde el principio.
Cargá admin_email
Sin destinatario, los envíos se guardan pero no se manda ningún email. Es el paso que más se olvida.
Ajustá diseño y textos
Definí display_type, accent_color, el texto del botón y el mensaje de éxito. Si vas a
redirigir después del envío, cargá redirect_url.
Instalá el snippet en el sitio
Con el formulario ya armado, copiá el código y pegalo en tu sitio. El detalle está en Instalación.
Hacé un envío de prueba real
Completá el formulario desde el sitio publicado y verificá las tres cosas: que el envío aparezca en el panel, que llegue el email al administrador y —si la activaste— que llegue la autorespuesta al email que cargaste.
Revisá las variables del email
Si usás {{Nombre}} u otra variable de campo, confirmá en el email recibido que se
resolvió con el valor cargado y no quedó el texto crudo. Las variables usan el label
exacto del panel.
Instalación
Koru Contact Form se instala con un único método: el código de integración
<script>. Sirve para cualquier plataforma que permita insertar HTML en la página
(VTEX, Shopify, WordPress, Tiendanube, HTML propio).
Antes de instalar
El formulario ya tiene que estar creado —el snippet incluye su Form ID— y la app tiene que estar activa para ese sitio en Koru. Ver Orden de implementación.
La instalación tiene dos partes obligatorias
El <script> del widget y un <div class="koru-contact-form"> que marca dónde se
monta. Ese <div> es requerido incluso en modo botón flotante: sin él el widget no
se renderiza y registra un error en consola.
Copiá el código desde el dashboard de la app
El snippet lo emite la propia app: panel de Koru → Configurar Apps → Koru Contact Form → tu formulario → botón Copiar Código (modal Código de Instalación). Viene completo, con el Website ID, el App ID, el Form ID y la App Manager URL ya resueltos.
Dónde vive el dashboard
El dashboard de Koru Contact Form todavía no tiene dominio propio: hoy se accede en
https://koru-dashboard.pages.dev/. Cuando se le asigne el dominio definitivo, la URL
cambia; el resto del procedimiento queda igual.
No lo busques en el Código de integración de la ficha del sitio: ese generador es genérico para widgets embebibles y no sirve para esta app — no emite el Form ID ni el contenedor.
Pegalo en el <body> de tu sitio
Va en el lugar exacto donde querés que aparezca el formulario. En modo Inline el
formulario se renderiza dentro del <div>; en modo Floating el <div> solo actúa
como punto de montaje y el panel se posiciona con position: fixed.
Así se ve el código que emite el panel:
<!-- Contenedor del Widget -->
<div class="koru-contact-form" data-form-id="TU-FORM-ID"></div>
<!-- Koru SDK & Widget Script -->
<script
src="URL-DEL-WIDGET-QUE-EMITE-EL-PANEL"
data-website-id="TU-WEBSITE-ID"
data-app-id="TU-APP-ID"
data-custom-data="TU-FORM-ID"
data-api-url="URL-DE-API-QUE-EMITE-EL-PANEL"
data-app-manager-url="URL-APP-MANAGER-QUE-EMITE-EL-PANEL"
async
></script>Pegalo tal cual, sin sacarle atributos: data-api-url y el data-form-id del
contenedor hoy no se leen, pero forman parte del código que emite el panel.
Verificá que cargue
Recargá la página. En modo Inline vas a ver un spinner dentro del contenedor mientras
carga y después el formulario; en modo Floating, la burbuja aparece cuando el widget
terminó de montarse. Si no ves nada, revisá la consola del navegador y mirá la sección
No aparece nada en la página.
Usá el valor de src tal cual lo emite el panel. Hoy sale sin nombre de archivo
(termina en /); si el widget no carga y en la consola ves un error de red sobre esa
URL, probá agregándole /index.js al final.
Atributos del script
srcstring · urlrequiredURL del bundle del widget. El bundle es autocontenido (incluye React y ReactDOM).
data-website-idstring · uuidrequiredID del sitio en Koru. Debe coincidir con el sitio asignado al formulario en el panel;
si no coincide, el backend responde 403 Unauthorized website ID.
data-app-idstring · uuidrequiredID de la app en Koru. Si falta, el SDK lanza Missing required data attributes y no
renderiza nada.
data-custom-datastringrequiredForm ID (slug) del formulario a mostrar. Es lo que determina qué formulario carga.
Si se omite, el widget cae al app_id como identificador y la búsqueda de configuración
falla.
data-app-manager-urlstring · urlrequiredHost de autorización del SDK. Si falta, el SDK lanza Missing required data attributes.
data-api-urlstring · urloptionalEmitido por el panel, ignorado por el código.
asyncbooloptionalRecomendado — el panel lo emite. El script no bloquea el render: el SDK espera
DOMContentLoaded, resuelve la autorización y recién entonces monta el formulario.
Atributos del contenedor
classstringrequiredTiene que ser exactamente koru-contact-form: es el selector que busca el widget
(document.querySelector('.koru-contact-form')).
data-form-idstringoptionalEl snippet del panel lo incluye, pero el widget no lo lee: el formulario se resuelve
por el data-custom-data del script.
Reglas de posición e instancias
- Un solo formulario por página. El widget usa
querySelector, es decir el primer.koru-contact-formdel DOM. Dos scripts en la misma página apuntan al mismo contenedor y el segundo sobrescribe el render del primero. Para varios formularios, usá páginas distintas. - El posicionamiento del modo flotante se resuelve por configuración (
positionen el panel), no por CSS del sitio. - El contenedor puede ir en cualquier parte del
<body>. En modoFloatingsu posición en el DOM es indistinta, porque el panel se muestra conposition: fixed.
Configuración (cómo usar)
Una vez instalada, todo se maneja desde el panel de Koru → Configurar Apps →
Koru Contact Form. La app abre su propio dashboard —hoy en
https://koru-dashboard.pages.dev/, todavía sin dominio propio— con tres pestañas por
formulario: Campos, Diseño y Emails. Ningún cambio requiere volver a tocar el
sitio.
Identificación del formulario
| Campo | Descripción |
|---|---|
formId | Slug único autogenerado al crear el formulario (koru-xxxxxxxxx). Solo lectura: no se puede cambiar después de crear el formulario, y es el valor que va en data-custom-data. |
title | Nombre del formulario. Se muestra como encabezado del formulario en el sitio y se usa como variable {{FormName}} en los emails. Se guarda también en name. |
website_id | Sitio web destino, elegido de la lista de sitios de tu cuenta Koru (Sitio Web Destino en la pestaña Campos). Debe coincidir con el data-website-id del snippet. |
Campos (fields_config)
Cada campo del array tiene estas propiedades:
| Campo | Descripción |
|---|---|
id | Identificador interno autogenerado (field_<timestamp>). Es la clave con la que se guarda el valor en el envío. |
label | Etiqueta visible. También es el nombre de la columna en el email al administrador y el nombre de la variable en plantillas (ej.: label: "Nombre" → {{Nombre}}). |
type | Tipo de campo (ver tabla siguiente). |
required | Si es obligatorio. Se aplica como validación nativa del navegador (required). |
width | 100% (campo a ancho completo) o 50% (dos campos por fila). |
options | Solo para select: opciones separadas por coma, ej.: Consulta, Soporte, Ventas. |
Tipos disponibles en el panel:
type | Qué renderiza | Campos que usa |
|---|---|---|
text | Input de texto simple | label, required, width |
email | Input de email con validación de formato del navegador | label, required, width |
textarea | Área de texto multilínea (alto mínimo 100px, redimensionable) | label, required, width |
number | Input numérico | label, required, width |
select | Desplegable con opción inicial "Seleccionar…" | label, required, width, options |
Incluí al menos un campo type: email si querés autorespuesta
El backend detecta el email del usuario tomando el primer campo de tipo email. Sin
ese campo, la autorespuesta no se envía (la notificación al administrador sí) y tampoco
se setea el reply_to del email al admin.
Otras notas de comportamiento:
- Los campos no renderizan
placeholderni reglas de validación adicionales (min/max/regex), aunque el modelo de datos de la API las mencione. - El orden de los campos en el panel es el orden de render.
Diseño (layout_settings)
| Opción | Qué hace | Campos que usa |
|---|---|---|
display_type: "Inline" | El formulario se renderiza embebido dentro del <div class="koru-contact-form">, ancho 100% con máximo 500px y centrado. Es el valor por defecto. | accent_color, submit_text, success_msg, redirect_url |
display_type: "Floating" | Muestra una burbuja circular de 60px fija en pantalla; al hacer clic abre el formulario como modal centrado con fondo oscurecido y blur. | position, bubble_icon, accent_color, submit_text, success_msg, redirect_url |
Cualquier valor distinto de Inline se comporta como flotante.
| Campo | Descripción |
|---|---|
position | Ubicación de la burbuja. El panel ofrece Bottom-Right y Bottom-Left; el widget también interpreta Top-Right y Top-Left si el valor llega por API. Ignorado en modo Inline (el panel lo deshabilita). |
bubble_icon | Ícono de la burbuja. Envelope → sobre, User → usuario, Chat y Question → globo de chat (ambos caen en el ícono por defecto). Ignorado en modo Inline. |
accent_color | Color del botón de envío, de la burbuja, del foco de los inputs y del ícono de éxito. Se elige con color picker o hex. |
submit_text | Texto del botón de envío (ej.: Enviar consulta). Se muestra junto a un ícono de avión. |
success_msg | Mensaje bajo el título "¡Enviado!" después de un envío exitoso. |
redirect_url | Opcional. Si tiene valor, redirige a esa URL 2 segundos después del envío exitoso (ej.: https://ejemplo.com/gracias). Si queda vacío, el usuario se queda en la pantalla de éxito. |
Si accent_color queda vacío, el widget usa #4F46E5 (violeta): no hereda los
estilos de la tienda. El resto de la paleta (fondos, bordes, tipografías) es fija y no
configurable.
Emails (email_settings)
| Campo | Descripción |
|---|---|
admin_email | Obligatorio para que se envíen emails. Uno o varios destinatarios separados por coma (ventas@empresa.com, soporte@empresa.com). Si está vacío, el envío se guarda igual en el panel pero no se manda ningún email y queda registrado el error en el log del envío. |
subject_line | Asunto del email al administrador. Admite variables (ver abajo). Si queda vacío, se usa Nuevo mensaje de <título del formulario>. |
autoresponder_enabled | Activa la autorespuesta al usuario que completó el formulario. Requiere un campo de tipo email en el formulario. (autoresponder funciona como alias legacy del mismo toggle.) |
autoresponder_subject | Asunto de la autorespuesta. Si queda vacío: Gracias por tu contacto. |
autoresponder_message | Cuerpo de la autorespuesta, en texto plano. Los saltos de línea se respetan y se convierten en párrafos. Si queda vacío: Gracias por contactarnos. |
Los emails salen desde Koru Contact Form <contactform@korusuite.com>. Si el formulario
tiene un campo de tipo email, el aviso al administrador lleva reply_to con la
dirección que dejó el usuario, así que se puede responder directo desde el cliente de
correo; sin ese campo, el reply_to no se setea.
Variables disponibles en asuntos y mensajes
| Variable | Valor |
|---|---|
{{<label del campo>}} | El valor que cargó el usuario en ese campo, ej.: {{Nombre}}, {{Teléfono}}. Usa el label exacto configurado en el panel. |
{{Name}} | Primer valor disponible entre el campo con label Nombre Completo, el campo con label Nombre, o la clave name. Si no encuentra ninguno: Cliente. |
{{Email}} | Email detectado del usuario (vacío si no hay campo email). |
{{FormName}} | Título del formulario. |
{{PageURL}} | URL de la página desde donde se envió (N/A si no llega). |
{{Timestamp}} | Fecha y hora del envío. |
Ejemplo: subject_line: "Nueva consulta de {{Nombre}} — {{FormName}}".
Estado del formulario
| Campo | Descripción |
|---|---|
isActive | 1 = activo. Con isActive = 0 el widget sigue renderizando el formulario, pero el envío falla con 404 Form not found or inactive. |
| Eliminar formulario | Es un borrado lógico: el formulario deja de cargar en el sitio (404 Form deleted) y los envíos históricos se conservan para auditoría. |
Datos y privacidad
- Cada envío se guarda en Koru junto con un snapshot de la configuración vigente al momento del envío, así un cambio posterior de campos no altera el histórico.
- Se registra la URL de la página desde donde se envió (disponible como
{{PageURL}}) y la fecha y hora ({{Timestamp}}). - Eliminar el formulario es un borrado lógico: los envíos históricos se conservan.
- Los envíos marcados como spam por el honeypot quedan archivados en el panel y no disparan emails.
- El resultado del envío de cada email queda registrado en el log del envío.
Límites funcionales
La versión actual:
- Muestra un solo formulario por página (el widget toma el primer
.koru-contact-formdel DOM). - Notifica solo por email; no hay otros canales de salida.
- Ofrece cinco tipos de campo:
text,email,textarea,numberyselect. - No renderiza
placeholderni validaciones adicionales (min/max/regex). - Acepta las opciones de un
selectúnicamente como string separado por comas. - Detecta el email del usuario solo desde el primer campo de tipo
email. - Permite configurar únicamente el color de acento; el resto de la paleta y la tipografía son fijas.
- No expone handles ni variables CSS, y no tiene archivo de override.
- No incluye captcha: la protección anti-spam es el honeypot.
- No permite cambiar el
formIddespués de crear el formulario. - Tiene los textos de la interfaz fijos en español (
Seleccionar…,¡Enviado!, los mensajes de error): no son traducibles. Lo que cargás vos —labels, botón, mensaje de éxito, asuntos y cuerpo de la autorespuesta— sí puede estar en cualquier idioma.
Checklist antes de publicar
- El formulario tiene todos los campos que necesitás, con
requiredywidthdefinidos. - Hay un campo de tipo
emailsi vas a usar autorespuesta. -
admin_emailcargado con los destinatarios correctos. -
subject_linerevisado, con las variables escritas igual que ellabeldel campo. - Autorespuesta configurada (asunto y mensaje) si la vas a usar.
-
display_typeyaccent_colordefinidos;positionybubble_iconsi es flotante. -
submit_textysuccess_msgcon el texto final. -
redirect_urlcargada si querés mandar a una página de gracias. - El
<div class="koru-contact-form">está en la página, también si es flotante. -
data-website-iddel snippet coincide con el sitio asignado al formulario. -
data-custom-dataes el Form ID de este formulario. - La app figura activa para ese sitio en el panel de Koru.
- Envío de prueba hecho: aparece en el panel y llegan los emails.
- Una sola instancia del snippet por página.
Preguntas frecuentes
¿Cuánto tarda en verse un cambio de configuración?
Los cambios de campos, diseño y emails se leen del backend en cada carga de página, sin caché propia: se ven al recargar el sitio.
Lo que sí se cachea es la autorización del widget (que la app esté habilitada para
ese sitio), en localStorage bajo la clave koru_widget_<WEBSITE-ID>_<APP-ID>, con TTL
de 1 hora. Para forzarlo antes, desde la consola del navegador:
localStorage.removeItem('koru_widget_TU-WEBSITE-ID_TU-APP-ID');
location.reload();Aparte, el archivo del widget se sirve con caché de navegador de 1 hora y caché de CDN larga; eso afecta actualizaciones del código del widget, no tu configuración.
¿Afecta la performance del sitio?
No. El script es async: no bloquea el render ni el parseo del HTML. Todo el trabajo es
client-side y arranca después de DOMContentLoaded — una request de autorización a Koru
y otra de configuración al backend, y recién entonces se monta el formulario. El bundle
es autocontenido (incluye React y ReactDOM), así que no depende de que el sitio tenga
React ni de otra app de Koru. En modo Inline, mientras tanto se muestra un spinner
dentro del contenedor.
No aparece nada en la página. ¿Qué reviso?
En orden, mirando la consola del navegador:
Falta el contenedor
[ContactFormWidget] No container found. Please add <div class="koru-contact-form"></div> to your page.
→ Agregá el <div>, también en modo flotante.
Faltan atributos
[Widget SDK] Missing required data attributes → data-website-id, data-app-id y
data-app-manager-url son obligatorios los tres.
App no autorizada o inactiva en Koru
El SDK loguea Widget not authorized y no renderiza. Verificá que la app esté activa
para ese sitio en el panel de Koru y que el Website ID sea el correcto.
Error de configuración
Se muestra el cartel rojo No se pudo cargar la configuración del formulario. Casos
típicos: el data-custom-data no corresponde a ningún formId (404 Form not found),
el formulario fue eliminado (404 Form deleted), o el data-website-id no coincide con
el sitio asignado al formulario en el panel (403 Unauthorized website ID).
Dos formularios en la misma página
Solo se monta uno; separalos en páginas distintas.
El formulario se ve pero al enviar da error
El envío responde 404 Form not found or inactive si el formulario está inactivo o
eliminado.
Hay un proceso automático diario (00:00 UTC) que desactiva (isActive = 0) todos los
formularios cuyo sitio ya no existe en Koru. Si el sitio se dio de baja o se recreó con
otro ID, reasigná el sitio al formulario y reactivalo.
Llega el email al administrador pero no la autorespuesta
Tres condiciones tienen que cumplirse: autoresponder_enabled activado, el formulario
tiene un campo de tipo email, y el usuario lo completó. El detector toma el primer
campo type: "email" — un campo de texto llamado "Email" no alcanza.
No llega ningún email pero el envío aparece en el panel
Es el comportamiento esperado cuando admin_email está vacío. El envío se guarda
siempre; el email es un proceso posterior e independiente, y su resultado queda
registrado en el log del envío. Si admin_email está cargado y aun así no llega, revisá
spam y confirmá el estado del envío en el panel.
Recibo envíos vacíos o sospechosos
El widget incluye un honeypot: un campo oculto _trap. Si un bot lo completa, el
envío se marca como spam, se archiva y no dispara emails, mientras el navegador ve
la pantalla de éxito. Esos registros quedan en el panel marcados como spam.
Las opciones de un select no aparecen
options es un string separado por comas (Consulta, Soporte, Ventas), no una lista de
objetos. Cada opción se recorta de espacios y se usa como label y value.
El formulario no toma los colores ni la tipografía de mi tienda
Es intencional: el widget aplica sus propios estilos inline (fondo blanco, bordes
grises, tipografía Inter con fallback a la del sistema) y solo el color de acento es
configurable. No hereda estilos del theme ni expone un archivo de override.
¿Puedo mostrar el mismo formulario en varias páginas?
Sí: el mismo snippet en cada página. La restricción es una instancia por página, no un formulario por sitio.
Datos útiles al pedir soporte
Informá el Website ID, el App ID, el Form ID, la URL de la página donde pasa, el horario aproximado y el mensaje que ves en la consola del navegador. No hace falta —ni conviene— mandar datos personales de los envíos.