Light AIDocs Beta Open Light AI

Webhooks & events

Event catalog

All 27 webhook event types, what triggers them and which scope they need.

Every event uses the envelope. An endpoint receives only the types it lists (or ["*"]) and that its credential's scopes can read. Events are also listed by GET /v1/events for 30 days.

Leads#

lead.created#

A new lead (any channel), once per lead. needs_address true when it waits for an address. Spam and blocked website repeats are not leads; one that is later reopened as a live lead is announced then (within 30 days of the request).

Needs leads:read · data.object: Lead

lead.touched#

A repeat request folded into an existing lead (API, inbound, or a website request blocked as a repeat of an earlier one): data.touch is the new request.

Needs leads:read · data.object: Lead

lead.updated#

Status, hold or address change (archived, spam, reopened, held → converted, address added → converted).

Needs leads:read · data.object: Lead

lead.mockup_ready#

A website request form lead's instant preview step finished (sent once, after lead.created, never delaying it): data.object.mockup_status is ready, failed or skipped. It fires once the request's one preview is settled (or 30 minutes after the request), so data.object.assets.mockups and preview_looks list it (older leads may list up to 3). Image URLs are not in the payload: GET /v1/leads/{id} returns data.object.assets with 60-minute signed links. Origin type is always embed.

Needs leads:read · data.object: Lead

Clients#

client.created#

A new client.

Needs clients:read · data.object: Client

client.updated#

Name, email or phone changed, or a property was added, edited or removed or the primary property changed (previous_attributes.updated_fields: first_name, last_name, email, phone, properties, primary_property), or another client was merged into this one (updated_fields merged: its client.deleted names this client in merged_into). Coalesced per client, per origin and per minute, trailing edge: a webhook endpoint gets one delivery per window, sent at its end and showing the client as it is then. A recorded event is never changed: a later edit in the same minute is a new client.updated (updated_fields lists every field the window changed so far), so the events feed can list more than one per minute, and an earlier delivery not yet sent is cancelled (superseded) in favor of the new one.

Needs clients:read · data.object: Client

client.deleted#

A client was deleted: id and external refs only. merged_into is set when the contractor merged it into an existing client (re-point your records to that client).

Needs clients:read · data.object: ClientTombstone

Projects#

project.created#

A new project (from a lead with an address, or created in the app). A website request blocked as a repeat still gets its own project in the app: its lead is that request (status duplicate), announced as lead.touched on the earlier lead.

Needs projects:read · data.object: Project

project.stage_changed#

The pipeline stage changed (a move, a quote or approval, a saved measurement in the app, a deleted draft quote). data.stage_change is the transition at event time ({from, to, sequence}); trigger on data.stage_change.to, not on data.object.pipeline_stage (the current stage). previous_attributes.pipeline_stage equals stage_change.from.

Needs projects:read · data.object: Project

Quotes#

quote.published#

A quote revision was published (a new quote id; key CRM records on project.id).

Needs quotes:read · data.object: Quote

quote.viewed#

The homeowner opened the quote (needs the engagement entitlement).

Needs quotes:read · data.object: Quote

The homeowner clicked the quote's external link.

Needs quotes:read · data.object: Quote

quote.change_requested#

The homeowner used "Request a change" on the quote link (the contractor turned on Let customers request changes for this quote): data.change_request is the request ({id, message, created_at}). One event per request; the quote stays sent and can still be approved, and the pipeline stage does not change. A homeowner has one open request per quote link at a time (the next is possible once the contractor marks it resolved, publishes an update or after 24 hours). Live mode only: test mode cannot simulate it. Origin type is portal.

Needs quotes:read · data.object: Quote

quote.approved#

Approved by the homeowner, in the app, or recorded with mark_approved. An approval with a different selection of optional items approves a new quote (replaces_quote_id names the published one), announced first by quote.superseded for the published quote (superseded_by this one), live and in test mode alike.

Needs quotes:read · data.object: Quote

quote.declined#

Declined (maps to closed-lost).

Needs quotes:read · data.object: Quote

quote.expired#

The link expired without a decision (maps to closed-lost).

Needs quotes:read · data.object: Quote

