Low commit activity in last 3 years
No release in over a year
Ruby JSON-only client for QuickBooks Online API v3. Built on top of the Faraday gem.



 Project Readme


Ruby client for the QuickBooks Online API version 3.

  • JSON only support.
    • Please don't ask about XML support. Intuit has stated that JSON is the primary data format for the QuickBooks API (v3 and beyond). This gem will specialize in JSON only. The quickbooks-ruby gem has fantastic support for those who favor XML.
  • Features specs built directly against a QuickBooks Online Sandbox via the VCR gem.
  • Robust error handling.

The Book

The QBO book

Ruby >= 2.6 required


Add this line to your application's Gemfile:

gem 'qbo_api'

And then execute:

$ bundle

Or install it yourself as:

$ gem install qbo_api



  qbo_api = 'REWR342532asdfae!$4asdfa', realm_id: 32095430444)
- qbo_api.get :customer, 1

Super fast way to use QboApi as long as Ruby >= 2.5 is installed

- cd ~/<local dir>
- git clone && cd qbo_api
- bundle
- bin/console
- QboApi.production = true
- qbo_api = "qyprd2uvCOdRq8xzoSSiiiiii", realm_id: "12314xxxxxx7")
- qbo_api.get :customer, 1

DateTime serialization

Some QBO entities have attributes of type DateTime (e.g., Time Activities with StartTime and EndTime). All DateTimes passed to the QBO API must be serialized in ISO 8601 format. If ActiveSupport is loaded, you can achieve proper serialization with the following configuration:

ActiveSupport::JSON::Encoding.use_standard_json_time_format = true
ActiveSupport::JSON::Encoding.time_precision = 0

If you're not using ActiveSupport, you'll need to use #iso8601 method to convert your Time/DateTime instances to strings before passing them to a QboApi instance. Failure to do so will result in a raised QboApi::BadRequest exception.

Configuration options

  • By default this client runs against a QBO sandbox. To run against the production QBO API URL do:
QboApi.production = true
  • Logging:
QboApi.log = true
  • To change logging target from $stdout e.g.
QboApi.logger = Rails.logger
QboApi.request_id = true
  • To run individual requests with a RequestId then do something like this:
  resp = qbo_api.create(:bill, payload: bill_hash, params: { requestid: qbo_api.uuid })
  # Works with .get, .create, .update, .query methods
  • By default, this client runs against the current "base" or major version of the QBO API. This client does not run against the latest QBO API minor version by default. To run all requests with a specific minor version, you must specify it:
