LightSkye Platform

Webhook API Reference

Send leads and appointments from any external system into LightSkye using HTTP webhooks authenticated by an API key.

Authentication

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
leadsCan only send to /api/v1/leads
appointmentsCan only send to /api/v1/appointments
bothCan send to both endpoints

Rate Limiting

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 }
POST

/api/v1/leads

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.

Contact fields → HubSpot contact

{
  "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: utilityProviderutility, billAmountelectric_bill_average, isHomeownerdo_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.

Qualification signals → routing & scoring

{
  "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
}

Metadata (optional)

{
  "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"]
}

Success response (201) - single

{
  "id":         "uuid",
  "status":     "pending",
  "created_at": "2024-01-15T10:30:00Z"
}

Cycle-refresh response (200) - single

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
}
BATCH

Sending many leads at once

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.

Request body - array

[
  { "first_name": "Jane", "email": "jane@email.com" },
  { "first_name": "John", "phone": "+15557654321" },
  { "email": "not-an-email" }        // will fail validation
]

Response - per-item results

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 }
}
POST

/api/v1/appointments

Submit an appointment. LightSkye auto-assigns it to the next available sales rep based on rank and calendar availability.

Request body

{
  // 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
}

Success response (201)

{
  "id":         "uuid",
  "status":     "pending",    // becomes "assigned" after auto-assignment
  "created_at": "2024-01-15T10:30:00Z"
}

Error Codes

StatusMeaning
201Created - lead(s) accepted (all items in a batch succeeded)
207Multi-Status - batch partially succeeded; inspect each results[] item
400Invalid JSON body
401Missing or invalid API key / key is inactive
403Key does not have permission for this endpoint
422Validation failed (or empty / oversized batch) - check details
429Rate limit exceeded - batch exceeds remaining capacity; wait and retry
500Internal server error

Example

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"
  }'