Light AIDocs Beta Open Light AI

API reference

PATCHUpdate a lead

PATCH /v1/leads/{lead_id}: Change the status (new, archived, spam), notes or reason, or add the street address of a lead that has none yet (no address, or only a ZIP or city): it then converts to a project and lead.updated reports the address.

PATCHhttps://api.light-ai.app/v1/leads/{lead_id}

Change the status (new, archived, spam), notes or reason, or add the street address of a lead that has none yet (no address, or only a ZIP or city): it then converts to a project and lead.updated reports the address. Archiving with reason test also archives its untouched intake project; nothing is ever deleted. Without leads:read only leads your own credential created can be changed, and the answer is a receipt. Intake-only keys: address the request by the receipt id POST /v1/leads answered with (/v1/leads/rcpt_…) to add the address later; a receipt id only takes address (and source_system) and only changes a lead your credential created. Without leads:read every receipt answer is the same 200 receipt (also when the request was folded into someone else's lead, which is never changed, or the lead already has its street address).

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.

Path parameters#

lead_idstringrequired#
The lead id (lead_…), or the receipt id (rcpt_…) your own credential's request was answered with.

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#

statusstring#
archived with reason test also archives the untouched intake project; new reopens a spam or archived lead. One of: new, archived, spam.
notesstring | null#
null or "" clears the notes. At most 5000 characters.
reasonstring | null#
null or "": none. At most 200 characters.
addressobject#
Adds the street address of a lead that has none yet (no address, or only a ZIP or city); a street address converts the lead.
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.
source_systemstring | null#
The system that caused this write (shown as origin.source_system on the resulting events, for loop prevention).

Response 200#

The updated lead (or a receipt without leads:read).

idstring#
Id prefix lead_.
objectstring#
Always lead.
livemodeboolean#
false for objects created with a test key.
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).
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#
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 | 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#
messagestring | null#
attributionobject#
custom_fieldsobject#
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).
submitted_attimestamp#
RFC 3339 timestamp (UTC, milliseconds).
created_attimestamp#
RFC 3339 timestamp (UTC, milliseconds).
updated_attimestamp#
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.

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