← Eternalgy ReferralDeveloper documentation

Lead intake API

Create leads and retrieve them by source.

Query available ad accounts and sales agents, then send incoming leads from Meta Ads, Google Ads, custom forms, or referrers. Assign each lead to a sales agent independently of who introduced it. Retrieve matching leads by referrer source name or ID.

GEThttps://referral.atap.solar/api/leads/sources(also: /api/leads/ads, /api/sources)
GEThttps://referral.atap.solar/api/leads/agents(also: /api/agents)
POSThttps://referral.atap.solar/api/leads
GEThttps://referral.atap.solar/api/leads?referrer=Meta%20Ads%20Leon

Quick start for AI agents

Base URL: https://referral.atap.solar. Use this page with the OpenAPI contract and raw integration guide from the same deployed server. If a documented operation returns 404 or 405, confirm the deployed version with the administrator.

  1. Obtain the existing LEADS_API_KEY from the administrator and configure it as a backend environment secret.
  2. Call GET /api/leads/sources and GET /api/leads/agents. Select real returned source and agent IDs; USER_BUBBLE_ID in examples is a placeholder.
  3. For a real incoming lead, call POST /api/leads with leadName, leadMobileNumber, and referrer. Add assignedAgentId to assign it, or omit the field to leave it pending. Save the returned IDs.
  4. Call GET /api/leads with the source name or ID as referrer. Page through results until pagination.hasMore is false.
Minimum creation payload · creates an unassigned lead
{
  "leadName": "Tan Ah Kow",
  "leadMobileNumber": "0123456789",
  "referrer": "Meta Ads Leon"
}

Terminology and supported scope

Introducer is the business term; the exact API field is referrer. referralId identifies the workflow, leadCustomerId the lead customer, and referrer.customerId the Introducer. Assignment identifies who receives the lead, independently of its source. Leads submitted here join the existing manager and assigned-agent dashboards.

These operations create and read records. Meta/Google form connections and assignment rules must be implemented by the calling integration. This API provides no update/delete, reassignment, single-lead lookup, full activity history, or WhatsApp notification operation. Status is the current value stored in this system.

Authentication

Obtain an API key from the site administrator and include it in every request. Login cookies are not required.

Every API request
Authorization: Bearer <value of LEADS_API_KEY>

For POST, also send Content-Type: application/json. The key is an opaque secret, not a JWT or OAuth token. Use the server's existing key value as one continuous string; do not generate a different key in the calling application.

Keep the key on your server or in your automation platform’s secret storage. Send requests from that server or platform.

The same key authenticates all four operations. It is a shared integration credential: filtering by referrer does not restrict the key to that source. Documentation is public; API data requires the key. Database credentials are unnecessary for callers.

List available ad sources

Query the available ad accounts and lead source referrers (e.g. Meta Ads Leon, Google Ads Gan, Facebook Ads, Google Ads) along with their customer IDs.

cURL · List available ad sources
curl --request GET 'https://referral.atap.solar/api/leads/sources' \
  --header "Authorization: Bearer $LEADS_API_KEY"

Response (`200 OK`)

200 OK · application/json
{
  "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"
    }
  ]
}

Pass either the source’s name or its id into the referrer field when creating a lead. The ads array is a subset of sources, not another page. Configured defaults can be listed before they have any leads.

List available sales agents

Call this endpoint before creating a lead to see which sales agents can be assigned. On the normal database schema, users must have an access level containing sales. Legacy schemas without that column cannot apply the access-level filter.

cURL · List available agents
curl --request GET 'https://referral.atap.solar/api/leads/agents' \
  --header "Authorization: Bearer $LEADS_API_KEY"

Response (`200 OK`)

200 OK · application/json
{
  "agents": [
    {
      "id": "USER_BUBBLE_ID",
      "name": "Ali Sales",
      "bubbleId": "USER_BUBBLE_ID",
      "numericId": 42,
      "accessLevels": [
        "Sales"
      ]
    }
  ]
}

Pass the returned id (or numericId) as the assignedAgentId when creating the lead. An empty list means leave the lead unassigned or contact the administrator.

Retrieve leads by referrer source

