Kwill

Sync clients from your CRM

List and create Kwill clients from an external CRM, deduplicating by email and handling rate limits.

This guide walks through a full sync loop: fetch all existing Kwill clients, compare them to your CRM contacts by email, and create any that are missing.

APIs used: GET /clients · POST /clients Related concepts: Pagination · Rate limits


Prerequisites

  • KWILL_API_KEY set in your environment
  • Your CRM exports contacts as an array of { name, email } objects

Step 1 — Fetch all existing Kwill clients

GET /clients returns a bare array. Use ?limit=500 (the maximum) to fetch all records. Store emails in a Set for O(1) lookups.

const BASE = 'https://usekwill.com/api/v1';
const HEADERS = { Authorization: `Bearer ${process.env.KWILL_API_KEY}` };

async function kwillGet(path, params = {}) {
  const url = new URL(BASE + path);
  Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
  const res = await fetch(url, { headers: HEADERS });
  if (res.status === 429) {
    const wait = parseInt(res.headers.get('Retry-After') ?? '5', 10) * 1000;
    await new Promise((r) => setTimeout(r, wait + Math.random() * 1000));
    return kwillGet(path, params);
  }
  if (!res.ok) throw new Error(`GET ${path} failed: ${res.status}`);
  return res.json();
}

async function fetchAllClients() {
  const existing = new Set();
  const clients = await kwillGet('/clients', { limit: 500 });
  for (const c of clients) {
    if (c.email) existing.add(c.email.toLowerCase());
  }
  return existing;
}

Step 2 — Create missing clients

For each CRM contact not already in Kwill, call POST /clients. Include backoff on 429.

async function kwillPost(path, body) {
  const res = await fetch(BASE + path, {
    method: 'POST',
    headers: { ...HEADERS, 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  if (res.status === 429) {
    const wait = parseInt(res.headers.get('Retry-After') ?? '5', 10) * 1000;
    await new Promise((r) => setTimeout(r, wait + Math.random() * 1000));
    return kwillPost(path, body);
  }
  if (!res.ok) throw new Error(`POST ${path} failed: ${res.status}`);
  return res.json();
}

async function syncClients(crmContacts) {
  const existing = await fetchAllClients();

  for (const contact of crmContacts) {
    if (!contact.email || existing.has(contact.email.toLowerCase())) continue;

    const client = await kwillPost('/clients', {
      name: contact.name,
      email: contact.email,
    });
    console.log(`Created client ${client.id} for ${contact.email}`);
  }
}

// Usage
const crmContacts = [
  { name: 'Acme Corp', email: 'billing@acme.com' },
  { name: 'Globex', email: 'ap@globex.com' },
];
await syncClients(crmContacts);

Complete script

// sync-clients.mjs — run with: node sync-clients.mjs
// Requires: KWILL_API_KEY in environment

const BASE = 'https://usekwill.com/api/v1';
const HEADERS = { Authorization: `Bearer ${process.env.KWILL_API_KEY}` };

async function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }

async function kwillGet(path, params = {}) {
  const url = new URL(BASE + path);
  Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
  const res = await fetch(url, { headers: HEADERS });
  if (res.status === 429) {
    const wait = parseInt(res.headers.get('Retry-After') ?? '5', 10);
    await sleep(wait * 1000 + Math.random() * 1000);
    return kwillGet(path, params);
  }
  if (!res.ok) throw new Error(`GET ${path} → ${res.status}`);
  return res.json();
}

async function kwillPost(path, body) {
  const res = await fetch(BASE + path, {
    method: 'POST',
    headers: { ...HEADERS, 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  if (res.status === 429) {
    const wait = parseInt(res.headers.get('Retry-After') ?? '5', 10);
    await sleep(wait * 1000 + Math.random() * 1000);
    return kwillPost(path, body);
  }
  if (!res.ok) throw new Error(`POST ${path} → ${res.status}`);
  return res.json();
}

async function fetchAllClients() {
  const emails = new Set();
  const clients = await kwillGet('/clients', { limit: 500 });
  for (const c of clients) if (c.email) emails.add(c.email.toLowerCase());
  return emails;
}

// Replace with your CRM's export
const CRM_CONTACTS = [
  { name: 'Acme Corp', email: 'billing@acme.com' },
  { name: 'Globex', email: 'ap@globex.com' },
];

const existing = await fetchAllClients();
let created = 0;

for (const contact of CRM_CONTACTS) {
  if (!contact.email || existing.has(contact.email.toLowerCase())) continue;
  const client = await kwillPost('/clients', { name: contact.name, email: contact.email });
  console.log(`Created ${client.id}  ${contact.email}`);
  created++;
}

console.log(`Done — ${created} client(s) created.`);

On this page