Website forms
התיעוד למפתחים כתוב באנגלית.
Website capture turns a form on your own site into a CloseUp lead. The browser posts straight to CloseUp. You need no server of your own, and no CRM secret goes into the page. It works with AI site builders such as Lovable, any site that allows custom JavaScript, React and Next.js apps, and plain REST calls from a backend.
Set up a website and a form
- Add your websiteIn CloseUp, go to Lead Sources → Website forms and click Add website. Enter a name and the site's Domain.
- Add a formClick Add form. Choose the source, campaign and assignment for its leads, and, if your inputs have unusual names, fill in the Field mapping.
- InstallClick Install. Pick a tab: AI site builder, JavaScript, React / Next.js, REST or Developer guide. Each one is generated from your live settings, with the real endpoint, site key and field names filled in.
- TestSubmit a test entry on your site. It appears under Submissions for the form and in Recent deliveries.
Site key and allowed origins
- The site key is public by design. It ships in your visitors' browsers. It identifies your company and its only power is to submit to this website's forms. It grants no read access to anything.
- A key only works with its own forms. The form ID and the site key must belong to the same website, so a key can never reach another company's form.
- Rotate when needed. Rotate issues a new key. The old key stops working immediately, so update the snippet on every form on the site.
- Allowed origins. When the website has a Domain, only browser requests from that domain and its subdomains are accepted (
www.is ignored). Leave it empty to accept any origin.
CloseUp website capture v2.AI site builders (Lovable and similar)
The AI site builder tab gives you a ready prompt. Paste it into Lovable or a similar builder. It tells the builder the endpoint, the site key, the body shape, which input names to use, the honeypot field, and to send a fresh Idempotency-Key per submission.
After the builder makes the change, submit a test entry and check Submissions. If the lead is refused, the submission row shows why.
Plain JavaScript
Use the JavaScript tab on any site that allows custom code. Paste it below your form and change #contact-form to your form's selector. The generated snippet also keeps UTM values and click IDs (gclid, fbclid, gbraid, wbraid, msclkid) in a first-party cookie for 90 days, first touch wins, so attribution survives page changes. The core of it looks like this:
(function () {
var ENDPOINT = "https://YOUR-CLOSEUP-APP/api/public/forms/YOUR_FORM_ID/submissions";
var SITE_KEY = "YOUR_SITE_KEY";
var form = document.querySelector("#contact-form"); // your form selector
if (!form) return;
var openedAt = Date.now();
form.addEventListener("submit", async function (event) {
event.preventDefault();
var fields = {};
new FormData(form).forEach(function (value, key) { fields[key] = value; });
try {
var response = await fetch(ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID()
},
body: JSON.stringify({
siteKey: SITE_KEY,
fields: fields,
elapsedMs: Date.now() - openedAt,
attribution: { pageUrl: location.href, referrer: document.referrer }
})
});
if (!response.ok) throw new Error("submit_failed");
form.reset();
// Show your own success message here.
} catch (error) {
// Show your own error message here.
console.error(error);
}
});
})();Also add the honeypot input inside the form: a text input named _hp (or the name shown in your snippet) with tabindex="-1", autocomplete="off" and aria-hidden="true", positioned off-screen. It must stay invisible to people and always be submitted empty. The generated snippet includes it ready to paste.
React / Next.js
The React / Next.js tab gives you a complete client component. A trimmed version:
"use client"
import { useRef, useState } from "react"
const ENDPOINT = "https://YOUR-CLOSEUP-APP/api/public/forms/YOUR_FORM_ID/submissions"
const SITE_KEY = "YOUR_SITE_KEY"
export function ContactForm() {
const openedAt = useRef(Date.now())
const [status, setStatus] = useState<"idle" | "sending" | "sent" | "error">("idle")
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const form = event.currentTarget
setStatus("sending")
try {
const response = await fetch(ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
siteKey: SITE_KEY,
fields: Object.fromEntries(new FormData(form).entries()),
elapsedMs: Date.now() - openedAt.current,
attribution: { pageUrl: window.location.href, referrer: document.referrer },
}),
})
if (!response.ok) throw new Error("submit_failed")
setStatus("sent")
form.reset()
} catch {
setStatus("error")
}
}
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Full name" required />
<input name="email" type="email" placeholder="Email" />
<input name="phone" type="tel" placeholder="Phone" />
<textarea name="message" placeholder="Message" />
{/* Spam trap: keep hidden, never remove. */}
<input
name="_hp"
tabIndex={-1}
autoComplete="off"
aria-hidden="true"
style={{ position: "absolute", left: "-9999px", opacity: 0, height: 0, width: 0 }}
/>
<button type="submit" disabled={status === "sending"}>Send</button>
</form>
)
}REST
Post directly from a backend or an automation tool. The endpoint takes the form ID in the path:
POST https://YOUR-CLOSEUP-APP/api/public/forms/{formId}/submissions
Content-Type: application/json
X-CloseUp-Site-Key: YOUR_SITE_KEY
Idempotency-Key: 6a0f3c1e-2b4d-4e8a-9c7f-1d3b5a7e9c20
{
"fields": {
"name": "Dana Levi",
"email": "dana@example.com",
"phone": "050-1234567",
"message": "I'd like a quote"
},
"attribution": {
"pageUrl": "https://example.com/contact?utm_source=google",
"referrer": "https://www.google.com/"
},
"elapsedMs": 9000
}curl -X POST "https://YOUR-CLOSEUP-APP/api/public/forms/YOUR_FORM_ID/submissions" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"siteKey":"YOUR_SITE_KEY","fields":{"name":"Dana Levi","phone":"0501234567"}}'Request
| Field | Type | Notes |
|---|---|---|
siteKey | string | Required, here or in the X-CloseUp-Site-Key header |
fields | object | All form inputs, keyed by input name. A flat body ({ "siteKey": "...", "name": "...", "email": "..." }) also works |
attribution | object, optional | pageUrl, referrer, utmSource, utmMedium, utmCampaign, utmTerm, utmContent, gclid, gbraid, wbraid, fbclid, msclkid. Missing UTM values and click IDs are recovered from pageUrl. referrer falls back to the Referer header |
elapsedMs | number, optional | Milliseconds since the form was shown. Used by the minimum-fill-time check |
captchaToken | string | Required only when the website has a CAPTCHA turned on |
Idempotency-Key header | string, optional | Or idempotencyKey in the body. Up to 200 characters. Recommended |
A URL-encoded form post also works. In that case siteKey, captchaToken, elapsedMs and idempotencyKey are taken out of the field list automatically.
Field names
CloseUp maps your inputs to lead fields. Matching ignores case, spaces, dashes, dots and underscores, so Full Name, full_name and fullname are the same. Built-in names include:
| Lead field | Built-in input names (sample) |
|---|---|
| Full name | name, full_name, your-name, שם, שם מלא |
| First / last name | first_name, fname, last_name, surname |
email, e-mail, mail, your-email, אימייל, מייל | |
| Phone | phone, tel, mobile, phone_number, your-phone, טלפון, נייד |
| Company | company, company_name, organization, business, חברה |
| Message | message, comments, notes, inquiry, your-message, הודעה |
Add your own names in the form's Field mapping. Inputs that match nothing are kept with the lead as custom fields, so no answer is lost. Every submission needs a valid email or phone.
Response
{ "ok": true, "submissionId": "c41e9a2b-7d3f-4b6e-8a1c-5f2d0e9b7a63", "duplicate": false }duplicate: true means the request repeated an Idempotency-Key that was already processed, and you get the original submissionId back. The lead ID is never returned, because any script on the page can read this response.
Errors use the shape { "ok": false, "error": "CODE" }:
error | HTTP | Meaning |
|---|---|---|
UNKNOWN_FORM | 404 | The form ID is not valid |
INVALID_SITE_KEY | 401 | The site key is missing, or it does not own this form |
INTEGRATION_DISABLED, FORM_DISABLED | 403 | The website or the form is switched off in CloseUp |
ORIGIN_NOT_ALLOWED | 403 | The request came from a domain outside the allowed origins |
CAPTCHA_FAILED | 403 | The CAPTCHA token is missing or was rejected |
RATE_LIMITED | 429 | Too many submissions. See limits below |
PAYLOAD_TOO_LARGE | 413 | Body over 64 KB, or more than 100 fields |
MALFORMED_PAYLOAD | 400 | The body is not a JSON object |
MISSING_CONTACT, INVALID_EMAIL, INVALID_PHONE | 422 | No usable email or phone |
INTERNAL_ERROR | 500 | Server error. Retry with the same Idempotency-Key |
Spam protection and CAPTCHA
- Honeypot. A hidden input (default name
_hp) that people never see. If it has any value, the submission is treated as spam. Hide it off-screen; do not usedisplay:none, and never remove it. - Minimum fill time. When set for the website, a form submitted faster than the minimum (based on
elapsedMs) is treated as spam. - CAPTCHA (optional). Cloudflare Turnstile or Google reCAPTCHA can be added on top. It is turned on per website by the CloseUp team on request. When it is on, every submission must include
captchaToken, and a missing or rejected token is refused.
Spam is not answered with an error. A honeypot or fill-time hit returns a normal 200 and is stored with status Spam under Submissions, so a bot learns nothing and you can review false positives.
| Limit | Value |
|---|---|
| Requests per site key | 120 per minute, across all its forms |
| Requests per visitor IP, per form | 10 per minute |
| Body size | 64 KB |
| Fields per submission | 100 |
Duplicates and retries
CloseUp identifies a lead by its phone or email, so a submission from an existing contact never creates a second lead. The website setting Submission from an existing contact (a form can override it) decides what happens:
| Option | What happens |
|---|---|
| Record a new enquiry on the existing lead | The enquiry is added to the existing lead as its own activity and note |
| Fill in missing details on the existing lead | Same, and empty fields on the lead are filled. Existing values are never overwritten |
| Log activity only | A timeline entry only |
| Ignore | The submission is stored as rejected and nothing changes on the lead |
Send a fresh Idempotency-Key for every new submission and reuse the same value when you retry after a network error. A retry then returns the original submission instead of being processed twice.
Privacy
The visitor's IP address is not stored. CloseUp keeps only a salted hash, per company, for abuse checks. The user agent is truncated, and control characters are stripped from every value.