Use GET /api/leads with the same bearer API key. Set referrer to Meta Ads Leon, Google Ads Gan, another source name, or a source ID from the sources endpoint. Names match exactly, ignoring case and repeated spaces. Omit referrer for leads across all source accounts, including non-ad sources. Person-referral accounts are excluded.

cURL · Retrieve leads from Meta Ads Leon
curl --get 'https://referral.atap.solar/api/leads' \
  --header "Authorization: Bearer $LEADS_API_KEY" \
  --data-urlencode 'referrer=Meta Ads Leon' \
  --data-urlencode 'limit=100' \
  --data-urlencode 'offset=0'

referrer accepts one string of 2–120 trimmed characters, not an object, array, partial match, wildcard, or comma-separated list. Query multiple sources separately or omit the filter. limit defaults to 100 (range 1–500), and offset defaults to 0 (range 0–2147483647). Send decimal whole numbers and URL-encode names. Unknown or repeated parameters return 400 INVALID_INPUT. Unknown sources return an empty list.

200 OK · application/json
{
  "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
  }
}

Results are ordered by descending referral ID and include current status, assignment, contact, location, and remarks. Increase offset by limit until pagination.hasMore is false to retrieve every matching lead. New arrivals can shift pages during retrieval. Missing legacy details return empty strings or null. pagination.total counts all matching records, not this page. An offset beyond the end returns an empty page with the same total.

Status is an open string. Current UI values include Pending, Assigned, Contacted, Qualified, Successful, and Rejected; preserve unfamiliar legacy values.leadCustomerId, leadMobileNumber, assignedAgentId, and createdAt can be null. createdAt is a timestamp string when available.

Working Node.js example

Save as example.mjs, configure LEADS_API_KEY, and run node example.mjs on Node.js 22 or later. This example discovers options and reads all pages without creating leads. Use a quiet period for a stable export; deduplication by referral ID handles repeats when pages shift.

JavaScript · discovery and paginated retrieval
// Node.js 22+, server-side. Set LEADS_API_KEY in your environment.
const baseUrl = process.env.LEADS_API_BASE_URL || "https://referral.atap.solar";
const apiKey = process.env.LEADS_API_KEY;
if (!apiKey) throw new Error("Set LEADS_API_KEY before calling the API.");

