xero-ruby
Xero Ruby SDK for OAuth 2.0 generated from Xero API OpenAPI Spec.
Documentation
Xero Ruby SDK supports Xero's OAuth2.0 authentication and the following Xero API sets.
API Client Documentation
- API client methods > 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) ### Model Docs
- Models > Directory of markdown files, describing the object models for the Accounting, Asset, Projects, Files, Payroll (AU, UK, NZ) Xero API sets. ## Sample Apps 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)
Xero Pre-Requisites
- 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"
" ## Installation To install this gem to your project:gem install 'xero-ruby'
Or more commonly in Ruby on Rails usage add to your gemfile and runbundle install
:gem 'xero-ruby'
Usage
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)
OAuth2.0 Authorization & Callback
All API requests require a valid access token to be set on the xero_client.
Step 1
Send the user to the authorization_url
after you have configured your xero_client
@authorization_url = xero_client.
redirect_to @authorization_url
Step 2
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
Step 3
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
What is a Token Set?
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:
Making API calls with a valid token_set
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.
Token Helpers
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": "[email protected]",
"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)
Connection Helpers
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
API Usage
Accounting API
https://xeroapi.github.io/xero-ruby/accounting/index.html
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 => 'ACCREC', line_items: [...]
puts invoices.to_hash(downcase: false) => '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)
### Assets API
> https://github.com/XeroAPI/xero-ruby/blob/master/accounting/lib/xero-ruby/api/asset_api.rb
```ruby
asset = {
"assetName": "AssetName: #{rand(10000)}",
"assetNumber": "Asset: #{rand(10000)}",
"assetStatus": "DRAFT"
}
asset = xero_client.asset_api.create_asset(tenant_id, asset)
Project API
https://github.com/XeroAPI/xero-ruby/blob/master/docs/projects/ProjectApi.md
projects = xero_client.project_api.get_projects(tenant_id).items
Files API
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
### Payroll API(s)
```ruby
# 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
BigDecimal
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 => "$")
Querying & Filtering
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
1) 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.
2) Some opts string values may need PascalCasing to match casing defined in our core API docs.
opts = { order: 'UpdatedDateUtc DESC'}
3) If you have use cases outside of these examples let us know.
Developing locally
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
Testing
rspec spec/
Participating in Xero’s developer community
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.