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-leadPreview endpoint (live now, unpublished hub)
https://project--82a3bf1c-584d-472c-851b-9ae02b6370e0-dev.lovable.app/api/public/submit-leadAccepted 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.