async function getJson(path, params = {}) {
  const url = new URL(path, baseUrl);
  url.search = new URLSearchParams(params).toString();
  const response = await fetch(url, {
    headers: { Authorization: `Bearer ${apiKey}` },
    cache: "no-store",
    signal: AbortSignal.timeout(30000),
  });
  const body = await response.json();
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${body.code}: ${body.error}`);
  }
  return body;
}

const { sources } = await getJson("/api/leads/sources");
const { agents } = await getJson("/api/leads/agents");
// Select actual source/agent IDs from these responses; example IDs are placeholders.
console.log({ sources, agents });

const leadsById = new Map();
const limit = 100;
let offset = 0;
while (true) {
  const page = await getJson("/api/leads", {
    referrer: "Meta Ads Leon", // Or "Google Ads Gan", another source name, or source ID.
    limit: String(limit),
    offset: String(offset),
  });
  for (const lead of page.leads) leadsById.set(lead.referralId, lead);
  if (!page.pagination.hasMore) break;
  offset += page.pagination.limit;
}
const leads = [...leadsById.values()];
console.log({ count: leads.length });

Create lead request

Set your API key in the LEADS_API_KEY environment variable, then replace USER_BUBBLE_ID with the assigned sales user’s ID from the agents list above.

cURL · Meta Ads lead assigned to an agent
curl --request POST 'https://referral.atap.solar/api/leads' \
  --header "Authorization: Bearer $LEADS_API_KEY" \
  --header 'Content-Type: application/json' \
  --data '{
    "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"
  }'

Introducers & agents

Advertising or another source

Use a source name (such as Meta Ads Leon, Google Ads Gan, Facebook Ads, Google Ads) or a source ID (e.g. src_6c7df534e1f9572f1d3da1e3). Case and repeated spaces are canonicalized automatically.

To add another source, send its intended name, for example TikTok Ads Mei. The first successful lead submission creates/reuses the source account automatically. Listing or querying does not create one. Reuse returned IDs for existing accounts; an unknown string is interpreted as a new source name, so do not invent IDs.

Meta Ads Leon by name
"referrer": "Meta Ads Leon"
Meta Ads Leon by ID
"referrer": "src_6c7df534e1f9572f1d3da1e3"

An existing person

Supply the customer ID of an existing referral account. This request does not create a person’s referrer account.

Use the object form for a person. A bare person's name is treated as a source name. The source-lead retrieval endpoint excludes person referrals.

Existing referrer account
"referrer": { "customerId": "EXISTING_REFERRER_CUSTOMER_ID" }

Independent agent assignment

Use the sales user’s Bubble ID or numeric user ID in assignedAgentId. The response returns the Bubble ID. Omit the field to create an unassigned lead. This sets the assigned agent; it leaves the separate preferred agent empty.

Request fields

Send a JSON object. Unknown fields are rejected; optional text fields default to empty strings.

Use exact camelCase field names. Omit optional fields for defaults; null is invalid, and an empty assignedAgentId is invalid. Source names and text fields are trimmed. Phone input accepts digits, an optional leading +, spaces, parentheses, dots and hyphens (6–40 characters); normalization must produce a Malaysian number matching ^60\d{9,11}$.

Lead creation request fields, requirements, types, and descriptions
FieldRequiredTypeDescription
leadNameYesstringLead name, 2–120 characters.
leadMobileNumberYesstringMalaysian mobile number. Local and +60 formats are accepted and normalized to 60… .
referrerYesstring | objectAn ad source name (e.g. "Meta Ads Leon"), source ID ("src_..."), or { "customerId": "EXISTING_REFERRER_CUSTOMER_ID" }.
assignedAgentIdNostring | integerSales user’s Bubble ID (recommended) or positive numeric user ID. Omit to leave the lead unassigned.
leadStateNostringUp to 120 characters.
leadCityNostringUp to 120 characters.
leadAddressNostringUp to 500 characters.
relationshipNostringOne of the relationship options below. Defaults to "Other".
projectTypeNostringOne of the project types below. Defaults to "OTHERS".
remarkNostringUp to 500 characters.

Relationship options

Family, Friend, Colleague, Neighbor, Business Partner, Existing Customer, Community Contact, Other

Project types

  • RESIDENTIAL (2%)
  • SHOP-LOT (2%)
  • FACTORY (1%)
  • OTHERS

Success response

HTTP 201 Created returns the new referral and lead IDs, the introducer, and the assigned user.

201 Created · application/json
{
  "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"
  }
}

Unassigned leads return assignedAgentId: null and status: "Pending". Person introductions return referrer.type: "person".

Each successful request creates a new lead. Requests are not deduplicated, so do not automatically replay a request whose outcome is unknown. A successful response means the lead, assignment, and audit entry have been saved together. This endpoint does not send WhatsApp messages.

Error responses

Check HTTP status before reading success fields. Errors contain error and code; POST field validation may include issues with path and message.

Example · 422 Unprocessable Content
{
  "error": "Assigned agent was not found or is not in sales.",
  "code": "INVALID_AGENT"
}
Error response status codes, internal error codes, and descriptions
StatusCodeDescription
400INVALID_JSONThe request body is not valid JSON.
400INVALID_INPUTInvalid fields or query parameters. Lead creation errors include an issues array for field details.
401UNAUTHORIZEDThe bearer API key is missing or incorrect.
415INVALID_CONTENT_TYPESend Content-Type: application/json.
422INVALID_AGENTThe assigned user does not exist or is not eligible for sales assignment.
422INVALID_REFERRERThe supplied person’s referrer account was not found.
503API_NOT_CONFIGUREDThe site administrator needs to configure the API key.
503ASSIGNMENT_NOT_CONFIGUREDThe site administrator needs to enable assignment storage.
500INTERNAL_ERRORThe request could not be completed. Database details are not returned.

Correct 400/401/415/422 errors before retrying. Configuration errors require the administrator. Read-only GET requests may retry transient transport/5xx failures with bounded backoff. POST has no deduplication or idempotency-key support: after a timeout, connection loss, or uncertain response, reconcile the submission before retrying. Maintain the caller's submission ledger and save successful response IDs.