Integrations
Xero accounting
Connect Xero, then every invoice you mark paid in Zeng Book pushes to Xero as an AR invoice — automatically. Retention is preserved as a negative line item; GST is snapshotted at issue.
How it works
- Connect your Xero organisation from Settings → Integrations. (Owner / admin only.)
- Set your default revenue account code on the same page. Xero Singapore's default Sales account is
200— use that unless you want a different mapping. - Issue invoices as usual. Nothing happens on Xero's side until an invoice is marked PAID.
- When you mark an invoice paid (manually or via Stripe), Zeng Book pushes it to Xero as an AUTHORISED invoice. The push runs fire-and-forget— your "mark paid" click returns immediately; the push happens in the background and writes its outcome to the invoice's Xero sync card.
- If the push fails (Xero rate-limit, expired token, wrong account code, etc.), the invoice detail page shows a red error card with a Retry push button.
What gets pushed
Zeng Book maps your invoice into a Xero ACCREC (accounts-receivable) invoice:
{
"Type": "ACCREC",
"Contact": { "Name": "Tan Renovations Pte Ltd", "EmailAddress": "[email protected]" },
"Date": "2026-05-15",
"DueDate": "2026-06-14",
"InvoiceNumber": "INV-2026-0117",
"Reference": "Zeng Book INV-2026-0117",
"Status": "AUTHORISED",
"LineAmountTypes": "Exclusive",
"CurrencyCode": "SGD",
"LineItems": [
{
"Description": "Phase 2 — Carpentry installation (40%)",
"Quantity": 1,
"UnitAmount": 18000.00,
"AccountCode": "200",
"TaxType": "OUTPUT"
},
{
"Description": "Less retention (5.0%)",
"Quantity": 1,
"UnitAmount": -900.00,
"AccountCode": "200",
"TaxType": "OUTPUT"
}
]
}- Money— Zeng Book stores integer cents; the push divides by 100 for Xero's decimal-dollar format.
- Retention— a single negative line item at the same TaxType as the rest of the invoice. Math is identical to Zeng Book's subtotal → retention → net → GST → total order.
- TaxType —
OUTPUTwhen the invoice has a non-zero GST rate,NONEotherwise. - InvoiceNumber — copied verbatim from Zeng Book so both systems share the same identifier.
- Status — pushed as
AUTHORISED. Payment reconciliation in Xero is still manual today; automatic payment push is on the roadmap.
Idempotency & retries
The push is idempotent per Zeng Book invoice: each invoice maps to at most one Xero invoice. The mapping is tracked in the XeroInvoiceMapping table with three states:
- synced — push succeeded. A retry returns the existing Xero invoice ID with no duplicate create.
- failed — last attempt failed. The retry button deletes the failed row and tries again.
- (no row) — invoice has never been pushed. First push creates a fresh mapping.
The fire-and-forget push has a 15-second timeout and a try/catch around every external call — a Xero outage never blocks payment recording.
Scopes we request
offline_access
accounting.transactions
accounting.contacts
accounting.settingsWhat's in development
Contact sync
Zeng Book clients sync to Xero contacts on first invoice push. Deduplication by UEN when present, otherwise by name + email. Today contacts are auto-created by name on the invoice POST — full sync gives you control over de-duplication.
Chart-of-accounts mapping
Per-org UI to map Zeng Book sections (carpentry, electrical, etc.) to specific Xero revenue account codes. Today every line uses the single org-level default code.
Payment reconciliation
Automatically log a Payment against the pushed Xero invoice so it shows as PAID in Xero immediately, with the correct bank account code. Today the invoice lands as AUTHORISED and your accountant reconciles payment manually.
For developers
The implementation lives in lib/xero.ts:
pushInvoiceToXero(orgId, invoiceId)— idempotent, returnsXeroPushResult(never throws)mapInvoiceToXero(invoice, currency, accountCode)— pure function, easy to unit-testgetValidAccessToken(orgId)— refreshes the token if it's within 60s of expiry
The hook into the user flow is in lib/invoice-service.ts on the line where recordPaymentEntry flips status to PAID. Same hook in bulkMarkPaid.
To build a custom push pipeline, call pushInvoiceToXero directly from your own action / cron / webhook handler. Or pull invoices via the REST API and push them through your own pipeline.