Guide

Sync customers

Keep one Flux customer for each of your records: look it up by your reference, create it if it's missing, and update only what changed.

Scope: customers:write.

The pattern#

  1. Look up with GET /customers?externalRef=ws_42. Your reference is unique in the organization, so there's at most one match.
  2. Create if there's no match, with externalRef set and an Idempotency-Key derived from it.
  3. Update if there is, with PATCH and only the fields that changed.
const BASE = `${process.env.FLUX_BASE_URL}/api/v1`;
const HEADERS = { Authorization: `ApiKey ${process.env.FLUX_API_KEY}`, "Content-Type": "application/json" };

// workspace: your record. Returns the Flux customer.
export async function syncCustomer(workspace) {
  const ref = `ws_${workspace.id}`;
  const fields = {
    customerType: "company",
    companyName: workspace.companyName,
    legalName: workspace.legalName,
    email: workspace.billingEmail,
    taxId: workspace.tin,
    rcNumber: workspace.rcNumber,
    defaultCurrency: "NGN",
  };

  // 1. Look it up by your reference.
  const found = await fetch(`${BASE}/customers?externalRef=${encodeURIComponent(ref)}`, { headers: HEADERS }).then((r) => r.json());
  const existing = found.data[0];

  if (!existing) {
    // 2. Create it. The Idempotency-Key makes a retry safe.
    const res = await fetch(`${BASE}/customers`, {
      method: "POST",
      headers: { ...HEADERS, "Idempotency-Key": `create-customer-${ref}` },
      body: JSON.stringify({ ...fields, externalRef: ref, contacts: workspace.billingContacts }),
    });
    if (res.status === 409) throw new Error(`Needs a person: ${(await res.json()).message}`);
    if (!res.ok) throw new Error(`Create failed: ${res.status} ${await res.text()}`);
    return res.json();
  }

  // 3. Update only what changed. Leave contacts out unless you mean to replace them.
  const changed = Object.fromEntries(Object.entries(fields).filter(([k, v]) => v != null && existing[k] !== v));
  if (Object.keys(changed).length === 0) return existing;
  const res = await fetch(`${BASE}/customers/${existing.id}`, { method: "PATCH", headers: HEADERS, body: JSON.stringify(changed) });
  if (!res.ok) throw new Error(`Update failed: ${res.status} ${await res.text()}`);
  return res.json();
}

Billing contacts#

  • Sending contacts on PATCH replaces the whole list. Send it only when your list is the source of truth, and send every contact you want to keep.
  • List results return contacts: null. Get the customer by id to compare contacts.
  • Invoices go to the primary contact (or the customer's email) plus every contact with ccOnInvoices. ccOnInvoices defaults to true when you leave it out.

Conflicts#

409 conflict means another customer in the organization already has the same externalRef, tax ID, or name and email. That usually means someone created the customer in the Flux app. Don't retry: show the message to a person, who can set your externalRef on the existing customer (with PATCH) and link the two.

Changes made in Flux#

People can edit customers in the Flux app too. Subscribe to customer.created and customer.updated if you need to know, and decide which system wins for each field so the two don't overwrite each other.