How to Sync Customers or Suppliers with Unified's Accounting API
January 20, 2026
Syncing customers and suppliers sounds like a basic accounting task. It becomes a product problem the moment you support more than one accounting system.
Many platforms treat customers and vendors as separate objects. Others merge them. Some allow a single entity to be both. Identifiers aren't stable, names change, emails aren't unique, and 'vendor vs customer' is often inferred rather than explicit. When teams integrate vendor-by-vendor, this usually leads to duplicate records, broken updates, or brittle reconciliation logic.
For a PM, this raises hard questions early:
- Is a customer and a supplier the same logical entity in our product?
- What happens if a contact switches roles or has multiple roles?
- How do we guarantee idempotent syncs without relying on provider-specific IDs?
Many products solve this by branching logic per accounting platform or maintaining separate customer and vendor pipelines. That increases complexity and makes long-term maintenance expensive.
Unified's Accounting API takes a different approach. It provides a single normalized contact model—AccountingContact—and uses explicit boolean flags (is_customer, is_supplier) to represent role. Identity is stable, behavior is documented, and sync semantics are consistent across accounting providers.
This guide shows how to build a production-grade customer and supplier sync on top of that model—listing contacts, filtering by role, handling incremental updates, creating and updating records.
Prerequisites
You'll need:
- A Unified workspace
- An API key
- A
connectionIdfor an authorized accounting integration - Node.js 18+ (or equivalent runtime)
Install the SDK:
npm install @unified-api/typescript-sdk
Initialize the client:
import { UnifiedTo } from "@unified-api/typescript-sdk";
const unified = new UnifiedTo({
security: {
jwt: process.env.UNIFIED_API_KEY!,
},
});
The AccountingContact model (important)
Unified normalizes customers and suppliers into one object:
type AccountingContact = {
id?: string;
name?: string;
first_name?: string;
last_name?: string;
emails?: { email?: string; type?: "WORK" | "HOME" | "OTHER" }[];
telephones?: { telephone?: string; type?: "WORK" | "HOME" | "OTHER" | "FAX" | "MOBILE" }[];
billing_address?: { ... };
shipping_address?: { ... };
is_customer?: boolean;
is_supplier?: boolean;
is_active?: boolean;
tax_number?: string;
payment_methods?: { ... }[];
company_name?: string;
identification?: string;
updated_at?: string;
};
Key implications:
- Customers and suppliers are not separate endpoints.
- Filtering is done using
is_customer/is_supplier. - The stable identifier is
(connectionId, id).
Step 1: List accounting contacts
Use listAccountingContacts to fetch contacts for a connection.
const contacts = await unified.accounting.listAccountingContacts({
connectionId,
limit: 100,
});
Pagination rules
limitdefaults to 100 (maximum)- Use
offsetto paginate - When returned results
< limit, there are no more records
Example paginated fetch:
let offset = 0;
const allContacts: AccountingContact[] = [];
while (true) {
const page = await unified.accounting.listAccountingContacts({
connectionId,
limit: 100,
offset,
});
allContacts.push(...page);
if (page.length < 100) break;
offset += 100;
}
Step 2: Filter customers or suppliers
Since Unified uses a shared model, filter in your application:
const customers = allContacts.filter(c => c.is_customer);
const suppliers = allContacts.filter(c => c.is_supplier);
You can also filter out inactive records:
const activeCustomers = customers.filter(c => c.is_active !== false);
Step 3: Incremental sync using updated timestamps
To avoid full re-syncs, use updated_gte:
const updatedSince = "2025-01-01T00:00:00Z";
const updatedContacts = await unified.accounting.listAccountingContacts({
connectionId,
updatedGte: updatedSince,
limit: 100,
});
Notes:
- Dates must be ISO-8601
- Time and timezone are optional (defaults to UTC)
- This is the recommended polling strategy when webhooks aren't used
Step 4: Store sync keys correctly
For every contact you store locally, persist:
{
connectionId: string;
unifiedContactId: string; // AccountingContact.id
}
Do not rely on:
- name
identification- raw provider IDs
Those are not guaranteed to be unique or stable.
Step 5: Create a customer or supplier
To create a new contact, use createAccountingContact.
const created = await unified.accounting.createAccountingContact({
connectionId,
accountingContact: {
name: "Acme Corp",
is_customer: true,
emails: [{ email: "billing@acme.com", type: "WORK" }],
},
});
Important:
- This is a full create (POST)
- You must supply the fields you want set
- The response returns the created
AccountingContact
Step 6: Update a contact (PUT)
Unified's update method is PUT, shown as:
updateAccountingContact
This replaces the contact resource.
const updated = await unified.accounting.updateAccountingContact({
connectionId,
id: existingUnifiedContactId,
accountingContact: {
name: "Acme Corporation",
is_customer: true,
is_supplier: false,
},
});
Step 7: Delete a contact (optional)
To remove a contact:
await unified.accounting.removeAccountingContact({
connectionId,
id: unifiedContactId,
});
This maps to:
DELETE /accounting/{connection_id}/contact/{id}
Final notes
Customers and suppliers are one of the most failure-prone parts of accounting integrations, not because the data is complex, but because identity is often split across inconsistent models.
By treating customers and suppliers as a single normalized AccountingContact and relying on explicit role flags (is_customer, is_supplier), you avoid duplicate records, vendor-specific branching, and fragile reconciliation logic. The sync patterns in this guide are designed to be safe by default: stable identifiers, explicit pagination, timestamp-based incremental updates, and documented create/update/delete semantics.
Used this way, Unified's Accounting API gives you a predictable, maintainable foundation for customer and supplier syncs across accounting systems—without guessing, scraping, or vendor-specific code paths.