ExQuickbooks

ExQuickbooks is an Elixir client for the QuickBooks Online Accounting API. It keeps the public API library-oriented and explicit: callers build a client, run OAuth flows, use resource helpers, and handle expected failures through {:ok, result} / {:error, %ExQuickbooks.Error{}} tuples.

Installation

Add ex_quickbooks to your dependencies:

def deps do
  [
    {:ex_quickbooks, "~> 0.8.0"}
  ]
end

What the library covers

Sandbox setup

  1. Create an app in the Intuit Developer portal.
  2. In Keys & OAuth, copy your Client ID and Client Secret.
  3. Add a local redirect URI such as http://localhost:4000/auth/quickbooks/callback.
  4. Open the sandbox company provided in your developer dashboard and note its realmId.
  5. Use the com.intuit.quickbooks.accounting scope during OAuth.

Useful references:

OAuth usage

Generate the authorization URL:

{:ok, authorization_url} =
  ExQuickbooks.Auth.authorization_url(
    client_id: "client-id",
    redirect_uri: "http://localhost:4000/auth/quickbooks/callback",
    state: "csrf-token"
  )

Exchange the callback code for tokens:

{:ok, token} =
  ExQuickbooks.Auth.exchange_code(
    client_id: "client-id",
    client_secret: "client-secret",
    redirect_uri: "http://localhost:4000/auth/quickbooks/callback",
    code: "authorization-code",
    realm_id: "9130357992221046"
  )

Refresh tokens with the latest refresh token returned by Intuit:

{:ok, refreshed_token} =
  ExQuickbooks.Auth.refresh_tokens(
    client_id: "client-id",
    client_secret: "client-secret",
    refresh_token: token.refresh_token,
    realm_id: token.realm_id
  )

Basic client creation

Construct a client once you have tokens:

{:ok, client} =
  ExQuickbooks.new(
    client_id: "client-id",
    client_secret: "client-secret",
    redirect_uri: "http://localhost:4000/auth/quickbooks/callback",
    realm_id: token.realm_id,
    access_token: token.access_token,
    refresh_token: token.refresh_token,
    environment: :sandbox,
    minor_version: 75
  )

You can use ExQuickbooks.request_path/3 to inspect the company-scoped request path that the shared HTTP pipeline will use:

ExQuickbooks.request_path(client, ["customer"], query: [active: true])
#=> "/v3/company/9130357992221046/customer?active=true&minorversion=75"

Company bootstrap and query helpers

Confirm the client can reach the target company:

{:ok, company_info} = ExQuickbooks.CompanyInfo.get(client)

Run raw QuickBooks queries with optional pagination:

{:ok, query_response} =
  ExQuickbooks.Query.run(
    client,
    "SELECT * FROM Customer",
    start_position: 1,
    max_results: 25
  )

{:ok, {"Customer", customers}} =
  ExQuickbooks.Query.top_level_collection(query_response)

Customer and invoice flows

Fetch active customers:

{:ok, customers} =
  ExQuickbooks.Customers.list(
    client,
    where: "Active = true",
    max_results: 25
  )

Create and update a customer:

{:ok, created_customer} =
  ExQuickbooks.Customers.create(client, %{
    "DisplayName" => "Acme"
  })

{:ok, updated_customer} =
  ExQuickbooks.Customers.update(client, %{
    "Id" => created_customer["Id"],
    "SyncToken" => created_customer["SyncToken"],
    "DisplayName" => "Acme Updated"
  })

Create and update an invoice:

{:ok, created_invoice} =
  ExQuickbooks.Invoices.create(client, %{
    "CustomerRef" => %{"value" => created_customer["Id"]},
    "Line" => [
      %{
        "Amount" => 100,
        "DetailType" => "SalesItemLineDetail"
      }
    ]
  })

{:ok, updated_invoice} =
  ExQuickbooks.Invoices.update(client, %{
    "Id" => created_invoice["Id"],
    "SyncToken" => created_invoice["SyncToken"],
    "PrivateNote" => "Updated through ExQuickbooks"
  })

The same list/2, get/3, create/3, and update/3 pattern is available for:

CDC sync helpers

Fetch grouped changes since a checkpoint:

{:ok, customer_changes} =
  ExQuickbooks.CDC.fetch(
    client,
    [:customer],
    "2026-04-20T00:00:00Z"
  )

{:ok, item_changes} =
  ExQuickbooks.CDC.fetch(
    client,
    [:item],
    "2026-04-20T00:00:00Z"
  )

{:ok, invoice_and_payment_changes} =
  ExQuickbooks.CDC.fetch(
    client,
    [:invoice, :payment],
    "2026-04-20T00:00:00Z"
  )

Each entity key maps to grouped records and deleted IDs:

%{
  "Customer" => %{
    records: [%{"Id" => "123"}],
    deleted_ids: [%{"Type" => "Customer", "Id" => "456"}]
  }
}

Error handling

The library returns typed ExQuickbooks.Error values for expected failures:

case ExQuickbooks.Customers.get(client, "123") do
  {:ok, customer} ->
    {:ok, customer}

  {:error, %ExQuickbooks.Error{type: :not_found}} ->
    {:error, :missing_customer}

  {:error, %ExQuickbooks.Error{type: :rate_limited, details: %{"retry_after" => retry_after}}} ->
    {:error, {:retry_later, retry_after}}

  {:error, %ExQuickbooks.Error{type: :unauthorized}} ->
    {:error, :refresh_required}

  {:error, %ExQuickbooks.Error{} = error} ->
    {:error, error}
end

Versioning strategy

ExQuickbooks is still pre-1.0, so versioning is conservative: