Leads in
Inbound leads
Send estimate requests into Light AI from any form, ad platform or automation tool, with or without code.
Every request becomes a lead. A lead with a street address also becomes a project in the contractor's pipeline. Repeat requests never create duplicates: the same address and service on an open lead becomes a touch (lead.touched).
| Path | Use it for | Credential |
|---|---|---|
POST /v1/leads | Your own code, or any tool that can set an API key. | API key with leads:write |
POST /v1/inbound/{source_id} | HighLevel workflows, Zapier, Make, n8n, form tools. Any JSON shape (mapped). | Source secret in a header |
POST /v1/inbound/{source_id}/{token} | Senders that cannot set headers (Sweep&Go, some form tools). | Secret token in the URL |
POST /v1/inbound/google-ads/{source_id} | Google Ads lead form extensions. Setup. | Google's google_key |
| n8n templates | Meta Lead Ads and Google Local Services Ads. Setup. | API key |
Required fields#
A name plus an e-mail or a phone. Everything else is optional. A lead without a street address waits for one (needs_address: true) and converts to a project when you add it with PATCH /v1/leads/{id}. (The contractor's own website request form always asks the homeowner for an e-mail, a phone and the address; that does not change what the API requires.)
Inbound sources#
An inbound source is a receive-only credential: it can create leads and nothing else (it can never read data or change existing clients). The owner creates one in Settings → Developers → Inbound (at most 10 per organization; revoked sources do not count) and picks a mode.
Header mode (recommended)#
Send the secret as Authorization: Bearer lai_whin_… or X-Light-AI-Source-Secret: lai_whin_….
curl -X POST "https://api.light-ai.app/v1/inbound/src_…" \
-H "Authorization: Bearer $INBOUND_SOURCE_SECRET" \
-H "Content-Type: application/json" \
-d '{
"contact": { "first_name": "Dana", "last_name": "Reyes", "email": "dana@example.com", "phone": "512-555-0134" },
"address": { "line1": "4102 Oak Hollow Dr", "postal_code": "78731" },
"message": "Roofline, warm white"
}'This is the POST /v1/leads body shape, which every source accepts as is. A sender with its own field names (name, street, zip, …) needs the source's field map first: see Field mapping.
URL mode#
For senders that cannot set headers, the secret token is part of the URL: https://api.light-ai.app/v1/inbound/src_…/lai_whu_…. The URL itself is the credential and shows up in the sender's logs, so keep it private and rotate it if it leaks.
Responses#
Inbound sources always get a receipt ({"object":"receipt","status":"received"}), never the lead, so a sender cannot probe for existing customers. Validation errors name the field pointer without echoing values.
Capture and map#
A source accepts either the POST /v1/leads body or any JSON your tool already sends. To map a custom shape:
- Open the source in Settings → Developers → Inbound and click Capture a sample.
- Send one real request from your tool. Light AI shows the received body (kept 24 hours).
- Click each value onto a Light AI field. Optional transforms: trim, lowercase, phone to +1 format, first name(s) or last word of a full name, join a list.
- Save. Every later request uses the map. Unmapped values can go to
custom_fields.
| Light AI field | Notes |
|---|---|
contact.full_name, contact.first_name, contact.last_name | Map a full name to contact.full_name, or split it with the first-word and last-word transforms. |
contact.email, contact.phone | At least one. Phones are normalized to E.164. |
address.formatted or address.line1, line2, city, region, postal_code, country | A country name is normalized (United States → US). |
service_interest | christmas_lights, permanent_lighting, landscape_lighting or free text (kept raw). |
message, source_detail, submitted_at | |
external_ref.id, external_ref.namespace | Your record id: a repeat with the same id replays the original lead. |
consent.email, consent.sms, consent.marketing, consent.text | Stored verbatim for your compliance records. |
attribution.utm_source, utm_medium, utm_campaign, gclid, campaign_id | |
custom_fields.<key> | Anything else (letters, digits, _). |
Only the contact can refuse a lead. An unusable address or optional value is dropped (its raw value kept in custom_fields.inbound_*) rather than losing the request.
Limits#
Each source takes 60 requests a minute by default and a daily limit (default 500; at most 2,000 in header mode and 500 in URL mode). Above a limit the answer is 429 with Retry-After. Organizations also have a daily cap on created leads across all channels.
With an API key#
curl -X POST "https://api.light-ai.app/v1/leads" \
-H "Authorization: Bearer $LIGHT_AI_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"source": "zapier",
"contact": {
"first_name": "Dana",
"last_name": "Reyes",
"phone": "+1 512 555 0134"
},
"address": {
"line1": "4102 Oak Hollow Dr",
"postal_code": "78731",
"country": "US"
},
"service_interest": "permanent_lighting",
"external_ref": {
"system": "zapier",
"id": "form-entry-88"
}
}'const response = await fetch('https://api.light-ai.app/v1/leads', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LIGHT_AI_API_KEY}`,
'Idempotency-Key': crypto.randomUUID(),
'Content-Type': 'application/json',
},
body: JSON.stringify({
"source": "zapier",
"contact": {
"first_name": "Dana",
"last_name": "Reyes",
"phone": "+1 512 555 0134"
},
"address": {
"line1": "4102 Oak Hollow Dr",
"postal_code": "78731",
"country": "US"
},
"service_interest": "permanent_lighting",
"external_ref": {
"system": "zapier",
"id": "form-entry-88"
}
}),
});
const data = await response.json();import os, uuid, requests
response = requests.post(
"https://api.light-ai.app/v1/leads",
headers={
"Authorization": f"Bearer {os.environ['LIGHT_AI_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"source": "zapier",
"contact": {
"first_name": "Dana",
"last_name": "Reyes",
"phone": "+1 512 555 0134"
},
"address": {
"line1": "4102 Oak Hollow Dr",
"postal_code": "78731",
"country": "US"
},
"service_interest": "permanent_lighting",
"external_ref": {
"system": "zapier",
"id": "form-entry-88"
}
},
)
data = response.json()A key without leads:read also gets a receipt (202). Keep its rcpt_… id: you can add the address later with PATCH /v1/leads/rcpt_….
Paused organizations#
While an organization is paused its leads are held (held: true) and convert automatically when it resumes. Nothing is lost.