Skip to main content

Website forms

Connect a form on your own website straight to CloseUp, with a public site key, an origin allow-list and built-in spam protection.

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.

On WordPress, use the dedicated CloseUp WordPress plugin instead of the snippets on this page.

Set up a website and a form

  1. Add your website
    In CloseUp, go to Lead Sources → Website forms and click Add website. Enter a name and the site's Domain.
  2. Add a form
    Click Add form. Choose the source, campaign and assignment for its leads, and, if your inputs have unusual names, fill in the Field mapping.
  3. Install
    Click 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.
  4. Test
    Submit 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.
The snippet is copied onto your site. CloseUp does not load it from a central URL, so it never updates itself. After you rotate the key or change settings, copy the snippet again. A current snippet contains the marker 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:

js
(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:

ContactForm.tsx
"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:

http
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
}
bash
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

FieldTypeNotes
siteKeystringRequired, here or in the X-CloseUp-Site-Key header
fieldsobjectAll form inputs, keyed by input name. A flat body ({ "siteKey": "...", "name": "...", "email": "..." }) also works
attributionobject, optionalpageUrl, 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
elapsedMsnumber, optionalMilliseconds since the form was shown. Used by the minimum-fill-time check
captchaTokenstringRequired only when the website has a CAPTCHA turned on
Idempotency-Key headerstring, optionalOr 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 fieldBuilt-in input names (sample)
Full namename, full_name, your-name, שם, שם מלא
First / last namefirst_name, fname, last_name, surname
Emailemail, e-mail, mail, your-email, אימייל, מייל
Phonephone, tel, mobile, phone_number, your-phone, טלפון, נייד
Companycompany, company_name, organization, business, חברה
Messagemessage, 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

200 OK
{ "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" }:

errorHTTPMeaning
UNKNOWN_FORM404The form ID is not valid
INVALID_SITE_KEY401The site key is missing, or it does not own this form
INTEGRATION_DISABLED, FORM_DISABLED403The website or the form is switched off in CloseUp
ORIGIN_NOT_ALLOWED403The request came from a domain outside the allowed origins
CAPTCHA_FAILED403The CAPTCHA token is missing or was rejected
RATE_LIMITED429Too many submissions. See limits below
PAYLOAD_TOO_LARGE413Body over 64 KB, or more than 100 fields
MALFORMED_PAYLOAD400The body is not a JSON object
MISSING_CONTACT, INVALID_EMAIL, INVALID_PHONE422No usable email or phone
INTERNAL_ERROR500Server 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 use display: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.

LimitValue
Requests per site key120 per minute, across all its forms
Requests per visitor IP, per form10 per minute
Body size64 KB
Fields per submission100

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:

OptionWhat happens
Record a new enquiry on the existing leadThe enquiry is added to the existing lead as its own activity and note
Fill in missing details on the existing leadSame, and empty fields on the lead are filled. Existing values are never overwritten
Log activity onlyA timeline entry only
IgnoreThe 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.