--- title: Accounting Sync — QuickBooks / Xero / FreshBooks (Pro addon) description: Two-way sync between Easy Invoice and your accounting platform. Invoices push out when created; payment status pulls back when reconciled there. ---
Pro · Agency plan Requires Easy Invoice Pro with an Agency license. Compare plans →
# Accounting Sync Stop re-entering invoices in your accounting tool. The Accounting Sync addon pushes every Easy Invoice document to **QuickBooks Online**, **Xero**, or **FreshBooks** automatically, and pulls payment status back when the invoice is reconciled there. ## When you need this - You (or your accountant) run your books in **QuickBooks Online**, **Xero**, or **FreshBooks** and double-entry is killing you - You're an **agency / bookkeeper** managing multiple clients and need each client's Easy Invoice to flow into their own accounting system - You want **payment status reconciliation** between WordPress and your accounting tool without manual matching - You issue invoices in **multiple currencies** and need the FX-correct line items to land in your books ## Architecture at a glance ``` ┌──────────────────────┐ │ AccountingSyncAddon │ bootstrap, settings page, sync log, └──────────┬───────────┘ queue dispatcher, manual-sync UI │ ▼ ┌──────────────────────┐ │ ProviderRegistry │ pick the right provider for each row └──────────┬───────────┘ │ ┌─────────┼─────────┬─────────────┐ ▼ ▼ ▼ ▼ QuickBooks Xero FreshBooks (your provider here) Provider Provider Provider — each extends AbstractProvider — implements pushInvoice / pushPayment / handleInboundEvent ``` Every provider implements the same five operations (`authorizationUrl`, `exchangeCodeForTokens`, `refreshTokensIfNeeded`, `pushInvoice`, `pushPayment`) so the queue dispatcher and admin UI stay provider-agnostic. ## Enabling 1. Open **Easy Invoice → Addons** 2. Find **Accounting Sync (QuickBooks / Xero / FreshBooks)** 3. Click **Activate** The addon adds two pages to the in-app sidebar: - **Accounting Sync** — pick provider, paste OAuth credentials, connect - **Accounting Sync → Sync Log** — every sync attempt with status, retry button, error messages A custom table `{prefix}_easy_invoice_accounting_sync` is created on first activation: | Column | Notes | |---|---| | `provider`, `local_entity_type`, `local_entity_id`, `direction` | Composite unique key — one row per local entity per provider per direction. Re-queuing is idempotent. | | `remote_entity_id` | The provider's ID after a successful sync. | | `status` | pending / running / success / failed. | | `attempts`, `next_attempt_at` | Exponential backoff: 0 → 5 min → 30 min → 2 hr → 12 hr → permanently failed (max 6 attempts). | | `error_message` | Truncated to 1000 chars; shown in the log. | ## Setup — register your own OAuth app Easy Invoice **never ships shared OAuth credentials** — that's the standard model for self-hosted WordPress plugins (same as every WooCommerce QuickBooks integration). You register a developer app once, paste in the credentials, and you own the connection. ### QuickBooks Online 1. Sign in at [Intuit Developer](https://developer.intuit.com/) and create a new app under **Apps → Create an app**. 2. Pick the **Accounting** scope. 3. Under **Keys & Credentials → Production**, copy the **Client ID** and **Client Secret**. 4. Under **Redirect URIs**, add: `{your-site}/wp-admin/admin-post.php?action=easy_invoice_acct_oauth_callback&provider=quickbooks` 5. Under **Webhooks**, paste the URL shown on the addon settings page, then copy the **Verifier Token** into the option `easy_invoice_acct_qbo_webhook_verifier` (Tools → Site Health → Info or via WP-CLI). ### Xero 1. Sign in at [Xero Developer](https://developer.xero.com/) and **Create a new app**. 2. Pick the **Web app** type. 3. Under **OAuth 2.0 Credentials**, copy the **Client ID** and **Client Secret**. 4. Add the redirect URI: `{your-site}/wp-admin/admin-post.php?action=easy_invoice_acct_oauth_callback&provider=xero` 5. Under **Webhooks**, paste the URL shown on the addon settings page, then copy the **Webhook signing key** into the option `easy_invoice_acct_xero_webhook_key`. ### FreshBooks 1. Sign in at [FreshBooks Developer](https://www.freshbooks.com/developers) and **Create new app**. 2. Pick scopes: `user:invoices:read`, `user:invoices:write`, `user:clients:write`, `user:payments:write`. 3. Copy the **Client ID** and **Client Secret**. 4. Add the redirect URI: `{your-site}/wp-admin/admin-post.php?action=easy_invoice_acct_oauth_callback&provider=freshbooks` 5. (Optional) Set the webhook secret into `easy_invoice_acct_freshbooks_webhook_secret` for inbound event verification. ## Connecting 1. On the **Accounting Sync** settings page, pick your provider radio. 2. Paste the **Client ID** and **Client Secret** from your provider's developer console. 3. Click **Save settings**. 4. Click **Connect to {Provider} →** — you'll be redirected to the provider's consent screen. 5. After granting access, you're redirected back to the settings page. The connection block shows the realm/tenant/account ID and token expiry. Tokens are stored **encrypted at rest** in the WP options table, using AES-256-CBC + HMAC with a key derived from `wp_salt('secure_auth')`. Reading the DB without `wp-config.php` is not enough to recover them. ## How sync works ### Auto-push (default ON) When Easy Invoice creates or updates an invoice (or records a payment), the addon listens on the canonical events: - `easy_invoice_invoice_created` `(invoice_id)` - `easy_invoice_invoice_updated` `(invoice_id)` - `easy_invoice_payment_recorded` `(payment_id, invoice_id)` …and enqueues a sync row. The queue dispatcher runs every 5 minutes via cron, claims pending rows in batches of 50, asks the right provider to push them, and persists success/failure. ### Manual push The invoice editor has a **Sync to {Provider}** button (when an active provider is connected). Click it to trigger an immediate push via the AJAX endpoint `easy_invoice_acct_sync_one`. The button reports the remote ID on success or surfaces the API error inline on failure — no queue wait. ### Inbound webhooks (payment-status reconciliation) Each provider hits a per-provider REST endpoint: ``` {your-site}/wp-json/easy-invoice/v1/accounting-sync/{provider}/webhook ``` The handler: 1. Verifies the provider's signature (HMAC-SHA256 with the verifier / signing key you configured). 2. Decodes the event. 3. Fires `do_action('easy_invoice_acct_{provider}_payment_event', $remote_id, $event_type, $payload)` so any extension (including the addon's own payment reconciler) can react. If verification fails, the endpoint returns 401 immediately — no further processing happens. ## Retry & backoff Failed sync rows are not silent. The dispatcher uses exponential backoff: | Attempt | Wait | |---|---| | 1 | immediate | | 2 | +5 min | | 3 | +30 min | | 4 | +2 hr | | 5 | +12 hr | | 6+ | permanently failed (surfaced in the Sync Log) | Two buttons on the log page: - **Run queue now** — fire the dispatcher immediately - **Retry all failed** — reset every `failed` row to `pending` for one more attempt ## Customizing the payload Each provider exposes filters so you can override the outgoing payload. Examples: ```php // QuickBooks: map Easy Invoice client_id to your QBO customer ID. add_filter('easy_invoice_acct_qbo_customer_ref', function ($customer_ref, $client_id, $invoice_id) { return get_user_meta($client_id, '_qbo_customer_id', true); }, 10, 3); // Xero: pick a different income account code per invoice. add_filter('easy_invoice_acct_xero_default_account_code', function ($code) { return '4100'; // Consulting income }); // FreshBooks: change the full payload right before send. add_filter('easy_invoice_acct_freshbooks_invoice_payload', function ($payload, $invoice_id) { $payload['notes'] = 'Synced from Easy Invoice ' . get_bloginfo('url'); return $payload; }, 10, 2); ``` ## Adding your own provider The provider layer is registry-driven — register your own to support Sage, Wave, KashFlow, or any custom API: ```php namespace MyPlugin; use EasyInvoicePro\Addons\AccountingSync\Providers\AbstractProvider; class SageProvider extends AbstractProvider { public function slug(): string { return 'sage'; } public function label(): string { return 'Sage Accounting'; } public function authorizationUrl(string $callback_url): string { /* … */ } public function exchangeCodeForTokens(array $callback_args): void { /* … */ } public function refreshTokensIfNeeded(): void { /* … */ } public function verifyWebhookSignature(\WP_REST_Request $r): bool { /* … */ } public function handleInboundEvent(\WP_REST_Request $r): void { /* … */ } public function pushInvoice(int $invoice_id): string { /* … */ } public function pushPayment(int $payment_id): string { /* … */ } } add_filter('easy_invoice_accounting_sync_providers', function ($p) { $p['sage'] = new \MyPlugin\SageProvider(); return $p; }); ``` The new provider automatically appears in the settings UI, the queue dispatcher routes to it, and the webhook URL `…/accounting-sync/sage/webhook` is live. ## Hooks reference | Hook | When | |---|---| | `easy_invoice_acct_sync_success` `(row, remote_id)` | A sync row completed successfully | | `easy_invoice_acct_sync_failure` `(row, exception)` | A sync attempt threw; row will retry per backoff | | `easy_invoice_acct_qbo_invoice_payload` `(payload, invoice_id)` | Filter the outgoing QBO Invoice payload | | `easy_invoice_acct_qbo_payment_payload` `(payload, payment_id)` | Filter the outgoing QBO Payment payload | | `easy_invoice_acct_qbo_customer_ref` `(value, client_id, invoice_id)` | Override the QBO Customer ref | | `easy_invoice_acct_qbo_default_item_ref` `(value)` | Override the QBO line-item Item ref | | `easy_invoice_acct_xero_invoice_payload` `(payload, invoice_id)` | Filter the outgoing Xero Invoice payload | | `easy_invoice_acct_xero_payment_payload` `(payload, payment_id)` | Filter the outgoing Xero Payment payload | | `easy_invoice_acct_xero_contact_id` `(value, client_id, invoice_id)` | Override the Xero ContactID | | `easy_invoice_acct_xero_default_account_code` `(value)` | Override the Xero income-account code | | `easy_invoice_acct_freshbooks_invoice_payload` `(payload, invoice_id)` | Filter the outgoing FreshBooks invoice payload | | `easy_invoice_acct_freshbooks_payment_payload` `(payload, payment_id)` | Filter the outgoing FreshBooks payment payload | | `easy_invoice_acct_qbo_payment_event` `(remote_id, operation, entity)` | Inbound QBO webhook for a Payment entity | | `easy_invoice_acct_xero_invoice_event` `(remote_id, type, event)` | Inbound Xero webhook for an INVOICE event | | `easy_invoice_acct_freshbooks_event` `(name, object_id, payload)` | Inbound FreshBooks webhook (any event) | | `easy_invoice_accounting_sync_providers` | Filter to register additional providers | ## See also - [Webhooks & Zapier Bridge](./webhooks) — for non-accounting destinations - [Team Members & Audit Log](./team-roles) — every sync attempt is auditable
Pro Accounting Sync is part of the Agency tier. Upgrade to Agency →