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

TaskUsual owner
Create the form and define its fieldsMarketing, ecommerce, or the site owner
Paste the snippet into the siteDeveloper or agency
Set the recipients and the email copyWhoever answers the inquiries
Review submissions in KoruCustomer support or sales

In a small team the same person can cover every role.

Required credentials

CredentialScript attributeStatusWhere to find it
Website IDdata-website-idREQUIREDInstallation Code modal in the app's dashboard. The ID of the site the form is assigned to.
App IDdata-app-idREQUIREDSame modal. The ID of the Koru Contact Form app — identical for every form in the account.
Form ID (slug)data-custom-dataREQUIREDGenerated 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 URLdata-app-manager-urlREQUIREDSame modal. The host the widget uses to validate its authorization (<APP-MANAGER-URL>/api/auth/widget). Copy it exactly as Koru provides it.
API URLdata-api-urlOptional (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 AppsKoru 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 · urlrequired

URL of the widget bundle. The bundle is self-contained (React and ReactDOM are included).

data-website-idstring · uuidrequired

Site 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 · uuidrequired

App ID in Koru. If it is missing, the SDK throws Missing required data attributes and nothing renders.

data-custom-datastringrequired

Form 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 · urlrequired

Authorization host for the SDK. If it is missing, the SDK throws Missing required data attributes.

data-api-urlstring · urloptional

Included by Koru, ignored by the code.

asyncbooloptional

Recommended — Koru includes it. The script does not block rendering: the SDK waits for DOMContentLoaded, resolves authorization, and only then mounts the form.

Container attributes

classstringrequired

Must be exactly koru-contact-form: it is the selector the widget looks for (document.querySelector('.koru-contact-form')).

data-form-idstringoptional

Koru'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-form in 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 (position in Koru), not from your site's CSS.
  • The container can sit anywhere in the <body>. In Floating mode its position in the DOM is irrelevant, because the panel is displayed with position: fixed.

Configuration (how to use it)

Once installed, everything is managed from KoruConfigure AppsKoru 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

FieldDescription
formIdUnique 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.
titleForm 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_idTarget 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:

FieldDescription
idInternal identifier generated automatically (field_<timestamp>). It is the key the value is stored under in the submission.
labelVisible label. It is also the column name in the admin email and the variable name in templates (e.g. label: "Name"{{Name}}).
typeField type (see the table below).
requiredWhether the field is mandatory. Applied as native browser validation (required).
width100% (full-width field) or 50% (two fields per row).
optionsFor select only: comma-separated options, e.g. Sales, Support, General.

Types available in Koru:

typeWhat it rendersFields it uses
textPlain text inputlabel, required, width
emailEmail input with native browser format validationlabel, required, width
textareaMultiline text area (100px minimum height, resizable)label, required, width
numberNumeric inputlabel, required, width
selectDropdown with an initial "Select…" optionlabel, 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 placeholder or 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)

OptionWhat it doesFields 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.

FieldDescription
positionBubble 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_iconBubble icon. Envelope → envelope, User → user, Chat and Question → chat bubble (both fall back to the default icon). Ignored in Inline mode.
accent_colorColor of the submit button, the bubble, the input focus state, and the success icon. Set with a color picker or a hex value.
submit_textSubmit button label (e.g. Send inquiry). Rendered next to a paper plane icon.
success_msgMessage shown under the "Sent!" title after a successful submission.
redirect_urlOptional. 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)

FieldDescription
admin_emailRequired 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_lineSubject of the admin email. Supports variables (see below). If empty, Nuevo mensaje de <form title> is used.
autoresponder_enabledEnables 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_subjectSubject of the autoresponder. If empty: Gracias por tu contacto.
autoresponder_messageBody 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

VariableValue
{{<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

FieldDescription
isActive1 = active. With isActive = 0 the widget still renders the form, but submissions fail with 404 Form not found or inactive.
Deleting a formThis 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-form in the DOM).
  • Notifies by email only; there are no other outbound channels.
  • Offers five field types: text, email, textarea, number, and select.
  • Does not render placeholder or additional validation rules (min/max/regex).
  • Accepts select options only as a comma-separated string.
  • Detects the user's email from the first field of type email only.
  • 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 formId after 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 required and width defined.
  • There is a field of type email if you are going to use the autoresponder.
  • admin_email is set with the correct recipients.
  • subject_line reviewed, with variables spelled exactly like the field label.
  • Autoresponder configured (subject and message) if you are going to use it.
  • display_type and accent_color set; position and bubble_icon if floating.
  • submit_text and success_msg contain the final copy.
  • redirect_url set 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-id matches the site assigned to the form.
  • data-custom-data is 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 attributesdata-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.

Koru Contact Form — Developers · Koru Suite