Concepts
External references
Write your CRM ids back onto Light AI objects, look objects up by them, and keep two-way syncs loop-free.
An external reference links a Light AI object to its id in your system. Leads, clients, projects and quotes carry them in external_refs.
curl -X PUT "https://api.light-ai.app/v1/projects/prj_…/external_refs/hubspot" \
-H "Authorization: Bearer $LIGHT_AI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"id": "deal-9981"
}'const response = await fetch('https://api.light-ai.app/v1/projects/prj_…/external_refs/hubspot', {
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.LIGHT_AI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"id": "deal-9981"
}),
});
const data = await response.json();import os, requests
response = requests.put(
"https://api.light-ai.app/v1/projects/prj_…/external_refs/hubspot",
headers={
"Authorization": f"Bearer {os.environ['LIGHT_AI_API_KEY']}",
},
json={
"id": "deal-9981"
},
)
data = response.json()systemis a lowercase name (jobber,hubspot,highlevel);namespaceis optional (an account or location id).- References are namespaced per integration: you can never overwrite another integration's ids.
- Look up by reference on any list:
GET /v1/leads?external_ref=jobber:Z2lk123. - On
POST /v1/leads, anexternal_refyou already sent replays the original lead;POST /v1/clientsmatches your reference first, then e-mail, then phone.
Key records on the project#
Every published revision of a quote is a new quote id. Key your deal or job on project.id (or its external reference) and update it as quotes are published, approved, declined or expire.
Outcomes from your system#
When the homeowner approves or declines in your CRM or field app, record it on the current quote. Light AI moves the project and emits the same events.
curl -X POST "https://api.light-ai.app/v1/quotes/qt_…/mark_approved" \
-H "Authorization: Bearer $LIGHT_AI_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"approved_at": "2026-10-12T16:00:00Z",
"signed_by": "Dana Reyes",
"external_ref": {
"system": "jobber",
"id": "Z2lkOi8vSm9iYmVyL1F1b3RlLzEyMw"
},
"source_system": "jobber"
}'const response = await fetch('https://api.light-ai.app/v1/quotes/qt_…/mark_approved', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LIGHT_AI_API_KEY}`,
'Idempotency-Key': crypto.randomUUID(),
'Content-Type': 'application/json',
},
body: JSON.stringify({
"approved_at": "2026-10-12T16:00:00Z",
"signed_by": "Dana Reyes",
"external_ref": {
"system": "jobber",
"id": "Z2lkOi8vSm9iYmVyL1F1b3RlLzEyMw"
},
"source_system": "jobber"
}),
});
const data = await response.json();import os, uuid, requests
response = requests.post(
"https://api.light-ai.app/v1/quotes/qt_…/mark_approved",
headers={
"Authorization": f"Bearer {os.environ['LIGHT_AI_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"approved_at": "2026-10-12T16:00:00Z",
"signed_by": "Dana Reyes",
"external_ref": {
"system": "jobber",
"id": "Z2lkOi8vSm9iYmVyL1F1b3RlLzEyMw"
},
"source_system": "jobber"
},
)
data = response.json()409 quote_already_decided means the homeowner already decided in Light AI; 409 quote_not_current means a newer revision exists.
Avoiding loops#
Send source_system (your system name) on writes. Events caused by them carry origin.source_system; skip those in your listener, or set exclude_own_origin: true on your endpoint.