Xero Ruby SDK for OAuth 2.0 generated from Xero API OpenAPI Spec.
Xero Ruby SDK supports Xero's OAuth2.0 authentication and the following Xero API sets.
This describes to ~200+ accounting API endpoints and their expected params. There are also method reference docs for the Assets, Files, Projects, and Payroll endpoints sets, though we are still working on accurately generating usable parameter examples for all! (feedback welcome)
Directory of markdown files, describing the object models for the Accounting, Asset, Projects, Files, Payroll (AU, UK, NZ) Xero API sets.
We have two sample apps showing SDK usage:
- https://github.com/XeroAPI/xero-ruby-oauth2-starter (Sinatra - bare minimum to hello world and simple session based storage)
- https://github.com/XeroAPI/xero-ruby-oauth2-app (Rails - token management with robust usage examples)
- Create a free Xero user account
- Login to your Xero developer /myapps dashboard & create an API application
- Copy the credentials from your API app and store/access them using a secure ENV variable strategy
- Resaearch and include the neccesary scopes for your app's functionality as a space-seperated list, ex. "
SCOPES="openid profile email accounting.transactions accounting.settings"
"
To install this gem to your project:
gem install 'xero-ruby'
Or more commonly in Ruby on Rails usage add to your gemfile and run bundle install
:
gem 'xero-ruby'
require 'xero-ruby'
creds = {
client_id: ENV['CLIENT_ID'],
client_secret: ENV['CLIENT_SECRET'],
redirect_uri: ENV['REDIRECT_URI'],
scopes: ENV['SCOPES'],
state: "this-can-be-a-custom-state-parameter" # optional
}
xero_client ||= XeroRuby::ApiClient.new(credentials: creds)
For additional logging or timeout, add or override any config option by passing an optional named parameter config: {..}
.
config = { timeout: 30, debugging: true }
@xero_client ||= XeroRuby::ApiClient.new(credentials: creds, config: config)
All API requests require a valid access token to be set on the xero_client.
Send the user to the authorization_url
after you have configured your xero_client
@authorization_url = xero_client.authorization_url
redirect_to @authorization_url
On successful authorization, Xero identity will redirect to the URI defined in your redirect_uri
config which must match exactly with the variable in your /myapps dashboard.
=> /oauth/redirect_uri
In your server defined callback route, exchange the temporary code for a valid token_set
that will get set on your client.
token_set = xero_client.get_token_set_from_callback(params)
At this point you should save the token_set as JSON in a datastore in relation to the authenticating user or entity.
The sample Rails app shows a solid pattern you can tweak to fit your needs:
# /oauth/redirect_uri -> 'application#callback'
def callback
@token_set = @xero_client.get_token_set_from_callback(params)
current_user.token_set = @token_set
current_user.token_set['connections'] = @xero_client.connections
current_user.active_tenant_id = latest_connection(current_user.token_set['connections'])
current_user.save!
flash.notice = "Successfully authenticated with Xero!"
end
A token_set
is what we call the XeroAPI response that contains data about your API connection:
{
"id_token": "xxx.yyy.zz", (if you requested `openid profile email` scope)
"access_token": "xxx.yyy.zzz",
"expires_in": 1800,
"token_type": "Bearer",
"refresh_token": "xxxxxx", (if you requested `offline_access` scope)
"scope": "email profile openid accounting.transactions offline_access"
}
Note that an access_token
is valid for 30 minutes but a refresh_token
can be used once in up to a 60 day window. If a refresh_token is used to refresh access you must replace the entire token_set.
Both the id_token
& access_token
are JWT's, and can be decoded to see additional metadata described in the Token Helpers section:
After the initial user interaction you can simply setup a xero_client by passing the whole token_set to the client.
xero_client.set_token_set(user.token_set)
xero_client.refresh_token_set(user.token_set)
This sets the access_token on the client, and returns a refreshed token_set
that you should save in your datastore for the next time you need to connect to Xero's API.
xero_client.token_set
=>
{
"id_token": "xxx.yyy.zz",
"access_token": "xxx.yyy.zzz",
"expires_in": 1800,
"token_type": "Bearer",
"refresh_token": "xxxxxx",
"scope": "email profile openid accounting.transactions offline_access"
}
xero_client.access_token
=> "xxx.yyy.zzz"
xero_client.decoded_access_token
=> {
"exp": 1619715843,
"xero_userid": "xero-user-uuid",
"scope": [
"email",
"profile",
"openid",
"accounting.transactions",
"offline_access"
]
}
xero_client.id_token
=> "aaa.bbb.ccc"
xero_client.decoded_id_token
=> {
"iss": "https://identity.xero.com",
"email": "luca.pacioli@accounting-services.com",
"given_name": "Luca",
"family_name": "Pacioli"
}
xero_client.set_token_set(token_set)
=> true
xero_client.get_token_set_from_callback(callback_url_params)
=> new_xero_token_set
xero_client.refresh_token_set(token_set)
=> new_xero_token_set
# These are automatically populated with `set_token_set`
# But if you need to set just an access or id token on the client
xero_client.set_access_token(access_token)
xero_client.set_id_token(id_token)
# Automatically run on initial OAuth flow - can be called its own if desired
# Read about why we have included this in the default library: https://auth0.com/docs/tokens/access-tokens/validate-access-tokens
xero_client.validate_tokens(token_set)
xero_client.decode_jwt(tkn)
xero_client.authorization_url
=> # https://login.xero.com/identity/connect/authorize?response_type=code&client_id=<client_id>&redirect_uri=<redirect_uri>&scope=<scopes>&state=<my-state>
# To completely Revoke a user's access token and all their connections
xero_client.revoke_token(token_set)
# In case there are > 1 tenants connected the `updatedDateUtc` will show you the most recently authorized tenant (aka organisation) - it is important to store the `tenantId` of the Org your user specified in their API authorization
connections = xero_client.connections
[{
"id" => "xxx-yyy-zzz",
"tenantId" => "xxx-yyy-zzz",
"tenantType" => "ORGANISATION",
"tenantName" => "Demo Company (US)",
"createdDateUtc" => "2019-11-01T20:08:03.0766400",
"updatedDateUtc" => "2020-04-15T22:37:10.4943410"
}]
# To disconnect a single org from a user's active connections pass the connection ['id'] (not ['tenantId'])
# If you want to enforce only a single org connection per token do this prior to sending user through Xero authorize flow a 2nd time.
remaining_connections = xero_client.disconnect(connections[0]['id'])
xero_client.token_expired?
=> true || false
# This will check against the following logic
token_expiry = Time.at(decoded_access_token['exp'])
token_expiry < Time.now
require 'xero-ruby'
xero_client.refresh_token_set(user.token_set)
tenant_id = user.active_tenant_id
# Example 'active tenant' logic storage of the tenant the user specified, xero_client.connections[0] is not a safe assumption in case they authorized multiple orgs.
# Get Accounts
accounts = xero_client.accounting_api.get_accounts(tenant_id).accounts
# Create Invoice
invoices = { invoices: [{ type: XeroRuby::Accounting::Invoice::ACCREC, contact: { contact_id: contacts[0].contact_id }, line_items: [{ description: "Big Agency", quantity: BigDecimal("2.0"), unit_amount: BigDecimal("50.99"), account_code: "600", tax_type: XeroRuby::Accounting::TaxType::NONE }], date: "2019-03-11", due_date: "2018-12-10", reference: "Website Design", status: XeroRuby::Accounting::Invoice::DRAFT }]}
invoice = xero_client.accounting_api.create_invoices(tenant_id, invoices).invoices.first
# display out all the serialized data as a snake_case hash
puts invoices.to_attributes
=> {type: 'ACCREC', line_items: [...]}
puts invoices.to_hash(downcase: false)
=> {'Type': 'ACCREC', 'LineItems': [...]}
# Create History
payment = xero_client.accounting_api.get_payments(tenant_id).payments.first
history_records = { history_records: [{ details: "This payment now has some History!" }]}
payment_history = xero_client.accounting_api.create_payment_history(tenant_id, payment.payment_id, history_records)
# Create Attachment
account = xero_client.accounting_api.get_accounts(tenant_id).accounts.first
file_name = "an-account-filename.png"
opts = {
include_online: true
}
file = File.read(Rails.root.join('app/assets/images/xero-api.png'))
attachment = xero_client.accounting_api.create_account_attachment_by_file_name(tenant_id, @account.account_id, file_name, file, opts)
https://github.com/XeroAPI/xero-ruby/blob/master/accounting/lib/xero-ruby/api/asset_api.rb
asset = {
"assetName": "AssetName: #{rand(10000)}",
"assetNumber": "Asset: #{rand(10000)}",
"assetStatus": "DRAFT"
}
asset = xero_client.asset_api.create_asset(tenant_id, asset)
https://github.com/XeroAPI/xero-ruby/blob/master/docs/projects/ProjectApi.md
projects = xero_client.project_api.get_projects(tenant_id).items
https://github.com/XeroAPI/xero-ruby/blob/master/docs/files/FileApi.md
opts = {
pagesize: 50, # Integer | pass an optional page size value
page: 2, # Integer | number of records to skip for pagination
sort: 'CreatedDateUTC DESC' # String | values to sort by
}
files = xero_client.files_api.get_files(tenant_id, opts).files
# https://github.com/XeroAPI/xero-ruby/blob/master/docs/payroll_au/PayrollAuApi.md
employee_id = 'employee_uuid'
employee = xero_client.payroll_au_api.get_employee(tenant_id, employee_id).employee
# https://github.com/XeroAPI/xero-ruby/blob/master/docs/payroll_nz/PayrollNzApi.md
timesheet_id = 'timesheeet_uuid'
timesheet = xero_client.payroll_nz_api.approve_timesheet(tenant_id, timesheet_id).timesheets
# https://github.com/XeroAPI/xero-ruby/blob/master/docs/payroll_uk/PayrollUkApi.md
employee_id = 'employee_uuid'
wages = xero_client.payroll_uk_api.get_employee_salary_and_wages(tenant_id, employee_id, opts).salary_and_wages
All monetary and fields and a couple quantity fields utilize BigDecimal
puts invoice.unit_amount
=> 0.2099e2
puts invoice.unit_amount.class
=> BigDecimal
puts invoice.unit_amount.to_s("F")
=> "20.99"
# Rails method-number_to_currency
number_to_currency(invoice.unit_amount, :unit => "$")
Examples for the opts
(options) parameters most endpoints support. This is an area of focus and improvement. If you have a complex filering/sorting/where usage that is not supported please open an issue.
# Invoices
opts = {
page: 1,
where: {
type: ['=', XeroRuby::Accounting::Invoice::ACCREC],
fully_paid_on_date: (DateTime.now - 6.month)..DateTime.now,
amount_due: ['>=', 0],
reference: ['=', "Website Design"],
invoice_number: ['=', "INV-0001"],
contact_id: ['=', 'contact-uuid-xxxx-xxx-xxxxxxx'],
contact_number: ['=', "the-contact-number"],
# date: (DateTime.now - 2.year)..DateTime.now
# ▲ you can pass a range ▼ or a date & operator
date: ['>=', DateTime.now - 2.year],
status: ['=', XeroRuby::Accounting::Invoice::PAID]
}
}
xero_client.accounting_api.get_invoices(tenant_id, opts).invoices
# Contacts
opts = {
if_modified_since: (DateTime.now - 1.weeks).to_s,
# ▼ ordering by strings needs PascalCase convention
order: 'UpdatedDateUtc DESC',
where: {
is_customer: ['==', true],
is_supplier: ['==', true],
name: ['StartsWith', 'Rick']
}
}
xero_client.accounting_api.get_contacts(tenant_id, opts).contacts
# for more complex where filtering that requires a null check, pass those in as a string
# see https://developer.xero.com/documentation/api/requests-and-responses for more
opts = {
where: {
email_address: '!=null&&EmailAddress.StartsWith("chris.knight@")'
}
}
# Bank Transactions
opts = {
if_modified_since: (DateTime.now - 1.year).to_s,
where: { type: ['==', XeroRuby::Accounting::BankTransaction::SPEND] },
order: 'UpdatedDateUtc DESC',
page: 2,
unitdp: 4 # (Unit Decimal Places)
}
xero_client.accounting_api.get_bank_transactions(tenant_id, opts).bank_transactions
# Bank Transfers
opts = {
if_modified_since: (DateTime.now - 1.month).to_s,
where: {
amount: [">=" , 999.99]
},
order: 'Amount ASC'
}
xero_client.accounting_api.get_bank_transfers(tenant_id, opts).bank_transfers
-
Not all
opts
parameter combinations are available for all endpoints, and there are likely some undiscovered edge cases. If you encounter a filter / sort / where clause that seems buggy open an issue and we will dig. -
Some opts string values may need PascalCasing to match casing defined in our core API docs.
opts = { order: 'UpdatedDateUtc DESC'}
- If you have use cases outside of these examples let us know.
To develop this gem locally against your project you can use the following development pattern:
xero-ruby
gem build
mv xero-ruby-<vsn>.gem xero-ruby.gem
pwd
=> /Users/your.user/code/sdks/xero-ruby/
https://github.com/XeroAPI/xero-ruby-oauth2-app Replace gem file with local path:
gem 'xero-ruby', path: '/Users/your.user/code/sdks/xero-ruby/'
bundle install
rspec spec/
This SDK is one of a number of SDK’s that the Xero Developer team builds and maintains. We are grateful for all the contributions that the community makes.
Here are a few things you should be aware of as a contributor:
- Xero has adopted the Contributor Covenant Code of Conduct, we expect all contributors in our community to adhere to it
- If you raise an issue then please make sure to fill out the github issue template, doing so helps us help you
- You’re welcome to raise PRs. As our SDKs are generated we may use your code in the core SDK build instead of merging your code
- We have a contribution guide for you to follow when contributing to this SDK
- Curious about how we generate our SDK’s? Have a read of our process and have a look at our OpenAPISpec
- This software is published under the MIT License
For questions that aren’t related to SDKs please refer to our developer support page.