The quote's customer link stopped working. previous_attributes.reason says why: disabled (the contractor turned the link off) or rotated (the contractor issued a new link: the old one is dead, and data.object.links.customer_url carries the new one when you hold quotes:share_link; re-read the quote rather than marking its link dead). A disabled link can be turned back on: quote.link_enabled follows. Rotating a disabled link sends rotated, then quote.link_enabled (the new link works).

Needs quotes:read · data.object: Quote

A disabled customer link works again: the contractor turned the same link back on, or rotated it (a new link, announced first by quote.link_disabled rotated; re-read customer_url). data.object.status is current; previous_attributes.status is disabled. Resume follow-ups you stopped on quote.link_disabled.

Needs quotes:read · data.object: Quote

quote.superseded#

This quote was replaced (superseded_by names the replacement): the contractor published a newer revision, or the quote was approved with a changed selection of optional items, which approves a new quote (its quote.approved follows). Close this quote in your system and link the replacement.

Needs quotes:read · data.object: Quote

Mockups#

mockup.completed#

A mockup generation finished with at least one image (images[].status says which; failed images were refunded). Image URLs are not in the payload: GET /v1/mockup_generations/{id} for signed URLs. Needs mockups:read.

Needs mockups:read · data.object: MockupGeneration

mockup.failed#

A mockup generation finished without any image; every credit was refunded (failure_code says why). Needs mockups:read.

Needs mockups:read · data.object: MockupGeneration

Account and partners#

organization.entitlement_changed#

Plan, pause, Off-Season, kill-switch or quote-engagement change (delivered even when it ends your access). paused and off_season are what is in effect: a seasonal pause or Off-Season scheduled ahead is announced when it starts (within the hour), not when it is scheduled; while paused the organization is read-only and inbound leads are held. previous_attributes holds the earlier tier, paused, api_disabled, off_season and engagement. engagement turns off while a payment is past due (the tier stays): quote.viewed and quote.external_link_clicked stop until it turns on again. API access is included on Pro: Pro, Enterprise and Pro trials have the full API; Free and Plus have none (tier names the plan; live_api, test_mode and the rest are false). A drop below Pro suspends the organization's keys, sources and endpoints (never deleted) right after this event, and a partner's grant carries none of its data; an upgrade to Pro restores them.

No read scope needed · data.object: OrganizationEntitlements

grant.created#

A contractor connected your partner account. Sent once per grant status: with data.object.status pending when a grant awaits your confirmation (POST /v1/partner/grants/{id}/confirm), and again with active once it is confirmed (a grant that starts active gets only that one). Provision on data.object.id with status active; the two deliveries have different event ids. A contractor that re-consents (new scopes) gets a new grant: data.object.replaces_grant_id names the grant it supersedes, so re-map that contractor (or de-duplicate on organization_id) rather than provisioning it twice.

No read scope needed · data.object: GrantLifecycle

grant.revoked#

A contractor's grant ended (revoked or expired); delivered as a farewell.

No read scope needed · data.object: GrantLifecycle

api_key.expiring#

Sent 14 and 3 days before the key expires, to that key's own endpoints (a partner key: the partner's endpoints) that subscribe to it (list it in events, or use ["*"]). The owners also get an e-mail.

No read scope needed · data.object: ApiKeyExpiring

webhook_endpoint.disabled#

Another endpoint of the same owner was disabled by Light AI: a key's endpoint notifies that key's other endpoints, a partner's notifies the partner's endpoints, a dashboard endpoint the organization's other dashboard endpoints; only endpoints that subscribe to it (list it in events, or use ["*"]) receive it. The owners also get an e-mail.

No read scope needed · data.object: WebhookEndpointDisabledNotice

ping#

Test delivery (only to the endpoint it was sent to).

No read scope needed · data.object: PingObject

Change requests#

When the contractor turns on Let customers request changes for a quote (off by default), the homeowner can send a short Request a change message from the quote link. Each request emits quote.change_requested: data.object is the quote (key your records on project.id) and data.change_request is {id, message, created_at}, with the homeowner's message as they typed it (plain text, up to 500 characters; treat it as untrusted input). The quote stays open and can still be approved, and the project keeps its stage. If the contractor then publishes an updated quote you receive quote.superseded and quote.published as usual. Endpoints keep the event list they were saved with (["*"] is expanded when you save), so add quote.change_requested to an existing endpoint to receive it.