Koru Contact Form
Dynamic contact forms with admin email notifications and optional autoresponder. Fields, styling, and alerts are configured in Koru — you install it on your site once.
Koru Contact Form lets you build contact forms (fields, required validation, column width, copy, and colors) and receive every submission in an admin panel, with email notifications to the administrator and an optional autoresponder to the person who filled out the form. It includes honeypot spam protection and stores each submission together with a snapshot of the configuration in effect at the time of the submission.
All content and behavior — fields, design, emails — is configured from Koru, with no further changes to your site's code. You install it once.
You need a Website ID required, an App ID required, a Form ID required, and the App Manager URL required. All four come already filled in inside the code generated by the Koru Contact Form dashboard → your form → Copy Code (the Installation Code modal). There is no need to collect them one by one.
What Koru Contact Form does not do
It does not notify over WhatsApp or SMS: email is the only outbound channel. It does not inherit your store's styles (only the accent color is configurable). It does not support more than one form per page. It exposes no CSS override file. Its only spam protection is the honeypot; there is no captcha.
The widget interface is Spanish-only
The strings rendered by the widget are fixed in Spanish and cannot be translated: "Seleccionar…" in dropdowns, "¡Enviado!" on the success screen, and the error notice "No se pudo cargar la configuración del formulario.". The email fallbacks are Spanish too. Every value you configure — labels, button text, success message, subjects, and the autoresponder body — is your own copy, so you can write those in any language.
Before you start
Recommended owners
| Task | Usual owner |
|---|---|
| Create the form and define its fields | Marketing, ecommerce, or the site owner |
| Paste the snippet into the site | Developer or agency |
| Set the recipients and the email copy | Whoever answers the inquiries |
| Review submissions in Koru | Customer support or sales |
In a small team the same person can cover every role.
Required credentials
| Credential | Script attribute | Status | Where to find it |
|---|---|---|---|
| Website ID | data-website-id | REQUIRED | Installation Code modal in the app's dashboard. The ID of the site the form is assigned to. |
| App ID | data-app-id | REQUIRED | Same modal. The ID of the Koru Contact Form app — identical for every form in the account. |
| Form ID (slug) | data-custom-data | REQUIRED | Generated automatically when you create the form (format koru-xxxxxxxxx) and shown in the builder header as ID: koru-…. Also included in the modal. |
| App Manager URL | data-app-manager-url | REQUIRED | Same modal. The host the widget uses to validate its authorization (<APP-MANAGER-URL>/api/auth/widget). Copy it exactly as Koru provides it. |
| API URL | data-api-url | Optional (not read) | Koru includes it, but neither the SDK nor the widget reads it: the backend URL is fixed in the widget build. It has no effect. |
All four required values come from the same place and are already resolved in the snippet, so in practice you never enter them by hand: you copy the whole block.
The Installation Code modal only opens if Koru Suite confirms your user has permissions over that form and that site. If you see "Access denied", it is a permissions issue in Koru, not a problem with the code.
Implementation order
This is the end-to-end path. Follow it in order so you never publish a half-finished form — above all, so no submission ends up with nobody to receive it.
Activate the app for the site in Koru
The app must be active for that website in Koru. If it is not, the widget never
mounts: the SDK logs Widget not authorized and the page shows nothing.
Create the form and define its fields
The Form ID is generated in this step, so you cannot install before creating the form.
Lay out the fields now and, if you want the autoresponder, include a field of type email
from the start.
Set admin_email
Without a recipient, submissions are stored but no email is sent. This is the most commonly missed step.
Adjust design and copy
Set display_type, accent_color, the button label, and the success message. If you plan
to redirect after submission, set redirect_url.
Install the snippet on your site
With the form already built, copy the code and paste it into your site. The details are in Installation.
Run a real test submission
Fill in the form on the published site and verify all three: the submission appears in Koru, the admin email arrives, and — if you enabled it — the autoresponder reaches the address you entered.
Check the email variables
If you use {{Name}} or any field variable, confirm in the received email that it
resolved to the submitted value instead of leaving the raw text. Variables use the exact
label configured in Koru.
Installation
Koru Contact Form has a single installation method: the <script> integration
code. It works on any platform that allows inserting HTML into the page — VTEX,
Shopify, WordPress, Tiendanube, or your own HTML.
Before installing
The form must already be created — the snippet embeds its Form ID — and the app must be active for that site in Koru. See Implementation order.
Installation has two required parts
The widget <script> and a <div class="koru-contact-form"> that marks the mount
point. That <div> is required even in floating button mode: without it the widget
does not render and logs an error to the console.
Copy the code from the app's dashboard
The snippet is generated by the app itself: Koru → Configure Apps → Koru Contact Form → your form → Copy Code (the Installation Code modal). It comes complete, with the Website ID, App ID, Form ID, and App Manager URL already filled in.
Where the dashboard lives
The Koru Contact Form dashboard does not have its own domain yet: today it is reachable
at https://koru-dashboard.pages.dev/. Once the final domain is assigned the URL will
change; the rest of the procedure stays the same.
Do not look for it under the integration code on the website record: that generator is generic for embeddable widgets and does not work for this app — it emits neither the Form ID nor the container.
Paste it into your site's <body>
Place it exactly where the form should appear. In Inline mode the form renders inside
the <div>; in Floating mode the <div> only acts as the mount point and the panel is
positioned with position: fixed.
This is what the generated code looks like:
<!-- Widget container -->
<div class="koru-contact-form" data-form-id="YOUR-FORM-ID"></div>
<!-- Koru SDK & Widget Script -->
<script
src="WIDGET-URL-PROVIDED-BY-KORU"
data-website-id="YOUR-WEBSITE-ID"
data-app-id="YOUR-APP-ID"
data-custom-data="YOUR-FORM-ID"
data-api-url="API-URL-PROVIDED-BY-KORU"
data-app-manager-url="APP-MANAGER-URL-PROVIDED-BY-KORU"
async
></script>Paste it as is, without removing attributes: data-api-url and the container's
data-form-id are not read today, but they are part of the code Koru generates.
Confirm it loads
Reload the page. In Inline mode you will see a spinner inside the container while it
loads and then the form; in Floating mode the bubble appears once the widget has
finished mounting. If nothing shows up, check the browser console and see
Nothing renders on the page.
Use the src value exactly as Koru provides it. Today it comes without a file name
(it ends in /); if the widget does not load and the console shows a network error for
that URL, try appending /index.js to it.
Script attributes
srcstring · urlrequiredURL of the widget bundle. The bundle is self-contained (React and ReactDOM are included).
data-website-idstring · uuidrequiredSite ID in Koru. It must match the site assigned to the form in Koru; otherwise the
backend responds 403 Unauthorized website ID.
data-app-idstring · uuidrequiredApp ID in Koru. If it is missing, the SDK throws Missing required data attributes and
nothing renders.
data-custom-datastringrequiredForm ID (slug) of the form to display. This is what determines which form loads. If
it is omitted, the widget falls back to app_id as the identifier and the configuration
lookup fails.
data-app-manager-urlstring · urlrequiredAuthorization host for the SDK. If it is missing, the SDK throws
Missing required data attributes.
data-api-urlstring · urloptionalIncluded by Koru, ignored by the code.
asyncbooloptionalRecommended — Koru includes it. The script does not block rendering: the SDK waits for
DOMContentLoaded, resolves authorization, and only then mounts the form.
Container attributes
classstringrequiredMust be exactly koru-contact-form: it is the selector the widget looks for
(document.querySelector('.koru-contact-form')).
data-form-idstringoptionalKoru's snippet includes it, but the widget does not read it: the form is resolved from
the script's data-custom-data.
Position and instance rules
- One form per page. The widget uses
querySelector, meaning the first.koru-contact-formin the DOM. Two scripts on the same page target the same container and the second one overwrites the first render. For multiple forms, use separate pages. - Floating mode placement is resolved from the configuration (
positionin Koru), not from your site's CSS. - The container can sit anywhere in the
<body>. InFloatingmode its position in the DOM is irrelevant, because the panel is displayed withposition: fixed.
Configuration (how to use it)
Once installed, everything is managed from Koru → Configure Apps → Koru Contact
Form. The app opens its own dashboard — today at https://koru-dashboard.pages.dev/,
still without its own domain — with three tabs per form: Fields, Design, and
Emails. No change requires touching your site again.
Form identification
| Field | Description |
|---|---|
formId | Unique slug generated when the form is created (koru-xxxxxxxxx). Read-only: it cannot be changed afterwards, and it is the value used in data-custom-data. |
title | Form name. It is shown as the form heading on your site and used as the {{FormName}} variable in emails. It is also stored in name. |
website_id | Target website, selected from the sites in your Koru account (Target Website in the Fields tab). It must match the snippet's data-website-id. |
Fields (fields_config)
Every field in the array has these properties:
| Field | Description |
|---|---|
id | Internal identifier generated automatically (field_<timestamp>). It is the key the value is stored under in the submission. |
label | Visible label. It is also the column name in the admin email and the variable name in templates (e.g. label: "Name" → {{Name}}). |
type | Field type (see the table below). |
required | Whether the field is mandatory. Applied as native browser validation (required). |
width | 100% (full-width field) or 50% (two fields per row). |
options | For select only: comma-separated options, e.g. Sales, Support, General. |
Types available in Koru:
type | What it renders | Fields it uses |
|---|---|---|
text | Plain text input | label, required, width |
email | Email input with native browser format validation | label, required, width |
textarea | Multiline text area (100px minimum height, resizable) | label, required, width |
number | Numeric input | label, required, width |
select | Dropdown with an initial "Select…" option | label, required, width, options |
Include at least one type: email field if you want the autoresponder
The backend detects the user's email from the first field of type email. Without
that field the autoresponder is not sent (the admin notification still is) and the admin
email's reply_to is not set either.
Other confirmed behavior:
- Fields do not render a
placeholderor additional validation rules (min/max/regex), even though the API data model mentions them. - The field order in Koru is the render order.
Design (layout_settings)
| Option | What it does | Fields it uses |
|---|---|---|
display_type: "Inline" | The form renders embedded inside <div class="koru-contact-form">, 100% width with a 500px maximum, centered. This is the default. | accent_color, submit_text, success_msg, redirect_url |
display_type: "Floating" | Shows a 60px circular bubble fixed on screen; clicking it opens the form as a centered modal with a dimmed, blurred backdrop. | position, bubble_icon, accent_color, submit_text, success_msg, redirect_url |
Any value other than Inline behaves as floating.
| Field | Description |
|---|---|
position | Bubble placement. Koru offers Bottom-Right and Bottom-Left; the widget also interprets Top-Right and Top-Left if the value arrives through the API. Ignored in Inline mode (Koru disables it). |
bubble_icon | Bubble icon. Envelope → envelope, User → user, Chat and Question → chat bubble (both fall back to the default icon). Ignored in Inline mode. |
accent_color | Color of the submit button, the bubble, the input focus state, and the success icon. Set with a color picker or a hex value. |
submit_text | Submit button label (e.g. Send inquiry). Rendered next to a paper plane icon. |
success_msg | Message shown under the "Sent!" title after a successful submission. |
redirect_url | Optional. When set, the user is redirected to that URL 2 seconds after a successful submission (e.g. https://example.com/thank-you). When empty, the user stays on the success screen. |
If accent_color is empty, the widget uses #4F46E5 (violet): it does not inherit
your store's styles. The rest of the palette — backgrounds, borders, typography — is fixed
and not configurable.
Emails (email_settings)
| Field | Description |
|---|---|
admin_email | Required for any email to be sent. One or more comma-separated recipients (sales@company.com, support@company.com). If it is empty, the submission is still stored in Koru but no email is sent, and the error is recorded in the submission log. |
subject_line | Subject of the admin email. Supports variables (see below). If empty, Nuevo mensaje de <form title> is used. |
autoresponder_enabled | Enables the autoresponder to the person who submitted the form. Requires a field of type email in the form. (autoresponder works as a legacy alias for the same toggle.) |
autoresponder_subject | Subject of the autoresponder. If empty: Gracias por tu contacto. |
autoresponder_message | Body of the autoresponder, in plain text. Line breaks are preserved and converted into paragraphs. If empty: Gracias por contactarnos. |
Emails are sent from Koru Contact Form <contactform@korusuite.com>. If the form has a
field of type email, the admin notification sets reply_to to the address the user
provided, so you can reply directly from your email client; without that field, reply_to
is not set.
Variables available in subjects and messages
| Variable | Value |
|---|---|
{{<field label>}} | The value the user entered in that field, e.g. {{Name}}, {{Phone}}. Uses the exact label configured in Koru. |
{{Name}} | First available value among the field labeled Nombre Completo, the field labeled Nombre, or the name key. If none is found: Cliente. |
{{Email}} | Detected user email (empty when there is no email field). |
{{FormName}} | Form title. |
{{PageURL}} | URL of the page the submission came from (N/A when not provided). |
{{Timestamp}} | Submission date and time. |
Example: subject_line: "New inquiry from {{Name}} — {{FormName}}".
Form state
| Field | Description |
|---|---|
isActive | 1 = active. With isActive = 0 the widget still renders the form, but submissions fail with 404 Form not found or inactive. |
| Deleting a form | This is a soft delete: the form stops loading on the site (404 Form deleted) and historical submissions are kept for auditing. |
Data and privacy
- Every submission is stored in Koru together with a snapshot of the configuration in effect at that moment, so later field changes do not alter the history.
- The page URL the submission came from (available as
{{PageURL}}) and the date and time ({{Timestamp}}) are recorded. - Deleting a form is a soft delete: historical submissions are kept.
- Submissions flagged as spam by the honeypot are archived in Koru and do not trigger any email.
- The result of each email send is recorded in the submission log.
Functional limits
The current version:
- Displays one form per page (the widget takes the first
.koru-contact-formin the DOM). - Notifies by email only; there are no other outbound channels.
- Offers five field types:
text,email,textarea,number, andselect. - Does not render
placeholderor additional validation rules (min/max/regex). - Accepts
selectoptions only as a comma-separated string. - Detects the user's email from the first field of type
emailonly. - Allows configuring the accent color only; the rest of the palette and the typography are fixed.
- Exposes no handles or CSS variables, and has no override file.
- Includes no captcha: spam protection is the honeypot.
- Does not allow changing the
formIdafter the form is created. - Ships its interface strings fixed in Spanish (
Seleccionar…,¡Enviado!, the error notices): they are not translatable. The copy you configure — labels, button, success message, subjects, and autoresponder body — can be in any language.
Checklist before going live
- The form has every field you need, with
requiredandwidthdefined. - There is a field of type
emailif you are going to use the autoresponder. -
admin_emailis set with the correct recipients. -
subject_linereviewed, with variables spelled exactly like the fieldlabel. - Autoresponder configured (subject and message) if you are going to use it.
-
display_typeandaccent_colorset;positionandbubble_iconif floating. -
submit_textandsuccess_msgcontain the final copy. -
redirect_urlset if you want to send users to a thank-you page. - The
<div class="koru-contact-form">is on the page, including in floating mode. - The snippet's
data-website-idmatches the site assigned to the form. -
data-custom-datais the Form ID of this form. - The app is active for that site in Koru.
- Test submission done: it appears in Koru and the emails arrive.
- Only one instance of the snippet per page.
Frequently asked questions
How long does a configuration change take to appear?
Field, design, and email changes are read from the backend on every page load, with no caching of their own: they appear as soon as you reload the site.
What is cached is the widget authorization (whether the app is enabled for that site),
in localStorage under the key koru_widget_<WEBSITE-ID>_<APP-ID>, with a 1 hour TTL.
To force it earlier, from the browser console:
localStorage.removeItem('koru_widget_YOUR-WEBSITE-ID_YOUR-APP-ID');
location.reload();Separately, the widget file is served with a 1 hour browser cache and a long CDN cache; that affects updates to the widget code, not your configuration.
Does it affect site performance?
No. The script is async: it blocks neither rendering nor HTML parsing. All the work
happens client-side and starts after DOMContentLoaded — one authorization request to
Koru and one configuration request to the backend, and only then is the form mounted. The
bundle is self-contained (React and ReactDOM included), so it does not depend on your site
using React or on any other Koru app. In Inline mode a spinner is shown inside the
container in the meantime.
Nothing renders on the page. What should I check?
In this order, using the browser console:
The container is missing
[ContactFormWidget] No container found. Please add <div class="koru-contact-form"></div> to your page.
→ Add the <div>, including in floating mode.
Attributes are missing
[Widget SDK] Missing required data attributes → data-website-id, data-app-id, and
data-app-manager-url are all three required.
The app is not authorized or not active in Koru
The SDK logs Widget not authorized and does not render. Confirm the app is active for
that site in Koru and that the Website ID is correct.
Configuration error
The red notice No se pudo cargar la configuración del formulario. is displayed. Typical
causes: data-custom-data does not match any formId (404 Form not found), the form was
deleted (404 Form deleted), or data-website-id does not match the site assigned to the
form in Koru (403 Unauthorized website ID).
Two forms on the same page
Only one is mounted; split them across separate pages.
The form renders but submitting fails
Submissions respond 404 Form not found or inactive when the form is inactive or deleted.
A daily automated process (00:00 UTC) deactivates (isActive = 0) every form whose site no
longer exists in Koru. If the site was removed or recreated with a different ID, reassign
the site to the form and reactivate it.
The admin email arrives but the autoresponder does not
Three conditions must be met: autoresponder_enabled is on, the form has a field of type
email, and the user filled it in. The detector takes the first type: "email" field — a
text field named "Email" is not enough.
No email arrives but the submission shows up in Koru
That is the expected behavior when admin_email is empty. The submission is always
stored; sending email is a separate, later process, and its result is recorded in the
submission log. If admin_email is set and the email still does not arrive, check spam and
confirm the submission status in Koru.
I receive empty or suspicious submissions
The widget includes a honeypot: a hidden _trap field. If a bot fills it in, the
submission is flagged as spam, archived, and does not trigger any email, while the
browser still sees the success screen. Those records remain in Koru marked as spam.
The options of a select do not show up
options is a comma-separated string (Sales, Support, General), not a list of objects.
Each option is trimmed and used as both label and value.
The form does not pick up my store's colors or typography
This is intentional: the widget applies its own inline styles (white background, gray
borders, Inter with a system font fallback) and only the accent color is
configurable. It does not inherit theme styles and exposes no override file.
Can I show the same form on several pages?
Yes: use the same snippet on each page. The limit is one instance per page, not one form per site.
Useful details when contacting support
Report the Website ID, App ID, Form ID, the URL of the page where it happens, the approximate time, and the message you see in the browser console. There is no need — and it is not advisable — to send personal data from the submissions.