QboApi.minor_version = 8
  • To run individual requests with a minor version then do something like this:
  resp = qbo_api.get(:item, 8, params: { minorversion: 8 })
  # Works with .get, .create, .update, .query methods


  invoice = {
    "Line": [
        "Amount": 100.00,
        "DetailType": "SalesItemLineDetail",
        "SalesItemLineDetail": {
          "ItemRef": {
            "value": "1",
            "name": "Services"
    "CustomerRef": {
      "value": "1"
  response = qbo_api.create(:invoice, payload: invoice)
  p response['Id'] # => 65


  customer = {
    DisplayName: 'Jack Doe',
    PrimaryPhone: {
      FreeFormNumber: "(415) 444-1234"
  response = qbo_api.update(:customer, id: 60, payload: customer)
  p response.fetch('PrimaryPhone').fetch('FreeFormNumber') # => "(415) 444-1234"

Delete (only works for transaction entities)

  response = qbo_api.delete(:invoice, id: 145)
  p response['status'] # => "Deleted"

NOTE: If you are deleting a journal entry you have to use the following syntax with the underscore, even though this is inconsistent with how you create journal entries.

  response = qbo_api.delete(:journal_entry, id: 145)
  p response['status'] # => "Deleted"

Deactivate (only works for name list entities)

  response = qbo_api.deactivate(:employee, id: 55)
  p response['Active'] # => false

Get an entity by its id

  response = qbo_api.get(:customer, 5)
  p response['DisplayName'] # => "Dukes Basketball Camp"

Get an entity by one of its filter attributes

  response = qbo_api.get(:customer, ["DisplayName", "Dukes Basketball Camp"])
  p response['Id'] # => 5

Get an entity by one of its filter attributes using a LIKE search

  response = qbo_api.get(:customer, ["DisplayName", "LIKE", "Dukes%"])
  p response['Id'] # => 5

Get an entity by one of its filter attributes using a IN search

  response = qbo_api.get(:vendor, ["DisplayName", "IN", "(true, false)"])
  p response.size # => 28

Import/retrieve all

Note: There is some overlap with the all and the get methods. The get method is limited to 1000 results where the all method will return all the results no matter the number.

  # retrieves all active customers
  qbo_api.all(:customers).each do |c|
    p "#{c['Id']} #{c['DisplayName']}"

  # retrieves all active or inactive employees
  qbo_api.all(:employees, inactive: true).each do |e|
    p "#{e['Id']} #{e['DisplayName']}"

  # retrieves all vendors by groups of 5
  qbo_api.all(:vendor, max: 5).each do |v|
    p v['DisplayName']

  # retrieves all customers by groups of 2 using a custom select query
  where = "WHERE Id IN ('5', '6', '7', '8', '9', '10')"
  qbo_api.all(:customer, max: 2, select: "SELECT * FROM Customer #{where}").each do |c|
    p c['DisplayName']

Note: .all() returns a Ruby Enumerator

api.all(:clients).take(50).each { |c| p c["Id"] }

Search with irregular characters

  # Use the .esc() method
  name = qbo_api.esc "Amy's Bird Sanctuary"
  response = qbo_api.query(%{SELECT * FROM Customer WHERE DisplayName = '#{name}'})
  # OR USE .get() method, which will automatically escape
  response = qbo_api.get(:customer, ["DisplayName", "Amy's Bird Sanctuary"])
  p response['Id'] # => 1

Email a transaction entity

api.send_invoice(invoice_id: 1, email_address: '')

Uploading an attachment

  payload = {"AttachableRef":
                    "type": "Invoice",
                    "value": "111"
             "FileName": "test.txt",
             "ContentType": "text/plain"
  # `attachment` can be either an IO stream or string path to a local file
  response = qbo_api.upload_attachment(payload: payload, attachment: '/tmp/test.txt')
  p response['Id'] # => 5000000000000091308

Be aware that any errors will not raise a QboApi::Error, but will be returned in the following format:

         [{"Message"=>"Object Not Found",
            "Object Not Found : Something you're trying to use has been made inactive. Check the fields with accounts, customers, items, vendors or employees.",

Change data capture (CDC) query

  response = qbo_api.cdc(entities: 'estimate', changed_since: '2011-10-10T09:00:00-07:00')
  # You can also send in a Time object e.g. changed_since:
  expect(response['CDCResponse'].size).to eq 1
  ids = response['CDCResponse'][0]['QueryResponse'][0]['Estimate'].collect{ |e| e['Id'] }
  p ids

Batch operations (limit 30 operations in 1 batch request)

  payload = {
          "bId": "bid1",
          "operation": "create",
          "Vendor": {
            "DisplayName": "Smith Family Store"
        }, {
          "bId": "bid2",
          "operation": "delete",
          "Invoice": {
            "Id": "129",
            "SyncToken": "0"
  response = qbo_api.batch(payload)
  expect(response['BatchItemResponse'].size).to eq 2
  expect(batch_response.detect{ |b| b["bId"] == "bid1" }["Vendor"]["DisplayName"]).to eq "Smith Family Store"


        params = { start_date: '2015-01-01', end_date: '2015-07-31', customer: 1, summarize_column_by: 'Customers' }
        response = qbo_api.reports(name: 'ProfitAndLoss', params: params)
        p response["Header"]["ReportName"]) #=> 'ProfitAndLoss'


See docs

        response = qbo_api.reconnect
        #=> if response['ErrorCode'] == 0
        #=>   p response['OAuthToken'] #=> rewq23423424afadsdfs==
        #=>   p response['OAuthTokenSecret'] #=> ertwwetu12345312005343453yy=Fg


See docs

        response = qbo_api.disconnect
        #=> if response['ErrorCode'] == 0
        #=>   # Successful disconnect

Respond to an error

  customer = { DisplayName: 'Weiskopf Consulting' }
    response = qbo_api.create(:customer, payload: customer)
  rescue QboApi::BadRequest => e
    if e.message =~ /Another customer already exists with this name/
      # Query for Id using DisplayName
      # Do an qbo_api.update instead

What kind of QuickBooks entity?

  p qbo_api.is_transaction_entity?(:invoice) # => true
  # Plural is supported as well
  p qbo_api.is_transaction_entity?(:invoices) # => true
  p qbo_api.is_transaction_entity?(:customer) # => false
  p qbo_api.is_name_list_entity?(:vendors) # => true

Spin up an example

  • Check out this article on spinning up the example.

  • Create a Intuit developer account at

  • Create an app in the intuit developer backend

    • Go to myapps
    • Create an app with both the Accounting & Payments selected.
  • Setup the app and gather credentials

    • Go to the Development Dashboard
    • Click on your App name
    • Click "Keys & credentials" under Development Settings in left menu
    • Copy the 'Client ID' and the 'Client Secret'
    • Add a Redirect URI: http://localhost:9393/oauth2-redirect (or whatever PORT= is in your .env)
    • Create a new Sandbox Company by clicking on "View Sandbox companies" Don't use it for anything else besides testing this app.
    • Copy the 'Company ID' for use later
      1. Within 'Sandbox Company_US_1' click on the COG -> Additional info
      2. Copy the 'Company ID'
  • Setup qbo_api

    • git clone git:// && cd qbo_api
    • bundle
  • Create a .env file

    • cp .env.example_app.oauth2 .env
    • Edit your .env and enter the following
  • Start up the example app and test

    • ruby example/oauth2.rb
    • Go to http://localhost:9393/oauth2
    • Use the Connect to QuickBooks button to connect to your QuickBooks sandbox, which you receive when signing up at
    • After successfully connecting to your sandbox go to http://localhost:9393/oauth2/customer/5
    • You should see "Dukes Basketball Camp" displayed
  • Checkout example/oauth2.rb to see what is going on under the hood.


See for how to install ngrok and what it is.

  • With the example app running, run: ngrok http 9393 -subdomain=somereasonablyuniquenamehere

  • Go to the Development tab

  • Add a webhook, Select all triggers and enter the https url from the ngrok output https://somereasonablyuniquenamehere/webhooks

  • After saving the webhook, click 'show token'. Add the token to your .env as QBO_API_VERIFIER_TOKEN

  • In another tab, create a customer via the API: bundle exec ruby -rqbo_api -rdotenv -e 'Dotenv.load; p ENV.fetch("QBO_API_ACCESS_TOKEN"), realm_id: ENV.fetch("QBO_API_COMPANY_ID")).create(:customer, payload: { DisplayName: "TestCustomer" })' (You'll also need to have added the QBO_API_COMPANY_ID and QBO_API_ACCESS_TOKEN to your .env)

    There could be a delay of up to a minute before the webhook fires.

    It'll appear in your logs like:

    {"eventNotifications"=>[{"realmId"=>"XXXX", "dataChangeEvent"=>{"entities"=>[{"name"=>"Customer", "id"=>"62", "operation"=>"Create", "lastUpdated"=>"2018-04-08T04:14:39.000Z"}]}}]}
    Verified: true
    "POST /webhooks HTTP/1.1" 200 - 0.0013

Just For Hackers

  • Using the build_connection method
connection = build_connection('', headers: { 'Accept' => 'application/json' }) do |conn|
  conn.basic_auth(client_id, client_secret)
  conn.request :url_encoded # application/x-www-form-urlencoded
  conn.response :json
  conn.use QboApi::RaiseHttpException

raw_response = do |req|
  req.body = { grant_type: :refresh_token, refresh_token: current_refresh_token }
  req.url '/oauth2/v1/tokens/bearer'
  • Once your .env file is completely filled out you can use the console to play around in your sandbox
bin/console test
>> @qbo_api.get :customer, 1


Bug reports and pull requests are welcome on GitHub at

Running the specs

Creating new specs or modifying existing spec that have been recorded using the VCR gem.


The gem is available as open source under the terms of the MIT License.