{"openapi":"3.1.0","info":{"title":"Eternalgy Lead API","version":"1.1.0","description":"Create leads with source attribution and optional agent assignment; discover source accounts and agents; retrieve current source-account leads. Introducer is the business term for referrer. Use LEADS_API_KEY as an opaque bearer secret, not a JWT. POST is not idempotent and must not be replayed automatically after an uncertain outcome. No update/delete, reassignment, full history, external ad-platform sync, or WhatsApp notification operation is provided here. Contract version describes this deployment's documentation, not a /v1 path."},"servers":[{"url":"https://referral.atap.solar","description":"Production; use a matching deployment for local testing."}],"externalDocs":{"url":"https://referral.atap.solar/api-docs/agent-guide","description":"Complete Markdown integration guide and runnable read-only Node.js example."},"security":[{"LeadApiKey":[]}],"tags":[{"name":"Leads"},{"name":"Sources"},{"name":"Agents"}],"paths":{"/api/leads/sources":{"get":{"operationId":"listLeadSources","tags":["Sources"],"summary":"List source Introducers","description":"Returns configured defaults and stored source accounts. ads is a subset of sources, not another page. Listing does not create accounts or connect to Meta/Google. New named sources are created on the first successful lead POST. Aliases: /api/leads/ads and /api/sources.","responses":{"200":{"description":"Available source accounts (may have no leads yet).","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"},"description":"API responses are not cacheable."}},"content":{"application/json":{"schema":{"type":"object","properties":{"sources":{"type":"array","items":{"$ref":"#/components/schemas/Source"}},"ads":{"type":"array","items":{"$ref":"#/components/schemas/Source"}}},"required":["sources","ads"],"additionalProperties":false},"example":{"sources":[{"id":"src_6c7df534e1f9572f1d3da1e3","name":"Meta Ads Leon","type":"ads"},{"id":"src_74b907b7af778fd4154dcff7","name":"Google Ads Gan","type":"ads"},{"id":"src_56c6cd5bfa64e52c9a416340","name":"Facebook Ads","type":"ads"},{"id":"src_62fa52b9d94790dcbd2d6d92","name":"Google Ads","type":"ads"}],"ads":[{"id":"src_6c7df534e1f9572f1d3da1e3","name":"Meta Ads Leon","type":"ads"},{"id":"src_74b907b7af778fd4154dcff7","name":"Google Ads Gan","type":"ads"},{"id":"src_56c6cd5bfa64e52c9a416340","name":"Facebook Ads","type":"ads"},{"id":"src_62fa52b9d94790dcbd2d6d92","name":"Google Ads","type":"ads"}]}}}},"401":{"description":"Missing or incorrect Authorization: Bearer <LEADS_API_KEY>.","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"},"description":"API responses are not cacheable."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Missing or incorrect Authorization: Bearer <LEADS_API_KEY>.","code":"UNAUTHORIZED"}}}},"500":{"description":"Unable to complete the request. Internal details are omitted.","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"},"description":"API responses are not cacheable."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Unable to complete the request. Internal details are omitted.","code":"INTERNAL_ERROR"}}}},"503":{"description":"Server API key is not configured; contact the administrator.","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"},"description":"API responses are not cacheable."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Server API key is not configured; contact the administrator.","code":"API_NOT_CONFIGURED"}}}}}}},"/api/leads/agents":{"get":{"operationId":"listLeadAgents","tags":["Agents"],"summary":"List assignable sales users","description":"Use a returned id as assignedAgentId. id and bubbleId are the same; numericId is also accepted on creation. Normal databases filter access levels containing sales; legacy schemas without access_level cannot apply that filter. An empty result means no eligible users were found. Alias: /api/agents.","responses":{"200":{"description":"Assignable agents.","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"},"description":"API responses are not cacheable."}},"content":{"application/json":{"schema":{"type":"object","properties":{"agents":{"type":"array","items":{"$ref":"#/components/schemas/Agent"}}},"required":["agents"],"additionalProperties":false},"example":{"agents":[{"id":"USER_BUBBLE_ID","name":"Ali Sales","bubbleId":"USER_BUBBLE_ID","numericId":42,"accessLevels":["Sales"]}]}}}},"401":{"description":"Missing or incorrect Authorization: Bearer <LEADS_API_KEY>.","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"},"description":"API responses are not cacheable."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Missing or incorrect Authorization: Bearer <LEADS_API_KEY>.","code":"UNAUTHORIZED"}}}},"500":{"description":"Unable to complete the request. Internal details are omitted.","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"},"description":"API responses are not cacheable."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Unable to complete the request. Internal details are omitted.","code":"INTERNAL_ERROR"}}}},"503":{"description":"Server API key is not configured; contact the administrator.","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"},"description":"API responses are not cacheable."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Server API key is not configured; contact the administrator.","code":"API_NOT_CONFIGURED"}}}}}}},"/api/leads":{"post":{"operationId":"createLead","tags":["Leads"],"summary":"Create a lead with Introducer and optional assignment","description":"Requires leadName, leadMobileNumber, referrer. Omit assignedAgentId for Pending/null assignment; a valid assignment returns Assigned and the user's Bubble ID. A bare referrer string resolves a known/existing source ID, otherwise creates/reuses a named source. Use {customerId} for an existing person or source; unknown account IDs in that object are rejected. Do not invent source IDs. Optional fields must be omitted rather than null. Unknown fields are rejected. Each successful POST creates a new customer lead and workflow, even for repeated phones; no idempotency-key support or deduplication. Source, lead, workflow, audit save atomically. Sends no WhatsApp notification.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLeadInput"},"examples":{"unassigned":{"summary":"Minimum request; Pending","value":{"leadName":"Tan Ah Kow","leadMobileNumber":"0123456789","referrer":"Meta Ads Leon"}},"assigned":{"summary":"Replace USER_BUBBLE_ID with a returned agent id","value":{"leadName":"Tan Ah Kow","leadMobileNumber":"0123456789","referrer":"Meta Ads Leon","assignedAgentId":"USER_BUBBLE_ID","leadState":"Selangor","leadCity":"Kajang","leadAddress":"1 Jalan Example","remark":"Interested in rooftop solar"}}}}}},"responses":{"201":{"description":"Lead created. Persist both returned IDs in the caller's submission ledger.","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"},"description":"API responses are not cacheable."}},"content":{"application/json":{"schema":{"type":"object","properties":{"lead":{"$ref":"#/components/schemas/CreatedLead"}},"required":["lead"],"additionalProperties":false},"example":{"lead":{"referralId":123,"leadCustomerId":"cust_example","leadName":"Tan Ah Kow","leadMobileNumber":"60123456789","referrer":{"customerId":"src_6c7df534e1f9572f1d3da1e3","name":"Meta Ads Leon","type":"source"},"assignedAgentId":"USER_BUBBLE_ID","status":"Assigned"}}}}},"400":{"description":"INVALID_JSON or INVALID_INPUT. Payload validation may include issues with field paths.","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"},"description":"API responses are not cacheable."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid lead payload.","code":"INVALID_INPUT","issues":[{"path":"leadName","message":"Lead name is required."}]}}}},"401":{"description":"Missing or incorrect Authorization: Bearer <LEADS_API_KEY>.","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"},"description":"API responses are not cacheable."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Missing or incorrect Authorization: Bearer <LEADS_API_KEY>.","code":"UNAUTHORIZED"}}}},"415":{"description":"Send Content-Type: application/json.","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"},"description":"API responses are not cacheable."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Send Content-Type: application/json.","code":"INVALID_CONTENT_TYPE"}}}},"422":{"description":"INVALID_AGENT or INVALID_REFERRER: correct the agent or existing account ID.","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"},"description":"API responses are not cacheable."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"INVALID_AGENT or INVALID_REFERRER: correct the agent or existing account ID.","code":"INVALID_AGENT"}}}},"500":{"description":"Unable to complete the request. Internal details are omitted.","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"},"description":"API responses are not cacheable."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Unable to complete the request. Internal details are omitted.","code":"INTERNAL_ERROR"}}}},"503":{"description":"API_NOT_CONFIGURED or ASSIGNMENT_NOT_CONFIGURED: contact the administrator.","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"},"description":"API responses are not cacheable."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"API_NOT_CONFIGURED or ASSIGNMENT_NOT_CONFIGURED: contact the administrator.","code":"ASSIGNMENT_NOT_CONFIGURED"}}}}}},"get":{"operationId":"listLeadsBySource","tags":["Leads"],"summary":"Retrieve current leads by source Introducer","description":"Only source-account referrals are included; person referrals are excluded even though POST accepts them. Omit referrer for all source accounts including non-ad sources. A referrer filter is not a permission boundary: the shared key can query all sources. Unknown sources return 200 and an empty list. Single exact source name (ignoring case/repeated spaces) or exact ID, not substring/wildcards/arrays/comma-separated names. Sort: descending referralId. Increase offset by limit until pagination.hasMore=false. New records/deletions can shift offsets; deduplicate by referralId and use a quiet period for stable exports. Current stored state only, not activity history or a change feed. Unknown and repeated query parameters are rejected.","parameters":[{"name":"referrer","in":"query","required":false,"description":"One source name or returned source customer ID; URL-encode. Omit rather than send blank. Business term: Introducer.","schema":{"type":"string","minLength":2,"maxLength":120},"examples":{"leon":{"value":"Meta Ads Leon"},"gan":{"value":"Google Ads Gan"},"byId":{"value":"src_6c7df534e1f9572f1d3da1e3"}}},{"name":"limit","in":"query","required":false,"description":"Decimal whole-number page size, once only.","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}},{"name":"offset","in":"query","required":false,"description":"Decimal whole number of matching records to skip, once only.","schema":{"type":"integer","minimum":0,"maximum":2147483647,"default":0}}],"responses":{"200":{"description":"Matching leads and total count, including total when a page is empty.","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"},"description":"API responses are not cacheable."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeadList"},"example":{"leads":[{"referralId":123,"leadCustomerId":"cust_example","leadName":"Tan Ah Kow","leadMobileNumber":"60123456789","referrer":{"customerId":"src_6c7df534e1f9572f1d3da1e3","name":"Meta Ads Leon","type":"source"},"assignedAgentId":"USER_BUBBLE_ID","status":"Assigned","leadState":"Selangor","leadCity":"Kajang","leadAddress":"1 Jalan Example","relationship":"Other","projectType":"OTHERS","remark":"Interested in rooftop solar","createdAt":"2026-10-16T02:00:00.000Z"}],"pagination":{"total":1,"limit":100,"offset":0,"hasMore":false}}}}},"400":{"description":"Invalid query; use referrer, limit (1-500), offset (0-2147483647), once each.","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"},"description":"API responses are not cacheable."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid query; use referrer, limit (1-500), offset (0-2147483647), once each.","code":"INVALID_INPUT"}}}},"401":{"description":"Missing or incorrect Authorization: Bearer <LEADS_API_KEY>.","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"},"description":"API responses are not cacheable."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Missing or incorrect Authorization: Bearer <LEADS_API_KEY>.","code":"UNAUTHORIZED"}}}},"500":{"description":"Unable to complete the request. Internal details are omitted.","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"},"description":"API responses are not cacheable."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Unable to complete the request. Internal details are omitted.","code":"INTERNAL_ERROR"}}}},"503":{"description":"Server API key is not configured; contact the administrator.","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"},"description":"API responses are not cacheable."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Server API key is not configured; contact the administrator.","code":"API_NOT_CONFIGURED"}}}}}}}},"components":{"securitySchemes":{"LeadApiKey":{"type":"http","scheme":"bearer","description":"Existing LEADS_API_KEY configured on the API server. Opaque shared secret, not OAuth/JWT. Send in Authorization header from your backend; never in URLs or browser bundles. Login cookies are not used."}},"schemas":{"Source":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["ads","other"]}},"required":["id","name","type"],"additionalProperties":false},"Agent":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"bubbleId":{"type":"string"},"numericId":{"type":"integer"},"accessLevels":{"type":"array","items":{"type":"string"}}},"required":["id","name","bubbleId","numericId","accessLevels"],"additionalProperties":false},"Referrer":{"type":"object","properties":{"customerId":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["source","person"]}},"required":["customerId","name","type"],"additionalProperties":false},"SourceReferrer":{"type":"object","properties":{"customerId":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","const":"source"}},"required":["customerId","name","type"],"additionalProperties":false},"CreateLeadInput":{"type":"object","properties":{"leadName":{"type":"string","minLength":2,"maxLength":120,"description":"Trimmed before checking length."},"leadMobileNumber":{"type":"string","minLength":6,"maxLength":40,"pattern":"^\\+?[\\d\\s().-]+$","description":"Trimmed input. Malaysian mobile only: strip formatting, normalize local 0/1 prefixes to 60, then require ^60\\d{9,11}$. Example 0123456789 -> 60123456789."},"referrer":{"oneOf":[{"type":"string","minLength":2,"maxLength":120,"description":"Source name or actual returned source ID. Trimmed; repeated spaces normalized. Unknown strings are treated as new source names."},{"type":"object","properties":{"customerId":{"type":"string","minLength":1,"maxLength":120,"description":"Existing person/source customer account ID; trimmed."}},"required":["customerId"],"additionalProperties":false}],"description":"Introducer. Existing persons must use the object form."},"assignedAgentId":{"oneOf":[{"type":"string","minLength":1,"maxLength":80,"pattern":"^[A-Za-z0-9_-]+$","description":"Trimmed returned agent id/Bubble ID (preferred) or numeric user ID as string."},{"type":"integer","minimum":1,"maximum":9007199254740991}],"description":"Optional; omit rather than null/empty for unassigned. Must resolve to an eligible user with a Bubble ID."},"leadState":{"type":"string","maxLength":120,"default":"","description":"Trimmed."},"leadCity":{"type":"string","maxLength":120,"default":"","description":"Trimmed."},"leadAddress":{"type":"string","maxLength":500,"default":"","description":"Trimmed."},"relationship":{"type":"string","enum":["Family","Friend","Colleague","Neighbor","Business Partner","Existing Customer","Community Contact","Other"],"default":"Other"},"projectType":{"type":"string","enum":["RESIDENTIAL (2%)","SHOP-LOT (2%)","FACTORY (1%)","OTHERS"],"default":"OTHERS"},"remark":{"type":"string","maxLength":500,"default":"","description":"Trimmed."}},"required":["leadName","leadMobileNumber","referrer"],"additionalProperties":false},"CreatedLead":{"type":"object","properties":{"referralId":{"type":"integer","description":"Workflow record ID; use for deduplication when reading pages."},"leadCustomerId":{"type":"string"},"leadName":{"type":"string"},"leadMobileNumber":{"type":"string","pattern":"^60\\d{9,11}$","description":"Canonical Malaysian mobile number."},"referrer":{"$ref":"#/components/schemas/Referrer"},"assignedAgentId":{"type":["string","null"],"description":"Assigned user's Bubble ID, or null if unassigned."},"status":{"type":"string","enum":["Pending","Assigned"]}},"required":["referralId","leadCustomerId","leadName","leadMobileNumber","referrer","assignedAgentId","status"],"additionalProperties":false},"ListedLead":{"type":"object","properties":{"referralId":{"type":"integer","description":"Workflow record ID; use for deduplication when reading pages."},"leadCustomerId":{"type":["string","null"]},"leadName":{"type":"string"},"leadMobileNumber":{"type":["string","null"]},"referrer":{"$ref":"#/components/schemas/SourceReferrer"},"assignedAgentId":{"type":["string","null"],"description":"Assigned user's Bubble ID, or null if unassigned."},"status":{"type":"string","description":"Open set of stored statuses. UI values include Pending, Assigned, Contacted, Qualified, Successful, Rejected; legacy values may differ."},"leadState":{"type":"string"},"leadCity":{"type":"string"},"leadAddress":{"type":"string"},"relationship":{"type":"string"},"projectType":{"type":"string"},"remark":{"type":"string"},"createdAt":{"type":["string","null"],"description":"Stored creation timestamp with timezone; null when unavailable. Example: 2026-10-16T02:00:00.000Z."}},"required":["referralId","leadCustomerId","leadName","leadMobileNumber","referrer","assignedAgentId","status","leadState","leadCity","leadAddress","relationship","projectType","remark","createdAt"],"additionalProperties":false},"LeadList":{"type":"object","properties":{"leads":{"type":"array","items":{"$ref":"#/components/schemas/ListedLead"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"required":["leads","pagination"],"additionalProperties":false},"Pagination":{"type":"object","properties":{"total":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":500},"offset":{"type":"integer","minimum":0,"maximum":2147483647},"hasMore":{"type":"boolean"}},"required":["total","limit","offset","hasMore"],"additionalProperties":false},"Error":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string","enum":["INVALID_JSON","INVALID_INPUT","UNAUTHORIZED","INVALID_CONTENT_TYPE","INVALID_AGENT","INVALID_REFERRER","API_NOT_CONFIGURED","ASSIGNMENT_NOT_CONFIGURED","INTERNAL_ERROR"]},"issues":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error","code"],"additionalProperties":false}}}}