Integration guide

One contract for every managed marketing site

Public endpoint

POST /api/public/submit-lead with Content-Type: application/json. No API key or secret is ever sent from the browser — the site is identified by its public site key, and the request origin must match the site's allowed origins.

Production endpoint (after this hub is published)

https://project--82a3bf1c-584d-472c-851b-9ae02b6370e0.lovable.app/api/public/submit-lead

Preview endpoint (live now, unpublished hub)

https://project--82a3bf1c-584d-472c-851b-9ae02b6370e0-dev.lovable.app/api/public/submit-lead

Accepted leads are emailed to calebtrevino@bearseoservice.com.

Required and optional fields

  • Required: siteKey, submissionId, name, service, and at least one of email or phone.
  • Recommended: startedAt (form mount timestamp), pageUrl, referrer, utm.*, location, timing, message.
  • Spam traps: include a hidden company_website input; submissions with it filled, or completed in under 3 seconds, are silently discarded (the visitor still sees success).
  • Idempotency: reuse the same submissionId across retries so a lead is never duplicated. Same-contact repeats inside 30 minutes are also merged.
  • Rate limit: 20 submissions per hour per site + visitor fingerprint; excess returns 429.

Exact JSON payload

Example body for a site

{
  "siteKey": "ks_your_site_key",
  "submissionId": "b7b0f0f2-2f4d-4f0b-9f0a-3f6f2b6a1c11",
  "startedAt": 1755112800000,
  "name": "Jane Homeowner",
  "email": "jane@example.com",
  "phone": "555-201-8890",
  "location": "60004",
  "service": "Driveway and patio wash",
  "timing": "Within 2 weeks",
  "message": "Two-car driveway plus back patio.",
  "company_website": "",
  "pageUrl": "https://example.com/quote?utm_source=google",
  "referrer": "https://www.google.com/",
  "utm": {
    "source": "google",
    "medium": "cpc",
    "campaign": "spring-wash",
    "content": "ad-a",
    "term": "pressure washing near me"
  }
}

curl smoke test

curl -X POST https://project--82a3bf1c-584d-472c-851b-9ae02b6370e0-dev.lovable.app/api/public/submit-lead \
  -H "Content-Type: application/json" \
  -d '{ "siteKey": "ks_your_site_key", "submissionId": "b7b0f0f2-2f4d-4f0b-9f0a-3f6f2b6a1c11", "startedAt": 1755112800000, "name": "Jane Homeowner", "email": "jane@example.com", "phone": "555-201-8890", "location": "60004", "service": "Driveway and patio wash", "timing": "Within 2 weeks", "message": "Two-car driveway plus back patio.", "company_website": "", "pageUrl": "https://example.com/quote?utm_source=google", "referrer": "https://www.google.com/", "utm": { "source": "google", "medium": "cpc", "campaign": "spring-wash", "content": "ad-a", "term": "pressure washing near me" } }'

Client snippet

Drop-in fetch handler

// Marketing site form — no secrets ship to the browser.
const startedAt = Date.now();             // set once when the form mounts
const submissionId = crypto.randomUUID(); // one per form attempt, reused on retry

async function submitLead(values) {
  const params = new URLSearchParams(window.location.search);
  const res = await fetch("https://project--82a3bf1c-584d-472c-851b-9ae02b6370e0.lovable.app/api/public/submit-lead", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      siteKey: "ks_your_site_key",
      submissionId,
      startedAt,
      name: values.name,          // required
      email: values.email,        // email OR phone required
      phone: values.phone,
      location: values.zip,
      service: values.service,    // required
      timing: values.timing,
      message: values.message,
      company_website: values.company_website, // honeypot: hidden + must stay empty
      pageUrl: window.location.href,
      referrer: document.referrer,
      utm: {
        source: params.get("utm_source"),
        medium: params.get("utm_medium"),
        campaign: params.get("utm_campaign"),
        content: params.get("utm_content"),
        term: params.get("utm_term"),
      },
    }),
  });

  const data = await res.json().catch(() => ({}));
  if (res.ok && data.ok) return showThankYou();
  showRetryMessage(); // never surface raw errors to visitors
}

Response behavior

  • Success: 200 { "ok": true } — show a thank-you state.
  • Rejected: generic code only (invalid_request, origin_not_allowed, unknown_site). Show a friendly retry message.
  • Rate-limited: 429 — ask the visitor to try again shortly.

Site keys — active (0)