CRM bridge
One request returns a contact and everything attached to them, so a sync job does not have to make five.
What problem this solves
The public API gives you customers, bookings, orders, refunds and escalations as separate resources. Syncing a CRM that way means one request to list contacts and then four more per contact. This returns the person and their work together.
Authentication
A standard API key with the customers:read scope. Contacts are always scoped to your own account.
curl -s "https://api.simcoai.co.uk/v1/crm/contacts?limit=25" \
-H "Authorization: Bearer $SIMCOAI_API_KEY" What comes back
{
"success": true,
"data": {
"count": 1,
"contacts": [
{
"contact": {
"id": "3f5b...",
"name": "Emily Carter",
"email": "emily.carter@example.com",
"phone": "+447700900456",
"status": "active",
"created_at": "2026-08-01T09:14:22.000Z",
"updated_at": "2026-08-19T14:32:07.114Z"
},
"bookings": [ { "id": "9f2c...", "service_name": "Haircut", "status": "requested" } ],
"orders": [],
"refunds": [],
"escalations": []
}
]
}
}Syncing only what changed
Pass since with an ISO timestamp and you get only the contacts updated after it. Store the highest updated_at you have seen and use it as the next since.
// A nightly sync that reads what changed rather than everything.
const BASE = 'https://api.simcoai.co.uk/v1';
async function syncContacts(since) {
const url = new URL(`${BASE}/crm/contacts`);
url.searchParams.set('limit', '100');
if (since) url.searchParams.set('since', since);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.SIMCOAI_API_KEY}` }
});
if (res.status === 429) {
// Back off and try again - see the rate limits page.
const wait = Number(res.headers.get('retry-after') || 5);
await new Promise((r) => setTimeout(r, wait * 1000));
return syncContacts(since);
}
if (!res.ok) throw new Error(`SIMCOAI ${res.status}`);
const { data } = await res.json();
let newest = since;
for (const entry of data.contacts) {
await upsertIntoYourCrm(entry.contact, entry);
if (!newest || entry.contact.updated_at > newest) newest = entry.contact.updated_at;
}
return newest; // store this and pass it back next run
}Fetching a single contact
curl -s https://api.simcoai.co.uk/v1/crm/contacts/CONTACT_ID \
-H "Authorization: Bearer $SIMCOAI_API_KEY" The same sync in Python
import os, requests
BASE = "https://api.simcoai.co.uk/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SIMCOAI_API_KEY']}"}
def sync_contacts(since=None):
params = {"limit": 100}
if since:
params["since"] = since
res = requests.get(f"{BASE}/crm/contacts", headers=HEADERS, params=params, timeout=20)
res.raise_for_status()
data = res.json()["data"]
newest = since
for entry in data["contacts"]:
contact = entry["contact"]
upsert_into_your_crm(contact, entry)
if newest is None or contact["updated_at"] > newest:
newest = contact["updated_at"]
return newestKeeping it up to date
Polling on a schedule is fine for a nightly sync. If you want a CRM to update the moment something happens, subscribe to the events instead - see Webhooks for the signed delivery format, or Zapier if you would rather not write anything.