I'm a huge fan of online ecommerce platforms. One in particular is Spree Commerce. Spree Commerce is an opened-source, API-driven platform, built upon the popular framework, Ruby on Rails. Spree Commerce was built for developers, by developers.

Spree Commerce is not your typical "plugin" where you "click to install" then all thing rosy. Lies! It is but a little developer experience goes a long way. We're not here to learn how all things Spree Commerce but rather know how to mark your items as paid in Xero. So what is Xero? In short, Xero is an online cloud accounting software for small to large businesses, this includes sole-traders. Xero is beautiful.

Your life would now be more productive for both you and your account. As you make a sale, you automatically generate an invoice then mark it as paid. What about refunds? No problem. Next few chapters I'll show you how to achieve this.

Today, we'll discuss when we make a successful order, our app makes an api request to our Xero account which creates a paid invoice for a customer. To achieve this, we'll use the Xeroizer gem.


  • You have knowledge using Ruby and Rails
  • You can do basic navigation using a terminal or Linux
  • You have an account with Xero. If not, create a free account here.

Let's create our Rails app!

# Before we begin, we make sure our system and libs are update

# Mac Users:
gem update --system
xcode-select --install

# Linux users:
sudo apt-get install build-essential patch ruby-dev zlib1g-dev liblzma-dev

# In a directory, install Rails 5.2.
# I have Rails 5.2 installed. Ensure Spree works with your current version
# of Rails.
rails new spree_commerce_with_xero --database=postgresql

We tell our rails gem to create a new application using the PostgreSQL database. You could install rails with rails new <whatever-you-want-to-call-it> and it would use the default SQLite database, if installed. If you the method above, ensure to have a database called spree_commerce_with_xero_development.

Run cd into the newly created directory then rails s, from your terminal, to boot up our server and we should have our app running on port 3000 by default.

rails on localhost:3000

Since we are good developers, we make use of Git! For today, I won't be using as I want to clear up clutter as much as possible.

We now need to generate our public and private keys. Head over here to learn more. If you encounter an issue such as unable to write 'random state', you have a file ownership issue located in your root directory: .rnd. To fix this, run: sudo chown <USER>:<GROUP> ~/.rnd where "USER" is your system's username and "GROUP",... that right 😉

openssl genrsa -out privatekey.pem 1024

# This will ask you some basic information. You decide to fill in or not
openssl req -new -x509 -key privatekey.pem -out publickey.cer -days 1825

Now that we have a copy of Spree running on our local machine and we have an account setup with Xero, head over to their apps section to create a Private application. Before creating an app, ensue to have the "Demo Organization" enabled as we are going to associate our app with the Demo Organization. Now copy both Private and Public keys for safe keeping. We'll come back to those two later.

Installing Spree Commerce 3!

Since I'll be using Rails 5.2, we install both Xeriozer gem and Spree as follows in our Gemfile:

# Gemfile
gem 'spree', '~> 3.6.4'
gem 'spree_auth_devise', '~> 3.3'
gem 'spree_gateway', '~> 3.3'

# Xeriozer gem
gem 'xeroizer'

Are we using Git? Now run bundle install. Everything should ran smooth.

Now to actually install Spree:

rails g spree:install --user_class=Spree::User
rails g spree:auth:install
rails g spree_gateway:install

Restart and we should have our basic Spree store running at http://localhost:3000 🎉

Our function lives inside Spree's CheckoutController.rb on line 27. We need to reuse then update method on our decorator file. In your controllers folder, create a spree folder with a file call checkout_controller_decorator.rb and in that file will have the below. *_decorator.rb is way Spree enables us to override a method or class. Our file before modification:

def update
  if @order.update_from_params(params, permitted_checkout_attributes, request.headers.env)
    @order.temporary_address = !params[:save_user_address]
    unless @order.next
      flash[:error] = @order.errors.full_messages.join("\n")
      redirect_to(checkout_state_path(@order.state)) && return

    if @order.completed?
      @current_order = nil
      flash.notice = Spree.t(:order_processed_successfully)
      flash['order_completed'] = true
      redirect_to completion_route
      redirect_to checkout_state_path(@order.state)
    render :edit

Few thing we will implement inside our update method:

  • Check we already have a Xero customer. If not, create one
  • Create an invoice in Xero and associate that invoice with our customer
  • Mark invoice as paid

Let's get started!

Now, we need a way to associate our Spree customers with our Xero account. How do we go about doing that? We need a customer ID. For this, we will now generate a xero_customer_id on our spree users table:

# In the root of your project, run
rails g migration add_xero_customer_id_to_spree_users

# In generated migration file, update to look as below
class AddXeroCustomerIdToSpreeUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :spree_users, :xero_customer_id, :string

# Now migrate the table from your terminal
rails db:migrate  

Remember our Xero keys: Consumer Secret and Consumer Key? We'll now need them. In your terminal, using vim:

// This is how we add secret keys as of Rails 5.1
EDITOR='vi' bundle exec rails credentials:edit

// Then add your keys:
  consumer_key: 'your_key'
  consumer_secret: 'your_secret'

To check everything was added correctly, we check the following in our rails console by running rails c. We should get back an Hash with our keys:

#=> {:consumer_key=>"your_key", :consumer_secret=>"your_secret"}

# Or
# Rails.application.credentials.xero[:consumer_key]

