How to Create and Sync Invoices with Unified's Accounting API
January 22, 2026
Invoicing is one of the most opinionated parts of accounting systems—and one of the easiest places for integrations to fail.
Across providers, invoices differ in how they're classified, when they can be edited, how they're delivered, and what 'paid' actually means. Some systems allow drafts to be modified freely. Others lock invoices once authorized. Payment collection, tax handling, and delivery behavior vary widely, even when the surface concepts look identical.
For a PM or backend team, this creates immediate risk:
- Can invoices be created and updated safely across providers without branching logic?
- When is an invoice immutable, and how should failures be surfaced to users?
- How do you control delivery and payment behavior without relying on undocumented defaults?
Many teams discover too late that invoice logic baked for one accounting system doesn't generalize. The result is partial writes, rejected updates, or invoices that don't behave the way users expect.
Unified's Accounting API is designed to make invoice automation explicit and defensible. Invoices are represented through a normalized AccountingInvoice model, with classification handled at the line-item level and lifecycle rules enforced by the provider rather than guessed upstream. Core behaviors—listing, syncing, creating, updating, delivering, and removing invoices—are exposed through documented parameters with consistent pagination and error semantics.
This guide shows how to build a production-grade invoice sync and write flow using Unified's Accounting API—resolving dependencies, creating classified invoices, controlling delivery and payment behavior, and handling provider constraints without vendor-specific code paths or assumptions.
What this guide covers
- Listing invoices with pagination and incremental sync
- Resolving required invoice dependencies (contacts, accounts, categories, tax rates)
- Creating invoices with classified line items
- Controlling invoice delivery and payment behavior
- Updating and removing invoices
- Handling pagination, rate limits, and provider constraints
Prerequisites
- Node.js 18+
- A Unified workspace
- An Accounting integration authorized for a customer
- A valid
connectionId - Unified API key
Step 1: Initialize the SDK
import { UnifiedTo } from '@unified-api/typescript-sdk';
const sdk = new UnifiedTo({
security: {
jwt: process.env.UNIFIED_API_KEY!,
},
});
All Accounting API calls are scoped by connectionId, which represents a single authorized customer integration.
Step 2: Understand the Invoice data model (critical)
Unified represents invoices using the normalized AccountingInvoice object.
Key characteristics:
- Invoices are customer-facing and reference a
contact_id - Classification happens at the line-item level
- Line items may reference:
account_id→AccountingAccountcategory_ids[]→AccountingCategorytaxrate_id→AccountingTaxrate
- Invoice lifecycle is expressed via
status - Payment and delivery behavior is controlled via
payment_collection_methodandsend - Provider rules (required fields, allowed transitions) are enforced downstream
Invoice status values
'DRAFT'
'VOIDED'
'AUTHORIZED'
'PAID'
'PARTIALLY_PAID'
'PARTIALLY_REFUNDED'
'REFUNDED'
'SUBMITTED'
'DELETED'
'OVERDUE'
These values are normalized, but providers may return additional states. Treat this as an open set.
Step 3: List invoices (sync in)
To sync invoices into your system, use listAccountingInvoices.
const results = await sdk.accounting.listAccountingInvoices({
connectionId,
limit: 50,
offset: 0,
updated_gte: '2026-01-22T22:41:23.849Z',
sort: 'updated_at',
order: 'asc',
query: '',
contact_id: '',
org_id: '',
type: '',
fields: '',
raw: '',
});
Pagination rules
- Default page size: 100
- Maximum page size: 100 on most endpoints
- Pagination uses offset, not cursors
- You've reached the end when
results.length < limit
Incremental sync
Use updated_gte to fetch only invoices updated since your last sync timestamp. This is the recommended polling strategy.
Step 4: Retrieve a single invoice
Use this to verify creates/updates or reconcile provider-side changes.
const invoice = await sdk.accounting.getAccountingInvoice({
connectionId,
id: invoiceId,
fields: '',
raw: '',
});
Step 5: Resolve invoice dependencies
Before creating an invoice, resolve the objects it references.
Resolve the customer (contact)
Invoices require a contact_id.
const contacts = await sdk.accounting.listAccountingContacts({
connectionId,
limit: 50,
offset: 0,
updated_gte: '2026-01-22T22:41:23.849Z',
sort: 'updated_at',
order: 'asc',
query: '',
type: 'CUSTOMER',
org_id: '',
fields: '',
raw: '',
});
Select the appropriate AccountingContact.id.
Resolve accounts (optional but common)
const accounts = await sdk.accounting.listAccountingAccounts({
connectionId,
limit: 50,
offset: 0,
updated_gte: '2026-01-20T00:59:41.301Z',
sort: 'updated_at',
order: 'asc',
query: '',
org_id: '',
fields: '',
raw: '',
});
Use an account ID when your provider supports line-item account classification.
Resolve categories (optional)
const categories = await sdk.accounting.listAccountingCategories({
connectionId,
limit: 50,
offset: 0,
updated_gte: '2026-01-20T00:59:41.301Z',
sort: 'updated_at',
order: 'asc',
query: '',
parent_id: '',
fields: '',
raw: '',
});
Resolve tax rates (optional)
const taxrates = await sdk.accounting.listAccountingTaxrates({
connectionId,
limit: 50,
offset: 0,
updated_gte: '2026-01-20T00:59:41.301Z',
sort: 'updated_at',
order: 'asc',
query: '',
org_id: '',
fields: '',
raw: '',
});
Step 6: Build a provider-safe invoice payload
A minimal, classification-aware invoice payload:
const invoice = {
contact_id: customerId,
currency: 'USD',
invoice_number: 'INV-10042',
notes: 'January subscription',
payment_collection_method: 'send_invoice',
send: true,
lineitems: [
{
unit_quantity: 1,
unit_amount: 500.0,
tax_amount: 50.0,
account_id: revenueAccountId,
category_ids: [subscriptionCategoryId],
taxrate_id: salesTaxId,
item_name: 'Pro Plan – January',
},
],
};
Important notes:
- Totals are typically derived as
unit_quantity * unit_amount + tax_amount - Providers may recompute totals or reject mismatches
- Leave optional fields unset unless you are certain the provider supports them
Step 7: Create the invoice
const created = await sdk.accounting.createAccountingInvoice({
connectionId,
accountingInvoice: invoice,
fields: '',
raw: '',
});
The response is a single AccountingInvoice object reflecting the provider's current state.
Step 8: Control invoice delivery and payment behavior
Send invoice (email / link)
payment_collection_method: 'send_invoice'
send: true
This typically emails the invoice or makes a payment link available via invoice.url.
Charge automatically
payment_collection_method: 'charge_automatically'
send: true
This attempts to charge the customer's default payment method.
Important: Provider behavior varies. Some platforms require additional configuration or a default payment method on the contact. Some may ignore send depending on provider capabilities.
Step 9: Update an invoice
To update an invoice, supply its id.
const updated = await sdk.accounting.updateAccountingInvoice({
connectionId,
id: invoiceId,
accountingInvoice: {
notes: 'Updated invoice notes',
},
fields: '',
raw: '',
});
Invoice updates may be restricted by provider lifecycle rules. For example, some providers do not allow updates once an invoice is submitted or paid.
Step 10: Remove an invoice (optional)
await sdk.accounting.removeAccountingInvoice({
connectionId,
id: invoiceId,
});
Use this cautiously. Some providers treat removal as a soft delete or disallow deletion in certain states.
Pagination, filtering, and querying
All list endpoints share the same semantics:
limit/offsetpaginationupdated_gtefor incremental sync- Sorting via
sort(name,updated_at,created_at) - Ordering via
order(asc,desc)
Query limitations
queryfilters by name or email only- Behavior is integration-specific
- For complex filtering, Unified strongly recommends syncing data into your own database
Rate limits and retries
Unified enforces:
- Upstream provider rate limits
- Unified-level fairness limits
Rate limit errors return HTTP 429.
Recommended handling
- Exponential backoff (1s → 2s → 4s → 8s)
- Add jitter
- Cap retry attempts
- Queue requests for high-volume operations
Strong recommendation
For continuous invoice sync or lifecycle tracking, use webhooks instead of polling. Unified manages retries, backoff, and initial sync for you.
Error handling
Unified uses standard HTTP status codes:
400– invalid parameters401– broken or revoked connection403– missing scopes404– resource not found429– rate limit exceeded501– feature not supported by this integration500– internal error (retryable)
Errors are often provider-specific and should be surfaced clearly to users.
Summary
Using Unified's Accounting API, you can:
- List, create, update, and remove invoices across accounting platforms
- Classify invoice line items using normalized accounts, categories, and tax rates
- Control invoice delivery and payment behavior through a single API
- Handle pagination, rate limits, and provider constraints safely
The key design principle is defensive integration: rely on normalized models, expect provider variance, and avoid assuming required fields or lifecycle transitions.
If you need real-time invoice updates or high-volume sync, pair this workflow with Unified webhooks.