Guides
Sync leads to your CRM or database
To copy Vyostra AI leads into your own CRM or database, call GET /v1/leads with limit=200 and keep following nextCursor until it is null, saving each lead under its id. Run that loop on a schedule. The API has no webhooks yet, so polling is how a system outside Vyostra AI learns about new leads.
By Vinayak Tiwari, Co-Founder & Builder. Published .
How does a lead sync work?
A sync is a loop that runs on a schedule:
- Request a page of leads with
limit=200. - Save each lead in your system, keyed by its
id. - If
nextCursoris notnull, request the next page withcursorset to it. - Stop when
nextCursorisnull.
Because every write is keyed on the lead id, running the loop again changes nothing that has not changed. That makes a failed run safe to repeat from the start.
You need an API key with the leads:read scope. Quickstart shows how to create one.
What does the full loop look like in Node.js?
This script reads every lead and hands each page to a function you write. It needs Node.js 18 or later and no packages.
const BASE_URL = process.env.VYOSTRA_API_URL
const API_KEY = process.env.VYOSTRA_API_KEY
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
async function fetchLeadPage(cursor) {
const url = new URL('/v1/leads', BASE_URL)
url.searchParams.set('limit', '200')
if (cursor) url.searchParams.set('cursor', cursor)
while (true) {
const response = await fetch(url, { headers: { Authorization: `Bearer ${API_KEY}` } })
if (response.ok) return response.json()
if (response.status === 429) {
await sleep(Number(response.headers.get('Retry-After') ?? 60) * 1000)
continue
}
const { error } = await response.json()
throw new Error(`${response.status} ${error.code}: ${error.message}`)
}
}
async function syncLeads(saveLeads) {
let cursor = null
let saved = 0
do {
const page = await fetchLeadPage(cursor)
if (page.incompleteSources) {
throw new Error(`Leads from ${page.incompleteSources.join(', ')} could not be read. Try again later.`)
}
await saveLeads(page.data)
saved += page.data.length
cursor = page.nextCursor
} while (cursor)
return saved
}
// Replace this with an upsert into your CRM or database, keyed on lead.id.
async function saveLeads(leads) {
for (const lead of leads) {
console.log(lead.id, lead.source, lead.name, lead.phone, lead.email)
}
}
console.log(`Synced ${await syncLeads(saveLeads)} leads`)What does it look like in Python?
import os
import time
import requests
BASE_URL = os.environ["VYOSTRA_API_URL"]
HEADERS = {"Authorization": f"Bearer {os.environ['VYOSTRA_API_KEY']}"}
def fetch_lead_page(cursor=None):
params = {"limit": 200}
if cursor:
params["cursor"] = cursor
while True:
response = requests.get(f"{BASE_URL}/v1/leads", params=params, headers=HEADERS, timeout=30)
if response.status_code == 429:
time.sleep(int(response.headers.get("Retry-After", 60)))
continue
response.raise_for_status()
return response.json()
def sync_leads(save_leads):
cursor = None
saved = 0
while True:
page = fetch_lead_page(cursor)
if page.get("incompleteSources"):
raise RuntimeError(f"Could not read leads from {page['incompleteSources']}. Try again later.")
save_leads(page["data"])
saved += len(page["data"])
cursor = page["nextCursor"]
if cursor is None:
return saved
def save_leads(leads):
# Replace with an upsert into your CRM or database, keyed on lead["id"].
for lead in leads:
print(lead["id"], lead["source"], lead["name"], lead["phone"], lead["email"])
print("Synced", sync_leads(save_leads), "leads")Which lead fields should you map?
| Vyostra AI field | Typical CRM field | Note |
|---|---|---|
id | External id | Use it as the unique key. It never changes. |
name, phone, email | Contact details | Any of them can be null. |
source | Lead source | chat, form, meta or voice. |
sourceUrl | Landing page | The page the lead was captured on. |
attributes | Custom fields | Extra answers, as text keyed by field name. |
status, outcome | Stage | The lead's stage in the Vyostra AI dashboard. |
createdAt | Created date | ISO 8601, in UTC. |
To copy the chat conversation as well, call GET /v1/leads/:id for a lead and read transcript. That is one request per lead, so do it only for leads you have not stored before.
What should a sync watch out for?
- Order is by urgency, not date. Leads come back in dashboard inbox order. Do not stop at the first lead you recognise; read to the last page.
- A lead can move while you read. If a lead's status changes in the dashboard during a run, it can shift between pages. Keying on
idmeans a lead seen twice is written once, and the next run picks up one that was missed. incompleteSourcesmeans stop. When it is present, one lead source could not be read and its leads are missing from the page. Treat the run as failed and try again later.- Meta and voice leads are capped. The list holds the 500 most recent Meta lead ad leads and the 500 most recent voice leads. Sync regularly so older ones are already stored before they fall out of the list.
- The API is read-only. Changing a lead in your CRM does not change it in Vyostra AI.
Do you need this for Zoho CRM?
Not always. Vyostra AI has a built-in Zoho CRM integration that creates leads from lead forms and Meta lead ads in Zoho automatically. Use the API when you need chat or voice leads as well, or a CRM other than Zoho.
Common questions
How often should I poll the Vyostra AI leads API?
Every few minutes is enough for most uses. Each Vyostra AI API key may make 120 requests a minute and a full read of 1,000 leads takes 5 requests at limit=200, so a poll every 5 minutes uses a small fraction of the limit. For an instant alert on each new lead, use the built-in WhatsApp lead alerts instead of polling faster.
How do I avoid importing the same lead twice?
Store the id field of each lead and use it as the unique key in your own system. A Vyostra AI lead id never changes, so writing with an upsert keyed on it makes every run safe to repeat.
Can I fetch only the leads created since my last sync?
Not with a filter. The Vyostra AI leads endpoint has no date parameter today, so a sync reads every page and relies on the lead id to skip what it already has. Each lead has a createdAt timestamp if you want to filter on your side.