Developers
GoldenSocial API
Show the leads GoldenSocial finds for you in your own portal, website back office or CRM.
Overview
There are two ways to get your leads out of GoldenSocial, and you can use both:
- Webhook (push): we POST each new lead, and each status or notes change, to a URL you choose. Set it in Settings → Send leads to your CRM. See Webhooks.
- API (pull): your server asks for leads when it wants them, and can update a lead’s status or notes.
The API is free for every GoldenSocial account. It returns only your own leads: the ones from the accounts you connected, the posts you saved and your searches.
Server-side only. A key gives full access to your leads. Call the API from your server, never from browser or mobile app code, and never commit a key to a public repository.
Authentication
Create a key in Settings → API keys. The full key (it starts with gsk_) is shown once, when you create it: store it in your server’s secrets. You can have several keys, one per integration, and revoke any of them at any time.
Send it in the Authorization header of every request:
Authorization: Bearer gsk_…A missing, wrong or revoked key gets 401 Unauthorized.
Rate limits
Each key may make 120 requests per minute. Every response carries:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute for this key (120). |
X-RateLimit-Remaining | Requests left in the current minute. |
Retry-After | Only on 429: seconds to wait before trying again. |
Over the limit you get 429 Too Many Requests. Wait for Retry-After seconds, then continue.
Endpoints
All paths are under https://api.YOUR-DOMAIN/api/v1. Bodies and responses are JSON; dates are ISO 8601 strings in UTC.
GET/v1/me
The account the key belongs to. Handy to check a key works.
curl https://api.YOUR-DOMAIN/api/v1/me \
-H "Authorization: Bearer $GOLDENSOCIAL_KEY"{
"id": "cm0z9y8x7w6v5u4t3s2r1q",
"email": "agent@example.com",
"name": "Asha Verma"
}GET/v1/leads
Your leads, oldest first (by createdAt, or by updatedAt when you pass updatedSince). All query parameters are optional.
| Parameter | Values | Meaning |
|---|---|---|
status | new, qualified, dismissed, converted | Only leads with this status. |
platform | facebook, instagram, whatsapp, x | Only leads from this platform. |
intent | buying, renting, selling, complaint, spam, other | Only leads with this detected intent. |
minScore | 0–100 | Only leads with a score at or above this. |
since | ISO date | Only leads created after this moment. |
updatedSince | ISO date | Only leads changed after this moment; sorts by updatedAt. |
cursor | string | The nextCursor from the previous page. |
limit | 1–100 (default 50) | Leads per page. |
A value outside these lists gets a 400 with a message saying which one.
curl "https://api.YOUR-DOMAIN/api/v1/leads?status=new&minScore=60&limit=2" \
-H "Authorization: Bearer $GOLDENSOCIAL_KEY"{
"data": [
{
"id": "cm1q2w3e4r5t6y7u8i9o0p",
"platform": "facebook",
"kind": "comment",
"source": "webhook",
"sourceDetail": null,
"account": { "id": "cm1a2b3c4d5e6f7g8h9i0j", "name": "Sunrise Homes" },
"author": { "id": "1029384756", "name": "Priya Sharma", "url": "https://facebook.com/1029384756" },
"contact": { "name": null, "email": null, "phone": null },
"content": "Is the 2BHK near the metro still available? Looking to buy this month.",
"url": "https://facebook.com/…",
"intent": "buying",
"score": 72,
"matchedKeywords": ["2bhk", "looking to buy"],
"status": "new",
"notes": null,
"occurredAt": "2026-10-01T09:12:44.000Z",
"createdAt": "2026-10-01T09:12:47.311Z",
"updatedAt": "2026-10-01T09:12:47.311Z"
},
{ … }
],
"nextCursor": "eyJjIjoiMjAyNi0xMC0wMVQwOToxMjo0Ny4zMTFaIn0"
}nextCursor is null on the last page.
GET/v1/leads/:id
One lead. A lead that does not exist, or is not yours, gets 404.
curl https://api.YOUR-DOMAIN/api/v1/leads/cm1q2w3e4r5t6y7u8i9o0p \
-H "Authorization: Bearer $GOLDENSOCIAL_KEY"The response is a lead object.
PATCH/v1/leads/:id
Change a lead’s status and/or notes (send only what changes; null clears the notes). Returns the updated lead. The change is visible in GoldenSocial straight away and fires a lead.updated webhook.
curl -X PATCH https://api.YOUR-DOMAIN/api/v1/leads/cm1q2w3e4r5t6y7u8i9o0p \
-H "Authorization: Bearer $GOLDENSOCIAL_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "qualified", "notes": "Called, site visit on Saturday" }'{
"id": "cm1q2w3e4r5t6y7u8i9o0p",
…
"status": "qualified",
"notes": "Called, site visit on Saturday",
"updatedAt": "2026-10-01T10:02:15.908Z"
}GET/v1/keywords
The keywords you track. negative: true means leads containing the phrase are skipped.
curl https://api.YOUR-DOMAIN/api/v1/keywords \
-H "Authorization: Bearer $GOLDENSOCIAL_KEY"{
"data": [
{ "id": "cm2k…", "phrase": "looking to buy", "negative": false },
{ "id": "cm2l…", "phrase": "job vacancy", "negative": true }
]
}The lead object
The same shape in API responses and webhook bodies. A value we do not know is null, never a guess.
| Field | Type | Meaning |
|---|---|---|
id | string | Stable ID. Use it as your unique key. |
platform | string | facebook, instagram, whatsapp or x. |
kind | string | comment, message, lead (a Lead Ads form) or post. |
source | string | webhook (your connected account), extension (saved by you), x_search or ig_hashtag. |
sourceDetail | string | null | Extra detail about where it came from, if any. |
account | { id, name } | null | Your connected account it arrived on. |
author | { id, name, url } | The person who wrote it; each field may be null. |
contact | { name, email, phone } | Contact details they gave (Lead Ads forms, WhatsApp); each may be null. |
content | string | The text of the comment, message, form or post. |
url | string | null | Link to the original on the platform. |
intent | string | null | buying, renting, selling, complaint, spam or other. |
score | number | null | 0–100, computed by rules (not AI). 60+ is hot, 30+ warm. |
matchedKeywords | string[] | Your keywords found in the content. |
status | string | new, qualified, dismissed or converted. |
notes | string | null | Your notes. |
occurredAt | string | null | When it was posted on the platform, if known. |
createdAt | string | When GoldenSocial received it. |
updatedAt | string | Last change (status, notes or refreshed content). |
{
"id": "cm1q2w3e4r5t6y7u8i9o0p",
"platform": "facebook",
"kind": "comment",
"source": "webhook",
"sourceDetail": null,
"account": { "id": "cm1a2b3c4d5e6f7g8h9i0j", "name": "Sunrise Homes" },
"author": { "id": "1029384756", "name": "Priya Sharma", "url": "https://facebook.com/1029384756" },
"contact": { "name": null, "email": null, "phone": null },
"content": "Is the 2BHK near the metro still available? Looking to buy this month.",
"url": "https://facebook.com/…",
"intent": "buying",
"score": 72,
"matchedKeywords": ["2bhk", "looking to buy"],
"status": "new",
"notes": null,
"occurredAt": "2026-10-01T09:12:44.000Z",
"createdAt": "2026-10-01T09:12:47.311Z",
"updatedAt": "2026-10-01T09:12:47.311Z"
}Pagination & syncing
Lists come back oldest first, at most limit per page. While nextCursor is not null, ask again with cursor=<nextCursor> to get the next page.
To keep your own copy in sync, pick one:
- Poll for new leads: call
GET /v1/leads?since=<createdAt of the last lead you stored>, or keep the lastnextCursorand continue from it. Every few minutes is plenty. - Poll for changes: call
GET /v1/leads?updatedSince=<the last updatedAt you saw>to pick up status and notes changes, sorted byupdatedAt. - Skip polling: use the webhook; we push each new lead and each change as it happens.
Always upsert on id: the same lead can reach you twice (a retry, or both a webhook and a poll).
// Pull everything new since the last run (Node.js 18+)
let cursor = null;
do {
const url = new URL("https://api.YOUR-DOMAIN/api/v1/leads");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
else if (lastCreatedAt) url.searchParams.set("since", lastCreatedAt);
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.GOLDENSOCIAL_KEY}` } });
if (res.status === 429) {
await new Promise((r) => setTimeout(r, Number(res.headers.get("Retry-After") ?? 60) * 1000));
continue;
}
if (!res.ok) throw new Error(`GoldenSocial API: ${res.status}`);
const page = await res.json();
for (const lead of page.data) await upsertLead(lead); // your code
cursor = page.nextCursor;
} while (cursor);Webhooks
Set a URL in Settings → Send leads to your CRM. When you save it you get a signing secret, shown once. We then POST JSON to your URL for these events:
| Event | When |
|---|---|
lead.created | A new lead arrives. |
lead.updated | A lead’s status or notes change (in the app or through the API). |
lead.test | You click “Send a test” in Settings. |
Request
POST <your URL>
Content-Type: application/json
X-GoldenSocial-Event: lead.created
X-GoldenSocial-Signature: sha256=5f2b…c9e1
{
"event": "lead.created",
"sentAt": "2026-10-01T09:12:48.020Z",
"lead": {
"id": "cm1q2w3e4r5t6y7u8i9o0p",
"platform": "facebook",
"kind": "comment",
"source": "webhook",
"sourceDetail": null,
"account": { "id": "cm1a2b3c4d5e6f7g8h9i0j", "name": "Sunrise Homes" },
"author": { "id": "1029384756", "name": "Priya Sharma", "url": "https://facebook.com/1029384756" },
"contact": { "name": null, "email": null, "phone": null },
"content": "Is the 2BHK near the metro still available? Looking to buy this month.",
"url": "https://facebook.com/…",
"intent": "buying",
"score": 72,
"matchedKeywords": ["2bhk", "looking to buy"],
"status": "new",
"notes": null,
"occurredAt": "2026-10-01T09:12:44.000Z",
"createdAt": "2026-10-01T09:12:47.311Z",
"updatedAt": "2026-10-01T09:12:47.311Z"
}
}| Header | Meaning |
|---|---|
X-GoldenSocial-Event | The event name, the same as the event field in the body. |
X-GoldenSocial-Signature | sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with your signing secret. |
Retries
Answer with any 2xx status, quickly. On a network error, 408, 429 or a 5xx, we try again: 3 tries in all, after 2 seconds and then 8 seconds. Other statuses (for example 400 or 401) are not retried. The result of the last delivery is shown in Settings.
Verifying the signature
Compute the HMAC over the raw body, before any JSON parsing, and compare in constant time. Node.js with Express:
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.GOLDENSOCIAL_WEBHOOK_SECRET;
// Keep the raw body: the signature is computed over the exact bytes we sent.
app.post("/goldensocial", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body; // a Buffer
const header = req.get("X-GoldenSocial-Signature") ?? "";
const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(rawBody).digest("hex");
const a = Buffer.from(header);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).end();
}
const { event, lead } = JSON.parse(rawBody.toString("utf8"));
if (event === "lead.created") {
// insert the lead (use lead.id as your unique key)
} else if (event === "lead.updated") {
// update status / notes for lead.id
}
res.status(200).end(); // answer quickly; do slow work afterwards
});
app.listen(3000);Errors
Errors use the usual HTTP statuses and always have the same JSON shape:
{
"statusCode": 400,
"message": "limit must be between 1 and 100",
"error": "Bad Request"
}| Status | When |
|---|---|
400 | A query parameter or body field is invalid; message says which. |
401 | The key is missing, wrong or revoked. |
404 | The lead does not exist or is not yours (same answer for both). |
429 | Too many requests: wait for Retry-After seconds. |