LightSkye Platform
Send leads and appointments from any external system into LightSkye using HTTP webhooks authenticated by an API key.
Every request must include your API key in the x-api-key header. Keys are issued from the Admin panel and scoped to specific endpoints.
POST /api/v1/leads x-api-key: lsk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Each API key has a per-minute rate limit configured at creation time. When exceeded, the server returns 429 Too Many Requests. Check the response headers for limit info.
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
{ "error": "Rate limit exceeded", "retry_after_seconds": 60 }Submit a lead. The endpoint accepts either a single lead object or a JSON array of up to 100 leads in one request. Each lead is deduplicated and routed independently. At least one of email, phone, or first_name is required per lead.
Every field is optional, but at least one of email, phone, or first_name must be present. Unknown fields are ignored. Fields fall into three groups: contact (synced to the HubSpot contact when a routing rule syncs to HubSpot), qualification signals (drive routing & scoring rules), and metadata.
{
"first_name": "Jane", // string
"last_name": "Smith", // string
"email": "jane@email.com", // string
"phone": "+15551234567", // string
"mobile_phone": "+15557654321", // string → HubSpot mobilephone
// Address
"contact_address": "123 Main St", // max 300
"contact_city": "Austin", // max 100
"contact_state": "TX", // max 50
"contact_zip": "78701", // max 20
"country": "USA", // max 100
// Profile / solar enrichment (→ HubSpot contact custom properties)
"company": "Acme Co", // → company
"job_title": "Owner", // → jobtitle
"website": "acme.com", // → website
"electric_company": "SCE", // → electric_company
// Referral attribution (plain text, passed through as-is)
"referral_code": "LS-4821", // → referral_code
"sales_rep": "Jane Doe", // → sales_rep (who referred, NOT owner)
// Free-text intake body: the customer's message plus any answers with no
// signal of their own. Rendered as a note on the HubSpot contact.
"notes": "Wants a call after 5pm.\n\nSurvey answers\n• Roof type: Metal"
}Two more HubSpot contact properties are derived from the qualification signals below: utilityProvider → utility, billAmount → electric_bill_average, isHomeowner → do_you_own_your_home. The contact's source and lead stage are set by the matched Sync to HubSpot routing rule, not by the webhook.
{
"isHomeowner": true, // boolean
"intentConfirmed": true, // boolean
"utilityProvider": "SCE", // string
"billAmount": 250, // number
"billOver100": true, // boolean | null
"creditOver650": true, // boolean | null
"creditRange": "Good", // string | null
"roofCondition": "Good", // "Good" | "Workable" | "Poor" | "Unknown" | null
"interestLevel": "High", // "High" | "Medium" | "Low" | null
"priorSolarConsideration": false, // boolean | null
"openToTreeTrimming": true, // boolean | null
"preferredCallbackWindow": "Mornings", // string | null
// Classification / scoring (usually computed upstream by the CRM)
"crm_type": "A", // "A" | "B"
"crm_score": 82, // 0-100
"classification": "Type A - ...", // string (sets crm_type if crm_type omitted)
"score": 82, // 0-100 (alias accepted)
"scoreBreakdown": { "intent": 30 }// object of numbers
}{
"idempotency_key": "unique-id", // re-submitting the same key refreshes
// the existing lead (cycle) instead of
// inserting a duplicate
"campaignId": "camp-123", "campaignName": "Spring Solar",
"agentId": "agent-9", "agentName": "Dana R.",
"qcNotes": { "agentNotes": "…", "clientNotes": "…" },
"starRatings": { "callQuality": 5, "compliance": 5, "tonality": 4 },
"callRecordingLinks": ["https://…/rec1.mp3"]
}{
"id": "uuid",
"status": "pending",
"created_at": "2024-01-15T10:30:00Z"
}Returned when an idempotency_key matches an existing lead from the same source. The lead is refreshed and re-routed.
{
"id": "uuid",
"status": "pending",
"created_at": "2024-01-15T10:30:00Z",
"cycle_refresh": true
}Send a JSON array (max 100). Each lead is validated and ingested independently - one invalid lead does not reject the others. Every lead consumes one unit of the API key's per-minute rate-limit budget, so a 40-lead batch needs 40 units of remaining capacity or the whole batch is rejected with 429.
[
{ "first_name": "Jane", "email": "jane@email.com" },
{ "first_name": "John", "phone": "+15557654321" },
{ "email": "not-an-email" } // will fail validation
]Status is 201 when every lead succeeds, 207 when some succeed and some fail, and 422 when every lead fails. The results array preserves request order via index.
HTTP/1.1 207 Multi-Status
{
"results": [
{ "index": 0, "id": "uuid", "status": "pending",
"created_at": "2024-01-15T10:30:00Z" },
{ "index": 1, "id": "uuid", "status": "pending",
"created_at": "2024-01-15T10:30:00Z" },
{ "index": 2, "error": "Validation failed",
"details": [{ "path": ["email"], "message": "Invalid email address" }] }
],
"summary": { "received": 3, "accepted": 2, "failed": 1 }
}Submit an appointment. LightSkye auto-assigns it to the next available sales rep based on rank and calendar availability.
{
// Contact info
"contact_first_name": "Jane", // optional
"contact_last_name": "Smith", // optional
"contact_email": "jane@email.com", // optional
"contact_phone": "+15551234567", // optional
"contact_address": "123 Main St", // optional
"contact_city": "Austin", // optional
"contact_state": "TX", // optional - 2-letter code (e.g. TX, CA)
"contact_zip": "78701", // optional - max 10 characters
// Appointment time (required)
"requested_date": "2024-02-01", // YYYY-MM-DD
"requested_time_start": "09:00", // HH:MM (24h)
"requested_time_end": "10:00", // HH:MM (24h)
"requested_timezone": "America/Chicago",
// Optional
"notes": "Please call ahead.",
"lead_id": "uuid", // link to an existing lead
"idempotency_key": "unique-id" // prevents duplicate inserts
}{
"id": "uuid",
"status": "pending", // becomes "assigned" after auto-assignment
"created_at": "2024-01-15T10:30:00Z"
}| Status | Meaning |
|---|---|
201 | Created - lead(s) accepted (all items in a batch succeeded) |
207 | Multi-Status - batch partially succeeded; inspect each results[] item |
400 | Invalid JSON body |
401 | Missing or invalid API key / key is inactive |
403 | Key does not have permission for this endpoint |
422 | Validation failed (or empty / oversized batch) - check details |
429 | Rate limit exceeded - batch exceeds remaining capacity; wait and retry |
500 | Internal server error |
cURL
curl -X POST https://console.lightskye.com/api/v1/appointments \
-H "Content-Type: application/json" \
-H "x-api-key: lsk_live_xxxx" \
-d '{
"contact_first_name": "Jane",
"contact_last_name": "Smith",
"contact_email": "jane@example.com",
"contact_phone": "+15551234567",
"requested_date": "2024-02-01",
"requested_time_start": "09:00",
"requested_time_end": "10:00",
"requested_timezone": "America/Chicago",
"idempotency_key": "ext-appt-9912"
}'