Happy days! We could use some coffee at this point. I sure do. Go have a drink, walk the dog or do some activities with kids then and head right back after.

Spree::CheckoutController.class_eval do
  # Initializing xero to use @xero variable
  before_action :get_xero

  def update
    if @order.update_from_params(params, permitted_checkout_attributes, request.headers.env)
      @order.temporary_address = !params[:save_user_address]
      unless @order.next
        flash[:error] = @order.errors.full_messages.join("\n")
        redirect_to(checkout_state_path(@order.state)) && return

      if @order.completed?
        if !spree_current_user.xero_customer_id.nil?
          contact_id = spree_current_user.xero_customer_id
          contact_id = create_xero_contact_and_return_contact_id

        invoice = build_xero_invoice(contact_id)

        invoice = add_line_items_to_invoice(invoice)

        # Save the invoice

        # Optional
        # Invoice must be saved, in xero, before you can mark an invoice paid

        # Continue with Spree
        @current_order = nil
        flash.notice = Spree.t(:order_processed_successfully)
        flash['order_completed'] = true
        redirect_to completion_route
        redirect_to checkout_state_path(@order.state)
      render :edit

    # Creates a new contact for xero.
    # @see https://developer.xero.com/documentation/api/contacts#POST
    def create_xero_contact_and_return_contact_id
      user_address = spree_current_user.bill_address
      country_id = user_address.country_id
      contact_full_name = user_address.firstname + " " + user_address.lastname # Must be unique in xero!!!!!

      # Some logic will need to be in place to determin if
      # if you already have a contact by the name of "Daveyon Mayne"
      # in your xero account. If we do not perform this check and a name if found
      # that matches your spree customer, you'll receive an error preventing
      # our invoice from creating
      contact = xero.Contact.all(where: {name: contact_full_name})

      if contact.present?
        # Contact returns an array. We need the first.
        # It will never be a case where you have more than on as xero treats the "name"
        # as unique
        contact_id = contact.first.contact_id
        contact = @xero.Contact.build(
          name:           contact_full_name,
          first_name:     user_address.firstname,
          last_name:      user_address.lastname,
          email_address:  spree_current_user.email,

        # Include a contact address
          type:           "STREET",
          line1:           user_address.address1,
          line2:           user_address.address2,
          city:            user_address.city,
          postal_code:     user_address.zipcode,
          country:         Spree::Country.find(country_id).name,
          # contact_number: user_address.phone # As of Xeroizer version 2.18
          # This will throw an error: undefined method [] for nil:NilClass

        # Attached a contact number
        contact.add_phone(:type => 'DEFAULT', number: user_address.phone)

        # Save contact to your xero account.
        contact_id = contact.contac_id

      # Update our spree_users table with the contact id for
      # future reference
      spree_current_user.update_attributes(xero_customer_id: contact_id)

      # return the contact_id
      return contact_id

    def build_xero_invoice(contact_id)
      invoice =  @xero.Invoice.build(
        type: "ACCREC",
        status: "AUTHORISED",
        currency_code: "GBP", # Change to your conuntry code
        reference: "Order " + @current_order.number,
        contact: { contact_id: contact_id },
        sent_to_contact: false,
        date: DateTime.now,
        due_date: DateTime.now, # Change to your needs

      return invoice

    def add_line_items_to_invoice(invoice)
      @current_order.products.each do |product|
        # Amount is quantity times price. Integer values
        amount = product.line_items.first.quantity * product.line_items.first.price
          description:  product.name,
          quantity:     product.line_items.first.quantity,
          unit_amount:  amount,
          account_code: 200

       # Optional if you want shipping to be included on the invoice statement
         description: "Postage and packaging",
         quantity: 1,
         unit_amount: @current_order.shipment_total,
         account_code: 200

       return invoice

    def mark_invoice_as_paid(invoice)
      invoice_payment = {
        invoice:      invoice,
        account:      { code: '090' },
        date:         invoice.date,
        amount:       invoice.amount_due,
        payment_type: 'ACCRECPAYMENT'
      payment = @xero.Payment.build(invoice_payment)

    # Initializing Xeroizer with our public and private keys
    # found at https://developer.xero.com/myapps
    # @see https://developer.xero.com/documentation/api/api-overview
    def get_xero
      xero_config = Rails.application.credentials.xero
      consumer_key = xero_config[:consumer_key]
      consumer_secret = xero_config[:consumer_secret]

      # privatekey.pem file located at the root of your rails app
      privatekey_path = "#{Rails.root}/privatekey.pem"
      @xero = Xeroizer::PrivateApplication.new(consumer_key, consumer_secret, privatekey_path)

😱 What's going on here? Spree checks for a completed order then checks for a xero contact id, xero_customer_id. At first, that will be nil. If it's nil, we create our contact in xero. If a match is found based on the first and last name (in xero, this will always be unique), it will return the contact object. Nice! We then proceed to build our invoice, add the line items (items that we sold) and a shipping. After saving the invoice, we now mark the invoice as paid.

Paid invoice in our xero account

With this in place, whenever an order is completed, a paid invoice will be generated in our Xero account under Sales > Invoice (paid). Please see their call limit to determine of this implementation is right for your business.

Well done! Both you and your accountant are now happy.

Want to quickly test it out on your local machine? Grab the repo here.

Up next, I'll demonstrate how to do this with Shopify. Remember to subscribe for more articles like this below! For now, ✌🏻