Light AIDocs Beta Open Light AI

API reference

POSTRecord an approval made outside Light AI

POST /v1/quotes/{quote_id}/mark_approved: For a homeowner who approved in your CRM or field app (e.g.

POSThttps://api.light-ai.app/v1/quotes/{quote_id}/mark_approved

For a homeowner who approved in your CRM or field app (e.g. Jobber). Works on the current published revision while it is sent; 409 quote_already_decided if the homeowner already decided in Light AI and 409 quote_not_current for an older revision. A different selection creates the approved quote with its own id (replaces_quote_id). Emits quote.approved. Test keys run the same contract on the quotes test mode created (publish one with POST /v1/test_helpers/leads/{lead_id}/publish_quote); events reach test endpoints only.

Scopequotes: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#

quote_idstringrequired#
The quote id (qt_…). Id prefix qt_.

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#

approved_attimestamp | null#
RFC 3339 date-time with offset.
selected_item_idsstring[] | null#
Defaults to the published selection. 1–300 items.
external_refobject | null#
The approval's id in your system (e.g. the Jobber quote id).
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.
signed_bystring | null#
At most 120 characters.
notestring | null#
At most 1000 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 approved quote.

idstringrequired#
Id prefix qt_.
objectstringrequired#
Always quote.
livemodebooleanrequired#
false for objects created with a test key.
statusstringrequired#
One of: sent, approved, declined, expired, superseded, disabled.
is_currentbooleanrequired#
superseded_bystring | nullrequired#
Id prefix qt_.
replaces_quote_idstring | nullrequired#
Id prefix qt_.
external_refsobject[]required#
The ids integrations wrote back on this quote (PUT /v1/quotes/{id}/external_refs/{system}).
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#
truncatedboolean#
Webhook payloads only: present (true) when the quote was too large for one delivery (256 KB). line_items is then empty; read the full quote with GET /v1/quotes/{id}. Never present on API responses. Always true.
projectobjectrequired#
project.idstringrequired#
Id prefix prj_.
project.pipeline_stagestring | nullrequired#
One of: request, quote_drafted, quote_sent, approved, completed, closed_lost.
project.external_refsobject[]required#
project.external_refs[].systemstringrequired#
project.external_refs[].namespacestring | nullrequired#
project.external_refs[].idstringrequired#
project.external_refs[].ownerstringrequired#
self: written by your integration (editable); others are read-only. One of: self, organization, partner.
project.external_refs[].read_onlybooleanrequired#
leadobject | nullrequired#
lead.idstringrequired#
Id prefix lead_.
lead.sourcestring | nullrequired#
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.
lead.external_refsobject[]required#
lead.external_refs[].systemstringrequired#
lead.external_refs[].namespacestring | nullrequired#
lead.external_refs[].idstringrequired#
lead.external_refs[].ownerstringrequired#
self: written by your integration (editable); others are read-only. One of: self, organization, partner.
lead.external_refs[].read_onlybooleanrequired#
clientobject | nullrequired#
Contact details need clients:read; otherwise only the id.
client.idstringrequired#
Id prefix cli_.
client.first_namestring | null#
client.last_namestring | null#
client.emailstring | null#
client.phonestring | null#
E.164, or null when the stored number could not be normalized (see phone_raw).
client.phone_rawstring | null#
The number as stored in Light AI.
client.external_refsobject[]#
client.external_refs[].systemstringrequired#
client.external_refs[].namespacestring | nullrequired#
client.external_refs[].idstringrequired#
client.external_refs[].ownerstringrequired#
self: written by your integration (editable); others are read-only. One of: self, organization, partner.
client.external_refs[].read_onlybooleanrequired#
propertyobject | null#
property.idstringrequired#
Id prefix prop_.
property.addressobject | null#
property.address.line1string | nullrequired#
property.address.line2string | nullrequired#
property.address.citystring | nullrequired#
property.address.regionstring | nullrequired#
property.address.postal_codestring | nullrequired#
property.address.countrystring | nullrequired#
property.address.formattedstringrequired#
Always present. Components may be null for addresses entered as one line.
property.address.latnumber | nullrequired#
property.address.lngnumber | nullrequired#
assigned_repobject | null#
assigned_rep.idstringrequired#
assigned_rep.namestring | nullrequired#
assigned_rep.emailstring | nullrequired#
titlestring | nullrequired#
messagestring | nullrequired#
termsstring | nullrequired#
disclaimerstring | null#
revisionobject#
revision.idstringrequired#
Id prefix rev_.
revision.numberintegerrequired#
revision.published_attimestamprequired#
RFC 3339 timestamp (UTC, milliseconds).
revision.evidence_hashstringrequired#
approval_modestring | nullrequired#
One of: light-ai, external, view-only.
allow_selectionsbooleanrequired#
currencystringrequired#
Always USD.
line_itemsobject[]required#
line_items[].idstringrequired#
line_items[].item_keystringrequired#
The line item id in the published document.
line_items[].codestring | nullrequired#
Stable catalog code (template line item); null for custom lines.
line_items[].namestring | nullrequired#
line_items[].descriptionstring | nullrequired#
line_items[].measurementstring | nullrequired#
line_items[].quantitynumber | nullrequired#
The measured quantity (decimal for feet and square feet, whole for counted items); null for an unmeasured line.
line_items[].unitstring | nullrequired#
One of: ft, sqft, each, set.
line_items[].unit_priceMoney | nullrequired#
Set when quantity × unit_price equals amount to the cent; otherwise null (e.g. mini lights priced by the set) and amount is the line total.
line_items[].unit_price_centsinteger | nullrequired#
line_items[].amountMoneyrequired#
line_items[].total_centsintegerrequired#
line_items[].bulbstring | nullrequired#
One of: c9, mini.
line_items[].colorsstring[]required#
line_items[].patternstring | nullrequired#
line_items[].requiredbooleanrequired#
line_items[].selectedbooleanrequired#
line_items[].taxablenullrequired#
discountobject | null#
discount.typestringrequired#
One of: percentage, fixed.
discount.valuenumberrequired#
discount.labelstring#
The discount line's name on the customer quote, e.g. "Black Friday offer" (a website request's offer); absent for a plain discount.
totalsobjectrequired#
totals.subtotalMoneyrequired#
totals.discountMoneyrequired#
totals.taxnullrequired#
totals.totalMoneyrequired#
totals.depositobjectrequired#
totals.deposit.percentagenumberrequired#
totals.deposit.amountMoneyrequired#
selected_item_idsstring[]#
assetsobjectrequired#
assets.mockupsobject[]required#
assets.mockups[].idstringrequired#
assets.mockups[].titlestring | nullrequired#
The image's title as published on the quote. For a mockup: the image's own title, else its mockup card's name (cards are named from their light colors, for example "Warm White" or "Candy Cane", unless the contractor renamed them).
assets.mockups[].urlstring | nullrequired#
null (ids only) unless the credential or endpoint holds quotes:asset_links: then a durable link that works while the customer link is active (at most 30 days); null for an image no longer stored.
assets.schematicsobject[]required#
assets.schematics[].idstringrequired#
assets.schematics[].titlestring | nullrequired#
The image's title as published on the quote. For a mockup: the image's own title, else its mockup card's name (cards are named from their light colors, for example "Warm White" or "Candy Cane", unless the contractor renamed them).
assets.schematics[].urlstring | nullrequired#
null (ids only) unless the credential or endpoint holds quotes:asset_links: then a durable link that works while the customer link is active (at most 30 days); null for an image no longer stored.
engagementobjectrequired#
engagement.availablebooleanrequired#
engagement.first_viewed_attimestamp | null#
RFC 3339 timestamp (UTC, milliseconds).
engagement.last_viewed_attimestamp | null#
RFC 3339 timestamp (UTC, milliseconds).
engagement.view_countinteger#
engagement.active_secondsnumber#
approvalobject | nullrequired#
approval.sourcestringrequired#
One of: light_ai, legacy_portal, external.
approval.signed_bystring | null#
approval.accepted_attimestamp | null#
RFC 3339 timestamp (UTC, milliseconds).
approval.selected_item_idsstring[] | null#
approval.evidence_hashstring | null#
approval.external_refobject | null#
approval.external_ref.systemstringrequired#
approval.external_ref.namespacestring | null#
approval.external_ref.idstringrequired#
declineobject | null#
decline.sourcestringrequired#
Always external.
decline.reasonstring | nullrequired#
decline.declined_attimestamp | nullrequired#
RFC 3339 timestamp (UTC, milliseconds).
decline.external_refobject | nullrequired#
decline.external_ref.systemstringrequired#
decline.external_ref.namespacestring | null#
decline.external_ref.idstringrequired#
sent_attimestamp | nullrequired#
RFC 3339 timestamp (UTC, milliseconds).
approved_attimestamp | nullrequired#
RFC 3339 timestamp (UTC, milliseconds).
declined_attimestamp | nullrequired#
RFC 3339 timestamp (UTC, milliseconds).
expires_attimestamp | nullrequired#
RFC 3339 timestamp (UTC, milliseconds).
created_attimestamprequired#
RFC 3339 timestamp (UTC, milliseconds).
updated_attimestamprequired#
RFC 3339 timestamp (UTC, milliseconds).
fixtureboolean#
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