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.
https://referral.atap.solar/api/leads/sources(also: /api/leads/ads, /api/sources)https://referral.atap.solar/api/leads/agents(also: /api/agents)https://referral.atap.solar/api/leadshttps://referral.atap.solar/api/leads?referrer=Meta%20Ads%20LeonQuick 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.
- Obtain the existing
LEADS_API_KEYfrom the administrator and configure it as a backend environment secret. - Call
GET /api/leads/sourcesandGET /api/leads/agents. Select real returned source and agent IDs;USER_BUBBLE_IDin examples is a placeholder. - For a real incoming lead, call
POST /api/leadswithleadName,leadMobileNumber, andreferrer. AddassignedAgentIdto assign it, or omit the field to leave it pending. Save the returned IDs. - Call
GET /api/leadswith the source name or ID asreferrer. Page through results untilpagination.hasMoreis false.
{
"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.
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 --request GET 'https://referral.atap.solar/api/leads/sources' \
--header "Authorization: Bearer $LEADS_API_KEY"Response (`200 OK`)
{
"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 --request GET 'https://referral.atap.solar/api/leads/agents' \
--header "Authorization: Bearer $LEADS_API_KEY"Response (`200 OK`)
{
"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 --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.
{
"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.
// 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 --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.
"referrer": "Meta Ads Leon""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.
"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}$.
| Field | Required | Type | Description |
|---|---|---|---|
| leadName | Yes | string | Lead name, 2–120 characters. |
| leadMobileNumber | Yes | string | Malaysian mobile number. Local and +60 formats are accepted and normalized to 60… . |
| referrer | Yes | string | object | An ad source name (e.g. "Meta Ads Leon"), source ID ("src_..."), or { "customerId": "EXISTING_REFERRER_CUSTOMER_ID" }. |
| assignedAgentId | No | string | integer | Sales user’s Bubble ID (recommended) or positive numeric user ID. Omit to leave the lead unassigned. |
| leadState | No | string | Up to 120 characters. |
| leadCity | No | string | Up to 120 characters. |
| leadAddress | No | string | Up to 500 characters. |
| relationship | No | string | One of the relationship options below. Defaults to "Other". |
| projectType | No | string | One of the project types below. Defaults to "OTHERS". |
| remark | No | string | Up 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.
{
"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".
Error responses
Check HTTP status before reading success fields. Errors contain error and code; POST field validation may include issues with path and message.
{
"error": "Assigned agent was not found or is not in sales.",
"code": "INVALID_AGENT"
}| Status | Code | Description |
|---|---|---|
| 400 | INVALID_JSON | The request body is not valid JSON. |
| 400 | INVALID_INPUT | Invalid fields or query parameters. Lead creation errors include an issues array for field details. |
| 401 | UNAUTHORIZED | The bearer API key is missing or incorrect. |
| 415 | INVALID_CONTENT_TYPE | Send Content-Type: application/json. |
| 422 | INVALID_AGENT | The assigned user does not exist or is not eligible for sales assignment. |
| 422 | INVALID_REFERRER | The supplied person’s referrer account was not found. |
| 503 | API_NOT_CONFIGURED | The site administrator needs to configure the API key. |
| 503 | ASSIGNMENT_NOT_CONFIGURED | The site administrator needs to enable assignment storage. |
| 500 | INTERNAL_ERROR | The 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.