Skip to content
Pro · Agency planRequires 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:

ColumnNotes
provider, local_entity_type, local_entity_id, directionComposite unique key — one row per local entity per provider per direction. Re-queuing is idempotent.
remote_entity_idThe provider's ID after a successful sync.
statuspending / running / success / failed.
attempts, next_attempt_atExponential backoff: 0 → 5 min → 30 min → 2 hr → 12 hr → permanently failed (max 6 attempts).
error_messageTruncated 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 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 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 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:

AttemptWait
1immediate
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

HookWhen
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_providersFilter to register additional providers

See also

ProAccounting Sync is part of the Agency tier. Upgrade to Agency →