1 | # xero-node
|
2 | ![npm](https://img.shields.io/npm/v/xero-node?label=xero-node)
|
3 |
|
4 | ## OAuth 2 support
|
5 | Version 4.x of Xero NodeJS SDK supports OAuth2 authentication and the following API sets.
|
6 | * [accounting](https://developer.xero.com/documentation/api/api-overview)
|
7 | * [assets](https://developer.xero.com/documentation/assets-api/overview)
|
8 | * [projects](https://developer.xero.com/documentation/projects/overview-projects)
|
9 | * [AU Payroll](https://developer.xero.com/documentation/payroll-api/overview)
|
10 | * [BankFeeds (Restricted API)](https://developer.xero.com/documentation/bank-feeds-api/overview)
|
11 | * [UK Payroll](https://developer.xero.com/documentation/payroll-api-uk/overview)
|
12 | * [NZ Payroll](https://developer.xero.com/documentation/payroll-api-nz/overview)
|
13 | * [files](https://developer.xero.com/documentation/files-api/overview-files)
|
14 |
|
15 |
|
16 | ## Getting Started
|
17 |
|
18 | ### Create a Xero App
|
19 | Follow these steps to create your Xero app
|
20 |
|
21 | * Create a [free Xero user account](https://www.xero.com/us/signup/api/) (if you don't have one)
|
22 | * Login to [Xero developer center](https://developer.xero.com/myapps)
|
23 | * Click "New App" link
|
24 | * Enter the redirect URI (this is your callback url - localhost, etc)
|
25 | * Agree to terms and condition and click "Create App".
|
26 | * Click "Generate a secret" button.
|
27 | * Copy your client id and client secret and save for use later.
|
28 |
|
29 | ## Repo Context & Contributing
|
30 | This SDK's functionality is majority generated [from our OpenAPISpec](https://github.com/XeroAPI/Xero-OpenAPI).
|
31 | The exception is the `src/xeroClient.ts` which contains the typescript that is unique to this repository. Contributions are welcome but please keep in mind that majority of SDK is auto-generated from the OpenAPISpec. We try to get changes in that projects to be released on a reasonable cadence.
|
32 |
|
33 | > Read more about our process in [maintaining our suite of SDK's](https://devblog.xero.com/building-sdks-for-the-future-b79ff726dfd6)
|
34 |
|
35 | ## Testing
|
36 | We are working to build out a more robust test suite, and currently just have tests setup for our xeroClient.ts - PR's will now run against a CI build - and as we add more tests to this project community collaboration will be easier to incorporate.
|
37 |
|
38 | ```
|
39 | npm test
|
40 | ```
|
41 |
|
42 | ## Authentication
|
43 |
|
44 | We use [OAuth2.0](https://oauth.net/2) to generate access tokens that authenticate requests against our API. Each API call will need to have a valid token populated on the API client to succeed. In a tokenSet will be an *access_token* which lasts for 30 minutes, and a *refresh_token* which lasts for 60 days. If you don't want to require your users to re-authenticate each time you want to call the API on their behalf, you will need a datastore for these tokens and will be required to refresh the tokens at least once per 60 days to avoid expiration. The `offline_access` scope is required for refresh tokens to work.
|
45 |
|
46 | In Xero a user can belong to multiple organisations. Tokens are ultimately associated with a Xero user, who can belong to multiple tenants/organisations. If your user 'Allows Access' to multiple organisations, be hyper aware of which `tenantId` you are passing to each function.
|
47 |
|
48 | ---
|
49 |
|
50 | **Step 1:** Initialize the `XeroClient`, and redirect user to xero auth flow
|
51 |
|
52 | **Step 2:** Call `apiCallback` to get your tokenSet
|
53 |
|
54 | **Step 3:** Call `updateTenants` to populate additional tenant data
|
55 | *You will need to have the `accounting.settings` scope in order to use this helper*
|
56 |
|
57 | **NOTE:** If you have already authorized the user and have stored a valid tokenSet, you can create a `new XeroClient()` and refresh your token without triggering the openid-client dependency:
|
58 | ```js
|
59 | const tokenSet = getTokenSetFromUserId(user.id) // example function
|
60 | const newXeroClient = new XeroClient()
|
61 | const newTokenSet = await newXeroClient.refreshWithRefreshToken(xero_client_id, xero_client_secret, tokenSet.refresh_token)
|
62 | // refreshWithRefreshToken calls setAccessToken() so the refreshed token will be stored on newXeroClient
|
63 | await newXeroClient.accountingApi.getInvoices('my-tenant-uuid')
|
64 | ```
|
65 |
|
66 | ---
|
67 |
|
68 | ## Step 1
|
69 | * Configure client and generate Authorization URL
|
70 | * Choose [XeroAPI Scopes](https://developer.xero.com/documentation/oauth2/scopes) based on the access you need
|
71 | * `initialize()` the client to set up the 'openid-client'
|
72 | * Build the `consentUrl`
|
73 | * Redirect to auth flow
|
74 | ```js
|
75 | const port = process.env.PORT || 3000
|
76 |
|
77 | const xero = new XeroClient({
|
78 | clientId: 'YOUR_CLIENT_ID',
|
79 | clientSecret: 'YOUR_CLIENT_SECRET',
|
80 | redirectUris: [`http://localhost:${port}/callback`],
|
81 | scopes: 'openid profile email accounting.transactions offline_access'.split(" "),
|
82 | state: 'returnPage=my-sweet-dashboard', // custom params (optional)
|
83 | httpTimeout: 3000 // ms (optional)
|
84 | });
|
85 |
|
86 | // `buildConsentUrl()` will also call `await xero.initialize()`
|
87 | let consentUrl = await xero.buildConsentUrl();
|
88 |
|
89 | res.redirect(consentUrl);
|
90 | ```
|
91 |
|
92 | ## Step 2
|
93 | Call `apiCallback` function with the response url which returns a tokenSet you can save in your datastore for future calls.
|
94 |
|
95 | *The `tokenSet` can also be accessed from the client as `xero.readTokenSet()`.*
|
96 |
|
97 | > `http://localhost:${port}/callback`
|
98 | ```js
|
99 | console.log(xero.config.state)
|
100 | => 'returnPage=my-sweet-dashboard'
|
101 |
|
102 | const tokenSet = await xero.apiCallback(req.url);
|
103 | ```
|
104 | The `tokenSet` is what you should store in your database. That object is what you will need to pass to the client. It contains your access_token and refresh_token as well as other information regarding your connection.
|
105 | ```js
|
106 | {
|
107 | id_token: 'eyJhxxxx.yyy',
|
108 | access_token: 'eyJxxx.yyy.zzz',
|
109 | expires_at: 1231231234,
|
110 | token_type: 'Bearer',
|
111 | refresh_token: 'xxxyyyyzzz',
|
112 | scope: 'openid profile email accounting.settings accounting.reports.read accounting.journals.read accounting.contacts accounting.attachments accounting.transactions offline_access',
|
113 | session_state: 'xxx.yyy'
|
114 | }
|
115 | ```
|
116 |
|
117 | ## Step 3 (convenience step)
|
118 |
|
119 | Populate the XeroClient's active tenant data.
|
120 |
|
121 | For most integrations you will want to display the org name and use additional metadata about the connected org. The `/connections` endpoint does not currently serialize all org metadata so requires developers to make an additional call for each org your user connects to get information like default currency.
|
122 |
|
123 | Calling `await xero.updateTenants()` will query the /connections endpoint and store the resulting information on the client. It has an optional parameter named `fullOrgDetails` that defaults to `true`. If you do not pass `false` to this function you will need to have the `accounting.settings` scope on your token as the `/organisation` endpoint that is called, requires it.
|
124 |
|
125 | If you don't need additional org data (like currency, shortCode, etc) calling the helper with false param `await xero.updateTenants(false)` will not kick off additional org meta data calls.
|
126 |
|
127 | ```js
|
128 | // updateTenants fullOrgDetails param will default to true
|
129 | const tenants = await xero.updateTenants()
|
130 | console.log(xero.tenants)
|
131 | [
|
132 | {
|
133 | id: 'xxx-yyy-zzz-xxx-yyy',
|
134 | tenantId: 'xxx-yyy-zzz-xxx-yyy',
|
135 | tenantType: 'ORGANISATION',
|
136 | createdDateUtc: 'UTC-DateString',
|
137 | updatedDateUtc: 'UTC-DateString',
|
138 | tenantName: 'Demo Company (US)',
|
139 | orgData: {
|
140 | organisationID: 'xxx-yyy-zzz-xxx-yyy',
|
141 | name: 'My first org',
|
142 | version: 'US',
|
143 | shortCode: '!2h37s',
|
144 | ...
|
145 | }
|
146 | }
|
147 | ]
|
148 |
|
149 | // if you pass false, the client will not fetch additional metadata about each org connection
|
150 | const tenants = await xero.updateTenants(false)
|
151 | console.log(xero.tenants)
|
152 | [
|
153 | {
|
154 | id: 'xxx-yyy-zzz-xxx-yyy',
|
155 | tenantId: 'xxx-yyy-zzz-xxx-yyy',
|
156 | tenantType: 'ORGANISATION',
|
157 | createdDateUtc: 'UTC-DateString',
|
158 | updatedDateUtc: 'UTC-DateString',
|
159 | tenantName: 'Demo Company (US)'
|
160 | }
|
161 | ]
|
162 |
|
163 | // You can also remove a connection by passing `disconnect()` the `.id` which is that tenant's connection id.
|
164 | await xero.disconnect(xero.tenants[0].id)
|
165 | ```
|
166 |
|
167 | ---
|
168 | ## Making **offline_access** calls
|
169 |
|
170 | Once you have a valid token/tokenSet saved you can set the tokenSet on the client without going through the callback by calling `setTokenSet`.
|
171 |
|
172 | For example - once a user authenticates you can refresh the token (which will also set the new token on the client) to make authorized api calls.
|
173 |
|
174 | There are two ways to refresh a token.
|
175 |
|
176 | ```js
|
177 | // refreshToken()
|
178 | const validTokenSet = await xero.refreshToken()
|
179 | ```
|
180 |
|
181 | If you already generated a valid access token, you can initialize an empty client and refresh any saved access_tokens by passing the client, secret, and refresh_token to refreshWithRefreshToken()
|
182 | ```js
|
183 | const newXeroClient = new XeroClient()
|
184 | const refreshedTokenSet = await newXeroClient.refreshWithRefreshToken(client_id, client_secret, tokenSet.refresh_token)
|
185 | ```
|
186 |
|
187 | Making Authorized API calls:
|
188 |
|
189 | ```js
|
190 | const tokenSet = getTokenSetFromDatabase(userId) // example function name
|
191 |
|
192 | await xero.setTokenSet(tokenSet)
|
193 |
|
194 | // you can call this to fetch/set your connected tenant data on your client, or you could also store this information in a database so you don't need to updateTenants every time you connect to API
|
195 | await xero.updateTenants()
|
196 |
|
197 | await xero.accountingApi.getInvoices(xero.tenants[0].tenantId)
|
198 | ```
|
199 |
|
200 | ## SDK Documentation
|
201 | * Version 3 (OAuth1.0a) https://xeroapi.github.io/xero-node/v3/index.html (*deprecated end of 2020*)
|
202 | * Accounting API: https://xeroapi.github.io/xero-node/v4/accounting/index.html
|
203 | * Assets API: https://xeroapi.github.io/xero-node/v4/assets/index.html
|
204 | * AU Payroll API: https://xeroapi.github.io/xero-node/v4/payroll-au/index.html
|
205 | * Bankfeeds API: https://xeroapi.github.io/xero-node/v4/bankfeeds/index.html
|
206 | * UK Payroll API: https://xeroapi.github.io/xero-node/v4/payroll-uk/index.html
|
207 | * Files API: https://xeroapi.github.io/xero-node/v4/files/index.html
|
208 |
|
209 |
|
210 | ### Basics
|
211 | ```js
|
212 | // example flow of initializing and using the client after someone has already authenticated and you have saved their tokenSet
|
213 | const xero = new XeroClient({
|
214 | clientId: 'YOUR_CLIENT_ID',
|
215 | clientSecret: 'YOUR_CLIENT_SECRET',
|
216 | redirectUris: [`http://localhost:${port}/callback`],
|
217 | scopes: 'openid profile email accounting.transactions offline_access'.split(" ")
|
218 | });
|
219 | await xero.initialize();
|
220 |
|
221 | const tokenSet = getYourTokenSetFromSavedLocation(currentUser)
|
222 |
|
223 | await xero.setTokenSet(tokenSet)
|
224 | ...
|
225 |
|
226 | const activeTenantId = xero.tenants[0].tenantId
|
227 |
|
228 | const getOrgs = await xero.accountingApi.getOrganisations(activeTenantId)
|
229 | const orgCountry= getOrgs.body.organisations[0].countryCode
|
230 |
|
231 | const contactsResponse = await xero.accountingApi.getContacts(activeTenantId)
|
232 | const contactId = getContactsResponse.body.contacts[0].contactID
|
233 |
|
234 | ---
|
235 | import { XeroClient, Invoice } from "xero-node";
|
236 |
|
237 | const invoices = {
|
238 | invoices: [
|
239 | {
|
240 | type: Invoice.TypeEnum.ACCREC,
|
241 | contact: {
|
242 | contactID: contactId
|
243 | },
|
244 | lineItems: [
|
245 | {
|
246 | description: "Acme Tires",
|
247 | quantity: 2.0,
|
248 | unitAmount: 20.0,
|
249 | accountCode: "500",
|
250 | taxType: "NONE",
|
251 | lineAmount: 40.0
|
252 | }
|
253 | ],
|
254 | date: "2019-03-11",
|
255 | dueDate: "2018-12-10",
|
256 | reference: "Website Design",
|
257 | status: Invoice.StatusEnum.AUTHORISED
|
258 | }
|
259 | ]
|
260 | };
|
261 |
|
262 | const createdInvoice = await xero.accountingApi.createInvoices(activeTenantId, invoices)
|
263 |
|
264 | ---
|
265 |
|
266 | // getting files as PDF
|
267 | const getAsPdf = await xero.accountingApi.getPurchaseOrderAsPdf(
|
268 | req.session.activeTenant.tenantId,
|
269 | getPurchaseOrdersResponse.body.purchaseOrders[0].purchaseOrderID,
|
270 | { headers: { accept: 'application/pdf' } }
|
271 | )
|
272 |
|
273 | // CREATE ATTACHMENT
|
274 | const filename = "xero-dev.png";
|
275 | const pathToUpload = path.resolve(__dirname, "../public/images/xero-dev.png");
|
276 | const readStream = fs.createReadStream(pathToUpload);
|
277 | const contentType = mime.lookup(filename);
|
278 |
|
279 | const accountAttachmentsResponse: any = await xero.accountingApi.createAccountAttachmentByFileName(req.session.activeTenant.tenantId, accountId, filename, readStream, {
|
280 | headers: {
|
281 | 'Content-Type': contentType
|
282 | }
|
283 | });
|
284 |
|
285 | // UPLOAD A FILE
|
286 | import * as fs from "fs";
|
287 | const mime = require("mime-types");
|
288 | const path = require("path");
|
289 |
|
290 | const tenantId = 'valid-xero-tenant-id-uuid'
|
291 | const folderId = 'valid-folder-uuid-goes-here'
|
292 |
|
293 | const filename = "xero-dev.png";
|
294 | const pathToUpload = path.resolve(__dirname, "../public/images/xero-dev.png");
|
295 | const readStream = fs.createReadStream(pathToUpload);
|
296 | const contentType = mime.lookup(filename);
|
297 |
|
298 | const uploadFile = await xero.filesApi.uploadFile(tenantId, folderId, readStream, filename, contentType);
|
299 | ```
|
300 |
|
301 | # Sample App
|
302 | For more robust examples in how to utilize our accounting api we have *(roughly)* every single endpoint mapped out with an example in our sample app - complete with showing the Xero data dependencies required for interaction with many objects ( ie. types, assoc. accounts, tax types, date formats).
|
303 |
|
304 | Just visit the repo https://github.com/XeroAPI/xero-node-oauth2-app configure your credentials & get started.
|
305 |
|
306 | ## Other Helper functions
|
307 | ```js
|
308 | xero.tenants
|
309 |
|
310 | // This needs to be called to setup relevant openid-client on the XeroClient
|
311 | await xero.initialize()
|
312 |
|
313 | // buildConsentUrl calls `await xero.initialize()` so if you wont't need to call initialize() if your using the client to send user through the auth flow.
|
314 | await xero.buildConsentUrl()
|
315 |
|
316 | // tokenSet and its expiration
|
317 | const tokenSet = await xero.readTokenSet();
|
318 |
|
319 | tokenSet.expired() // returns boolean true/false
|
320 | tokenSet.expires_in // returns seconds
|
321 | tokenSet.expires_at // returns milliseconds
|
322 | new Date(tokenSet.expires_at * 1000).toLocaleString()) // readable expiration
|
323 |
|
324 | if (tokenSet.expired()) {
|
325 | const validTokenSet = await xero.refreshToken()
|
326 | // or you can refresh the token without needing to initialize the openid-client
|
327 | // helpful for background processes where you want to limit any dependencies
|
328 | await xero.refreshWithRefreshToken(client_id, client_secret, tokenSet.refresh_token)
|
329 | }
|
330 |
|
331 | // some endpoints date fields require
|
332 | // the MS date format for POST'ing data
|
333 | const dateString = "1990-02-05"
|
334 | const birthday = await xero.formatMsDate(dateString)
|
335 |
|
336 | await xero.disconnect(xero.tenants[0].id)
|
337 |
|
338 | await xero.readIdTokenClaims()
|
339 |
|
340 | await xero.readTokenSet()
|
341 |
|
342 | const tokenSet = await xero.readTokenSet()
|
343 | await xero.setTokenSet(tokenSet)
|
344 |
|
345 | // You can revoke a user's refresh token and remove all their connections to your app by making a request to the revocation endpoint.
|
346 | await xero.revokeToken()
|
347 | ```
|