CRM bridge

One request returns a contact and everything attached to them, so a sync job does not have to make five.

Why

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.

This is a sync surface, not a reporting one. Each record type is capped per contact so a large account cannot time out a single request. For full history, read the individual resources with paging.
Auth

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" 
Shape

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": []
      }
    ]
  }
}
Incremental

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
}
One

Fetching a single contact

curl -s https://api.simcoai.co.uk/v1/crm/contacts/CONTACT_ID \
  -H "Authorization: Bearer $SIMCOAI_API_KEY" 
Python

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 newest
Live

Keeping 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.

Careful. Contacts include personal data. Whatever you sync it into is yours to secure, and your own retention and lawful-basis obligations apply to the copy you hold. See our GDPR page for how SIMCOAI handles it on our side.