Your website keeps its own form, with its own look. You only change where it sends: to a Luxivo
address like https://luxivo.app/f/fk_…. Every message lands in the inbox as a conversation, your
team is notified, and you answer by e-mail from the inbox. When the customer replies, the answer
comes back into the same conversation.
Create a form in Contact forms, pick the website it belongs to, and copy the code from its Install section. A form accepts messages from the same websites as that website’s widget (Allowed websites under the widget’s General). The Free plan includes one form, Starter three, Pro ten and Business any number.
Plain HTML
Point the form’s action at your form’s address. After sending, the browser comes back to the page
in _redirect.
<form action="https://luxivo.app/f/fk_your_key" method="post">
<input name="name" required>
<input name="email" type="email" required>
<textarea name="message" required></textarea>
<input type="hidden" name="_redirect" value="https://example.com/contact?sent=1">
<input type="text" name="_gotcha" tabindex="-1" autocomplete="off" hidden>
<button>Send</button>
</form>
Without _redirect, the browser lands on a short thank-you page in your widget’s language. You can
also set a default page under the form’s Settings. Redirects only go to your allowed websites.
JavaScript
Send the form with fetch and show the result on the page. Ask for JSON with the Accept header:
const res = await fetch(form.action, {
method: "POST",
body: new FormData(form),
headers: { Accept: "application/json" },
});
const { ok, error } = await res.json();
ok is true when the message was saved. Otherwise error says what’s wrong in one sentence, and
errors lists every problem. JSON bodies (Content-Type: application/json) work too.
Fields
Use any field names. Luxivo recognizes the common ones and fills the conversation from them:
| What | Names |
|---|---|
email, e-mail, mail |
|
| Name | name, full_name, or first_name and last_name |
| Phone | phone, tel, telephone |
| Message | message, msg, comments |
| Subject | subject, _subject, topic |
Polish names (imie, nazwisko, telefon, wiadomosc, temat) work as well. Every other field is
kept too, like a hidden listing_id or a select. A message needs an e-mail address or a phone
number. Checkboxes that are ticked show as “Yes”, and fields with the same name are joined.
Under the form’s Fields you can give fields labels, put them in order and make them required. Give a checkbox a consent text, and the text is saved with every message where the box is ticked.
Page and campaign
Two hidden fields tell you where the message came from. Fill them when the page loads:
<input type="hidden" name="_page">
<input type="hidden" name="_referrer">
<script>
document.querySelector('[name="_page"]').value = location.href;
document.querySelector('[name="_referrer"]').value = document.referrer;
</script>
The inbox then shows the page and its utm_ campaign tags next to the message.
Spam
The hidden _gotcha field is a trap for bots: people never see it, so a message with it filled in is
dropped without an error. If your form already has a trap field, enter its name under Settings.
Each visitor can send up to 10 messages a minute, and only from your allowed websites.
Newsletter signup
Under Settings → Newsletter signup, pick a newsletter list. Senders who tick a checkbox named
newsletter join it, with the list’s e-mail confirmation if it has one. Describe the checkbox in
Fields with its consent text, and that text is stored as what they agreed to. See
Newsletter.
<label><input type="checkbox" name="newsletter"> Send me the newsletter</label>
Notifications and the automatic reply
Your team is notified like for any new conversation. Under Notifications you can also:
- send every message to addresses outside the team, such as an office mailbox,
- send messages to more people depending on a field, for example
agentequalsannagoes to Anna, - answer the customer right away with an automatic reply. Use
{{name}},{{email}},{{message}}or any field as{{field_name}}in it.
Replies and automatic replies go out with your website’s sender name, logo, color and footer. Set them in the widget’s E-mails section, where you can preview them and send yourself a test.
Replying
Form conversations are answered by e-mail. The reply box shows who it goes to and the subject. Enter starts a new line, and Cmd+Enter (Ctrl+Enter on Windows) sends. Each reply carries your name and company, the customer’s message quoted below and any files you attach. The AI button writes a draft, or improves, shortens, changes the tone of or translates what you wrote.
Export and webhooks
Messages lists everything sent with the form and exports it to CSV.
On Pro and Business, a webhook posts every message as JSON to your URL, for Zapier, Make or a CRM:
{
"id": "…",
"form": { "id": "…", "name": "Contact" },
"createdAt": "2026-10-27T16:43:00.000Z",
"email": "anna@example.com",
"name": "Anna Nowak",
"data": { "email": "anna@example.com", "message": "Is the flat still available?" },
"fields": [{ "name": "email", "label": "E-mail", "value": "anna@example.com" }],
"page": { "url": "https://example.com/contact", "utm": { "utm_source": "google" } },
"conversationUrl": "https://luxivo.app/app/inbox?c=…"
}
Each request has an X-Luxivo-Signature: t=<unix seconds>,v1=<hex> header. v1 is the
HMAC-SHA256 of <t>.<raw body> with the form’s webhook secret. Check it, and reject old timestamps:
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return v1?.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
Answer with any 2xx status. Other answers are tried again five times over about an hour, and the form’s Messages show the last status. Send a test posts a sample message.