Accounting System Integration
Moonlight integrates with external accounting systems through a pluggable provider architecture. It also supports a fully manual mode where an accountant works directly within Moonlight without any external system.
Supported Providers
| Provider | Status | Description |
|---|---|---|
| Manual | Built-in | No external system, accountant reviews in Moonlight |
| Intuit (QuickBooks) | Implemented | Full push/pull sync, OAuth2, webhooks |
| Custom providers | Extensible | Implement the AccountingProvider interface |
Sync Modes
Each connection is configured with a syncMode that controls how data flows:
| Mode | Description |
|---|---|
MANUAL | No external system. Accountant reviews and approves entries entirely in Moonlight |
MANUAL_PUSH | Accountant manually triggers push to external system per entry |
AUTO_PUSH | Automatically pushes to external system when review is approved |
BIDIRECTIONAL | Push + scheduled pull from external system |
Integration Patterns
Pattern A: Manual Review (no external system)
Pattern B: SDK → Moonlight → Accounting → Moonlight
Pattern C: Full Auto (no review)
Accounting Review Workflow
When requireReview = true on a connection, entries go through an accounting review queue before sync:
How Entries Enter the Review Queue
Entries are automatically queued for review when:
- Created via API (
POST /entries) — if a connection withrequireReview = trueexists for the workspace, a review is automatically created - Ingested via SDK (
POST /api/v1/ingest) — same auto-queuing logic after the ingest pipeline processes the entry - Backfill — when a new connection is created, use
POST /accounting/connections/{id}/backfill-reviewsto queue all existing entries for review (or click "Queue Entries for Review" in the UI) - Manual — create a review via
POST /accounting/reviewsfor a specific entry
Review Features
- Assignment: Accountant takes a review from the queue
- Category assignment: Assign a chart of accounts category during approval
- Document verification: Optionally require
isComplete(approved BASIS + PAYMENT_PROOF docs + transaction) before review approval - Batch operations: Approve or reject multiple reviews at once
- Invalidation: If an entry is modified while in review, the review is automatically reset to QUEUED
- Backfill: Queue all existing entries for a connection's review queue in one action
Chart of Accounts
Moonlight maintains an internal chart of accounts (accounting_category) with:
- Hierarchical categories (parent/child)
- Account types: ASSET, LIABILITY, EQUITY, REVENUE, EXPENSE
- External mappings per provider (e.g., QuickBooks account ID)
Categories can be:
- Created manually in Moonlight
- Pulled from an external system via
POST /accounting/sync/pull-chart-of-accounts
Party & Tax Mapping
Party Mapping
Map Moonlight parties to external system vendors/customers:
| Moonlight Party | → | External Entity |
|---|---|---|
Party #5 (Vendor) | → | QB Vendor ID 123 |
Party #8 (Customer) | → | QB Customer ID 456 |
When pushing an entry, the mapped external vendor/customer is automatically set on the Invoice/Bill.
Tax Mapping
Map Moonlight tax rates to external tax codes for accurate tax handling during sync.
Connection-Project Routing
A workspace may have multiple connections (e.g., QB US entity + QB UK entity). Use project routing to control which entries sync to which connection:
- If a connection has specific projects assigned, only entries from those projects sync through it
- If no projects are assigned, the connection handles all projects in the workspace
Sync Lifecycle
Sync Statuses
| Status | Description |
|---|---|
PENDING | Sync initiated, not yet completed |
SYNCED | Successfully synced with external system |
FAILED | Sync failed (error message stored, auto-retry with backoff) |
STALE | Entry was modified after sync — needs re-push |
CONFLICT | Data conflict between systems — requires manual resolution |
Re-sync After Edit
When a synced entry is modified (status change, amount change), the sync record is automatically marked STALE. The accountant can then trigger a re-push which updates the existing Invoice/Bill in the external system.
Void/Refund Propagation
When an entry is voided or refunded in Moonlight:
- The corresponding Invoice/Bill in the external system is automatically voided
- A
VOIDED_EXTERNALevent is recorded in the audit trail
Conflict Resolution
When a conflict is detected:
- Choose
KEEP_LOCALto re-push Moonlight's version - Choose
KEEP_EXTERNALto accept the external system's version
Retry Mechanism
Failed syncs are reprocessed by a periodic Cloud Scheduler tick (*/5 * * * *) that publishes a RETRY_FAILED message to the scheduler-tick topic. The scheduler handler then republishes a { type: "RETRY_FAILED" } message to accounting-sync, which the AccountingSyncWorker consumes to retry up to 5 failed sync rows per tick. In addition, every accounting-sync subscription has a Pub/Sub retry policy (minBackoff=5s, maxBackoff=20s) and a dead-letter topic (accounting-sync-dlq) wired with maxDeliveryAttempts=5, so transient infrastructure failures are retried automatically and poison messages are quarantined for inspection.
Scheduled Pulls
For connections with syncMode = BIDIRECTIONAL and a syncScheduleCron, a Cloud Scheduler tick (* * * * *) publishes SCHEDULED_PULL_TICK to scheduler-tick. The scheduler handler iterates active connections, evaluates each cron expression against last_scheduled_pull_check_at (persisted in accounting_connection, so the schedule survives Cloud Run restarts), and publishes { type: "SCHEDULED_PULL", connectionId, since } to accounting-sync for each due connection.
Accounting Periods
Define accounting periods (e.g., Q1 2026, FY 2025) and close them to prevent modifications:
- Entries within a closed period cannot be synced or modified
- Periods can be reopened if needed
- Overlapping periods are not allowed
Setup
1. Create a Connection
POST /accounting/connections
{
"workspaceId": 1,
"providerType": "INTUIT",
"name": "QuickBooks Production",
"syncMode": "AUTO_PUSH",
"requireReview": true,
"requireCompletenessForSync": true,
"credentials": {
"clientId": "your-client-id",
"clientSecret": "your-client-secret",
"realmId": "your-realm-id",
"accessToken": "...",
"refreshToken": "...",
"tokenExpiresAt": "2026-12-31T00:00:00Z"
},
"projectIds": [1, 2]
}
2. Set Up Mappings
POST /accounting/mappings/parties
{
"connectionId": "uuid",
"partyId": 5,
"externalId": "123",
"externalType": "Vendor",
"externalName": "Acme Corp"
}
3. Pull Chart of Accounts
POST /accounting/sync/pull-chart-of-accounts
{
"connectionId": "uuid"
}
4. Create Review and Approve
POST /accounting/reviews
{ "entryId": 42, "connectionId": "uuid" }
PATCH /accounting/reviews/{reviewId}/approve
{ "categoryId": "category-uuid", "notes": "Verified" }
5. Push Entry (manual)
POST /accounting/sync/push
{ "entryId": 42, "connectionId": "uuid" }
6. Configure Webhook
POST /webhooks/accounting/{connectionId}
Custom Provider Implementation
To add a new accounting provider, implement the AccountingProvider interface:
interface AccountingProvider {
readonly providerType: string;
connect(config): Promise<void>;
disconnect(): Promise<void>;
isConnected(): boolean;
pushEntry(context: PushEntryContext): Promise<ExternalReference>;
updateEntry(externalId, externalType, context): Promise<ExternalReference>;
voidEntry(externalId, externalType): Promise<void>;
pullEntries(since: Date): Promise<ExternalEntryData[]>;
pullChartOfAccounts(): Promise<ExternalAccount[]>;
pullParties(): Promise<ExternalParty[]>;
pullTaxCodes(): Promise<ExternalTaxCode[]>;
handleWebhook(payload, headers): Promise<WebhookResult>;
refreshCredentials(current): Promise<Record<string, unknown>>;
}
Register the provider in AccountingModule.onModuleInit().
Related Pages
- Accounting Review — review workflow before sync
- Accountant Workflow — step-by-step accountant guide
- Accounting API — REST endpoints for connections, sync, and reviews