Light AIDocs Beta Open Light AI

API reference

POSTCreate a lead

POST /v1/leads: Creates an inbound estimate request.

POSThttps://api.light-ai.app/v1/leads

Creates an inbound estimate request. Never loses a request: a repeat request for the same address and service on an open lead becomes a touch (lead.touched, HTTP 200), an external_ref your integration already sent replays the original lead (HTTP 200, replayed: true), and a paused organization holds the lead (converted on resume).

Without leads:read every answer is the same receipt (HTTP 202), so the response never reveals whether a person already exists. A lead without an address is stored and becomes a project when one is added.

Scopeleads:write
CredentialAPI key: Authorization: Bearer lai_live_… (or lai_test_…). Partner keys add Light-AI-Organization.
Test modeWorks with test keys against simulated test objects.
IdempotencyIdempotency-Key supported. See Idempotency.

Headers#

Light-AI-Organizationstring#
Partner keys: the connected organization to act for (org_…). Organization keys may omit it. Id prefix org_.
Light-AI-Versionstring#
Pin a dated API version (defaults to the key's version). One of: 2026-11-01.
Idempotency-Keystring#
Retry-safe writes: the same key and body replay the first response for 24 hours (Idempotent-Replayed: true). 8–128 characters.

Request body#

sourcestringrequired#
Where the request came from. One of: api, google_ads_lead_form, google_lsa, meta_lead_ad, highlevel, hubspot, jobber, service_autopilot, realgreen, sweepandgo, deeplawn, zapier, n8n, make, crm_import, other.
source_detailstring | null#
At most 200 characters.
external_refobject | null#
Your id for this request. Retrying with the same ref returns the original lead.
external_ref.systemstringrequired#
Lowercase system name, e.g. jobber, hubspot, highlevel.
external_ref.namespacestring | null#
Optional sub-namespace in that system (e.g. an account id). null or "": none. At most 120 characters.
external_ref.idstringrequired#
1–255 characters.
contactobjectrequired#
A name, and an email or a phone. null or "" in an optional field is the same as leaving it out.
contact.first_namestring | null#
At most 100 characters.
contact.last_namestring | null#
At most 100 characters.
contact.emailstring | null#
Normalized when valid. An address that is not a valid email is kept as contact.email_raw when the phone works. At most 254 characters.
contact.phonestring | null#
Any common format; normalized to E.164. A number that cannot be normalized is kept as phone_raw when the email works. At most 40 characters.
contact.phone_verifiedboolean | null#
addressobject | null#
Optional. A lead without a street address is stored (a partial address as a hint) and becomes a project when one is added.
address.line1string | null#
At most 200 characters.
address.line2string | null#
At most 200 characters.
address.citystring | null#
At most 120 characters.
address.regionstring | null#
State or region, e.g. TX. At most 120 characters.
address.postal_codestring | null#
At most 20 characters.
address.countrystring | null#
ISO 3166-1 alpha-2 in any case, or a common name ("USA", "United States"); normalized to alpha-2. Defaults to US. At most 60 characters.
address.formattedstring | null#
A one-line address, when components are not available. At most 500 characters.
address.latnumber | null#
-90–90.
address.lngnumber | null#
-180–180.
address.place_idstring | null#
Google place id, when known (best dedupe key). At most 300 characters.
service_intereststring | null#
One of christmas_lights, permanent_lighting, landscape_lighting, custom; other text is kept and filed as custom. At most 200 characters.
messagestring | null#
At most 5000 characters.
attributionobject | null#
utm_*, gclid, campaign ids and similar.
custom_fieldsobject | null#
submitted_attimestamp | null#
RFC 3339 date-time with offset.
is_testboolean | null#
Live keys reject true (use a test key for test data).

Response 200#

A touch on an open lead, or the replay of your external_ref.

idstring#
Id prefix lt_.
objectstring#
Always lead_touch.
livemodeboolean#
false for objects created with a test key.
lead_idstring#
Id prefix lead_.
sourcestring#
One of: website_widget, api, google_ads_lead_form, google_lsa, meta_lead_ad, highlevel, hubspot, jobber, service_autopilot, realgreen, sweepandgo, deeplawn, zapier, n8n, make, crm_import, other, manual.
source_detailstring | null#
contactobject | null#
messagestring | null#
attributionobject#
custom_fieldsobject#
external_refobject | null#
external_ref.systemstringrequired#
external_ref.namespacestring | null#
external_ref.idstringrequired#
submitted_attimestamp#
RFC 3339 timestamp (UTC, milliseconds).
created_attimestamp#
RFC 3339 timestamp (UTC, milliseconds).
updated_attimestamp#
RFC 3339 timestamp (UTC, milliseconds).
replayedboolean#
Always true.
statusstring#
One of: new, converted, spam, archived, duplicate.
heldboolean#
Accepted while the organization was paused; converts automatically on resume.
needs_addressboolean#
A lead without a street address (none, or only a ZIP, city or region) waits for one (PATCH /v1/leads/{id} with address).
addressobject | null#
address.line1string | nullrequired#
address.line2string | nullrequired#
address.citystring | nullrequired#
address.regionstring | nullrequired#
address.postal_codestring | nullrequired#
address.countrystring | nullrequired#
address.formattedstringrequired#
Always present. Components may be null for addresses entered as one line.
address.latnumber | nullrequired#
address.lngnumber | nullrequired#
service_intereststring | null#
One of: christmas_lights, permanent_lighting, landscape_lighting, custom.
service_interest_rawstring | null#
notesstring | null#
status_reasonstring | null#
client_idstring | null#
Id prefix cli_.
property_idstring | null#
Id prefix prop_.
possible_client_matchobject | null#
An existing client with the same email or phone that this integration was not allowed to update.
possible_client_match.client_idstringrequired#
Id prefix cli_.
projectobject | null#
project.idstringrequired#
Id prefix prj_.
project.statusstringrequired#
One of: intake, measuring, mockup, quote, approved, completed, archived.
project.pipeline_stagestring | nullrequired#
One of: request, quote_drafted, quote_sent, approved, completed, closed_lost.
project.app_urlstring | nullrequired#
formobject | null#
The website request form a website_widget lead came from (null for other sources).
form.idstringrequired#
Id prefix form_.
form.namestringrequired#
mockup_statusstring#
Instant preview of a website lead: not_requested (the form has no preview), pending (generating), ready (at least one image), failed, or skipped (a fallback applied: low credits, a cap, Off-Season, pause, plan). lead.mockup_ready fires once when it leaves pending. One of: not_requested, pending, ready, failed, skipped.
form_answersobject[]#
Every website request form answer in the form's question order, with its question, group, raw value (the same value as custom_fields[key]) and display labels. Branch questions appear only when they were shown (e.g. tree_count only when trees_bushes is true). Answers to questions the form no longer has come last. An empty array for other sources.
form_answers[].keystringrequired#
Stable answer key, the same key as in custom_fields (e.g. surfaces, primary_color, tree_count, custom_…).
form_answers[].questionstringrequired#
The question as the form asked it (the key when the form no longer has that question).
form_answers[].groupstringrequired#
Answer group: lighting, trees (trees & bushes), decor (wreaths & garland), details or custom (the contractor's own questions). More groups may be added.
form_answers[].typestring | nullrequired#
Question type (null when the form no longer has the question). One of: color, choice, multi_choice, toggle, text.
form_answers[].valueanyrequired#
The raw answer, identical to custom_fields[key]: a choice value, an array of values, true/false or text.
form_answers[].labelsstring[]required#
Display labels of the answer in order (toggles: Yes or No; text: the text).
preview_looksstring[]#
The look (light color value, e.g. warm_white) of the request's instant preview: an array with at most one item (one AI mockup per request; leads sent before October 2026 may list up to 3, in order). Empty when there is no preview.
offerobject | null#
The website request form's offer (for example "Black Friday: 20% off installs") that was showing when the homeowner sent the request, kept as it was then; null when no offer was active and for other sources. label is "20% off", "$150 off" or empty (a headline-only offer). When the request becomes a project, the quote builder pre-fills the quote discount from it once (the contractor can change or remove it); the published quote shows it as discount with a label such as "Black Friday offer".
offer.templatestring | null#
The Light AI campaign template that filled the offer; null or absent for the contractor's own. One of: classic, early_bird, black_friday, last_chance.
offer.headlinestringrequired#
offer.discount_typestringrequired#
One of: percent, amount.
offer.discount_percentinteger | nullrequired#
offer.discount_amountinteger | nullrequired#
offer.labelstringrequired#
offer.ends_atstring | nullrequired#
offer.book_bystring | nullrequired#
offer.captured_atstring | nullrequired#
assetsobject#
The homeowner's photo and the instant preview images (each with its look). GET, list and search answers carry 60-minute signed URLs; webhook payloads and the events feed carry the ids with url null (fetch the lead for fresh links).
assets.photosobject[]required#
assets.photos[].idstringrequired#
Asset id (lph_… photo, mk_… preview image).
assets.photos[].urlstring | nullrequired#
Signed HTTPS URL valid for 60 minutes (null in webhook payloads, or when the image was removed).
assets.photos[].expires_attimestamp | nullrequired#
RFC 3339 timestamp (UTC, milliseconds).
assets.mockupsobject[]required#
assets.mockups[].idstringrequired#
Asset id (lph_… photo, mk_… preview image).
assets.mockups[].urlstring | nullrequired#
Signed HTTPS URL valid for 60 minutes (null in webhook payloads, or when the image was removed).
assets.mockups[].expires_attimestamp | nullrequired#
RFC 3339 timestamp (UTC, milliseconds).
assets.mockups[].lookstring | null#
The look (light color) of this preview image: warm_white, cool_white, multi, candy_cane, red_green, red_white_green, red, green, blue or blue_white (more may be added). The preview uses the color the homeowner picked (one per request; older leads may have up to 3). Null when unknown.
external_refsobject[]#
external_refs[].systemstringrequired#
external_refs[].namespacestring | nullrequired#
external_refs[].idstringrequired#
external_refs[].ownerstringrequired#
self: written by your integration (editable); others are read-only. One of: self, organization, partner.
external_refs[].read_onlybooleanrequired#
touch_countinteger#
last_touched_attimestamp | null#
RFC 3339 timestamp (UTC, milliseconds).
fixtureboolean#
Test-mode fixture object. Always true.

Response 201#

Lead created.

idstringrequired#
Id prefix lead_.
objectstringrequired#
Always lead.
livemodebooleanrequired#
false for objects created with a test key.
statusstringrequired#
One of: new, converted, spam, archived, duplicate.
heldbooleanrequired#
Accepted while the organization was paused; converts automatically on resume.
needs_addressbooleanrequired#
A lead without a street address (none, or only a ZIP, city or region) waits for one (PATCH /v1/leads/{id} with address).
sourcestringrequired#
One of: website_widget, api, google_ads_lead_form, google_lsa, meta_lead_ad, highlevel, hubspot, jobber, service_autopilot, realgreen, sweepandgo, deeplawn, zapier, n8n, make, crm_import, other, manual.
source_detailstring | nullrequired#
contactobjectrequired#
contact.first_namestring | nullrequired#
contact.last_namestring | nullrequired#
contact.emailstring | nullrequired#
contact.email_rawstring | null#
The address as received when it is not a valid email (the lead was kept because its phone works).
contact.phonestring | nullrequired#
E.164, or null when the number could not be normalized (see phone_raw).
contact.phone_rawstring | nullrequired#
The number as received (extensions, local or foreign formats are kept here).
contact.phone_verifiedboolean | nullrequired#
addressobject | nullrequired#
address.line1string | nullrequired#
address.line2string | nullrequired#
address.citystring | nullrequired#
address.regionstring | nullrequired#
address.postal_codestring | nullrequired#
address.countrystring | nullrequired#
address.formattedstringrequired#
Always present. Components may be null for addresses entered as one line.
address.latnumber | nullrequired#
address.lngnumber | nullrequired#
service_intereststring | nullrequired#
One of: christmas_lights, permanent_lighting, landscape_lighting, custom.
service_interest_rawstring | nullrequired#
messagestring | nullrequired#
attributionobjectrequired#
custom_fieldsobjectrequired#
notesstring | nullrequired#
status_reasonstring | nullrequired#
client_idstring | nullrequired#
Id prefix cli_.
property_idstring | nullrequired#
Id prefix prop_.
possible_client_matchobject | nullrequired#
An existing client with the same email or phone that this integration was not allowed to update.
possible_client_match.client_idstringrequired#
Id prefix cli_.
projectobject | nullrequired#
project.idstringrequired#
Id prefix prj_.
project.statusstringrequired#
One of: intake, measuring, mockup, quote, approved, completed, archived.
project.pipeline_stagestring | nullrequired#
One of: request, quote_drafted, quote_sent, approved, completed, closed_lost.
project.app_urlstring | nullrequired#
formobject | null#
The website request form a website_widget lead came from (null for other sources).
form.idstringrequired#
Id prefix form_.
form.namestringrequired#
mockup_statusstring#
Instant preview of a website lead: not_requested (the form has no preview), pending (generating), ready (at least one image), failed, or skipped (a fallback applied: low credits, a cap, Off-Season, pause, plan). lead.mockup_ready fires once when it leaves pending. One of: not_requested, pending, ready, failed, skipped.
form_answersobject[]#
Every website request form answer in the form's question order, with its question, group, raw value (the same value as custom_fields[key]) and display labels. Branch questions appear only when they were shown (e.g. tree_count only when trees_bushes is true). Answers to questions the form no longer has come last. An empty array for other sources.
form_answers[].keystringrequired#
Stable answer key, the same key as in custom_fields (e.g. surfaces, primary_color, tree_count, custom_…).
form_answers[].questionstringrequired#
The question as the form asked it (the key when the form no longer has that question).
form_answers[].groupstringrequired#
Answer group: lighting, trees (trees & bushes), decor (wreaths & garland), details or custom (the contractor's own questions). More groups may be added.
form_answers[].typestring | nullrequired#
Question type (null when the form no longer has the question). One of: color, choice, multi_choice, toggle, text.
form_answers[].valueanyrequired#
The raw answer, identical to custom_fields[key]: a choice value, an array of values, true/false or text.
form_answers[].labelsstring[]required#
Display labels of the answer in order (toggles: Yes or No; text: the text).
preview_looksstring[]#
The look (light color value, e.g. warm_white) of the request's instant preview: an array with at most one item (one AI mockup per request; leads sent before October 2026 may list up to 3, in order). Empty when there is no preview.
offerobject | null#
The website request form's offer (for example "Black Friday: 20% off installs") that was showing when the homeowner sent the request, kept as it was then; null when no offer was active and for other sources. label is "20% off", "$150 off" or empty (a headline-only offer). When the request becomes a project, the quote builder pre-fills the quote discount from it once (the contractor can change or remove it); the published quote shows it as discount with a label such as "Black Friday offer".
offer.templatestring | null#
The Light AI campaign template that filled the offer; null or absent for the contractor's own. One of: classic, early_bird, black_friday, last_chance.
offer.headlinestringrequired#
offer.discount_typestringrequired#
One of: percent, amount.
offer.discount_percentinteger | nullrequired#
offer.discount_amountinteger | nullrequired#
offer.labelstringrequired#
offer.ends_atstring | nullrequired#
offer.book_bystring | nullrequired#
offer.captured_atstring | nullrequired#
assetsobject#
The homeowner's photo and the instant preview images (each with its look). GET, list and search answers carry 60-minute signed URLs; webhook payloads and the events feed carry the ids with url null (fetch the lead for fresh links).
assets.photosobject[]required#
assets.photos[].idstringrequired#
Asset id (lph_… photo, mk_… preview image).
assets.photos[].urlstring | nullrequired#
Signed HTTPS URL valid for 60 minutes (null in webhook payloads, or when the image was removed).
assets.photos[].expires_attimestamp | nullrequired#
RFC 3339 timestamp (UTC, milliseconds).
assets.mockupsobject[]required#
assets.mockups[].idstringrequired#
Asset id (lph_… photo, mk_… preview image).
assets.mockups[].urlstring | nullrequired#
Signed HTTPS URL valid for 60 minutes (null in webhook payloads, or when the image was removed).
assets.mockups[].expires_attimestamp | nullrequired#
RFC 3339 timestamp (UTC, milliseconds).
assets.mockups[].lookstring | null#
The look (light color) of this preview image: warm_white, cool_white, multi, candy_cane, red_green, red_white_green, red, green, blue or blue_white (more may be added). The preview uses the color the homeowner picked (one per request; older leads may have up to 3). Null when unknown.
external_refsobject[]required#
external_refs[].systemstringrequired#
external_refs[].namespacestring | nullrequired#
external_refs[].idstringrequired#
external_refs[].ownerstringrequired#
self: written by your integration (editable); others are read-only. One of: self, organization, partner.
external_refs[].read_onlybooleanrequired#
touch_countintegerrequired#
last_touched_attimestamp | nullrequired#
RFC 3339 timestamp (UTC, milliseconds).
submitted_attimestamprequired#
RFC 3339 timestamp (UTC, milliseconds).
created_attimestamprequired#
RFC 3339 timestamp (UTC, milliseconds).
updated_attimestamprequired#
RFC 3339 timestamp (UTC, milliseconds).
replayedboolean#
Present when an external_ref replay returned an existing lead. Always true.
fixtureboolean#
Test-mode fixture object. Always true.

Response 202#

Receipt (credential without leads:read).

objectstringrequired#
Always receipt.
idstringrequired#
Id prefix rcpt_.
statusstringrequired#
Always received.
livemodebooleanrequired#
false for objects created with a test key.

Errors#

StatusMeaning
400Bad request. bad_request, test_mode_mismatch, test_mode_unsupported, unsupported_api_version
401Missing or invalid credential. authentication_required, invalid_api_key, api_key_revoked, api_key_expired, api_key_suspended
403Not allowed. insufficient_scope, plan_upgrade_required, organization_paused, organization_off_season, api_disabled, api_access_not_enabled, ip_not_allowed, forbidden
404Not found (also for objects of other organizations and for organizations without an active grant).
409Conflict. stale_state, conflict, idempotency_in_progress, idempotency_scope_mismatch, quote_already_decided, quote_not_current, billing_review_required
413Request bodies are limited to 256 KB.
415The body is not JSON: send Content-Type: application/json. unsupported_media_type
422Validation failed. idempotency_key_reused
429Rate limited or daily cap reached. See Retry-After and the RateLimit headers. rate_limited, daily_cap_reached
500Unexpected server error. Safe to retry with exponential backoff; quote the Request-Id to support. internal_error
503Temporarily unavailable. Retry with exponential backoff. service_unavailable