I’m trying to set up a custom integration for my Xero organisation account, and I’m busy building it with the Demo Company. We have purchased a subscription for the actual organisation which will be used when the code goes to production.
I’m trying to set up my first API function, which is through the xero-node SDK.
I’m correctly authenticating using my client id and secret, which returns an access token. I then use this access token to make the API request, and i set the access token to my xero client in my code so that i can make the requests through the SDK.
When i try to make a call to xero.accountingAPI.getContacts, i get a 403 error.
I’ve double checked my scopes and they are fine, my access token decoded includes the accounting.contacts scope so that shouldn’t be the issue.
I’ve read a lot about the xero-tenant-id header, but when i run xero.updateTenants and then print out the list of tenants, them, there is no tenant in the list, it is empty. I’m a bit confused though, my understanding is that there shouldn’t be a need for a tenant-id because it is a custom integration, specifically for my own organisation, and so it shouldn’t be required.
-
If the tenant ID is required, can anyone please point me in the right direction of where I get the tenant ID and how i set it in the xero client in my code?
-
If it’s not required, could you please provide some guidance as to what else could be wrong with my set up?
I’ve included my code below which kicks off when I call the generateClientInXero function, and the error code below that.
A comment – the console.log(xero.tenants) prints a blank array ([])
Thanks!
const xero = new XeroClient({
clientId: process.env.XERO_CLIENT_ID,
clientSecret: process.env.XERO_CLIENT_SECRET,
grantType: "client_credentials",
scopes: "accounting.transactions accounting.settings accounting.contacts accounting.settings.read".split(" "),
});
const generateClientInXero = async (company) => {
try {
// await generateConsentUrl();
// Obtain access token
const tokenSet = await getAccessToken();
console.log("back with access token", tokenSet);
// const xero_scopes = ;
// Set the token in Xero client
xero.setTokenSet(tokenSet);
xero.updateTenants();
console.log(xero.tenants);
// // Setup client and contacts in Xero
const clientContactID = await setupClientInXero(company);
} catch (err) {
console.error(err);
}
};
const getClientCredentialsToken = async () => {
const url = "https://identity.xero.com/connect/token";
const client_id = process.env.XERO_CLIENT_ID;
const client_secret = process.env.XERO_CLIENT_SECRET;
const xero_scopes = "accounting.transactions accounting.settings accounting.contacts accounting.settings.read"; // Assuming xero_scopes is defined somewhere
// Encode client_id and client_secret to Base64
const credentials = Buffer.from(`${client_id}:${client_secret}`).toString("base64");
// Request body parameters
const data = new URLSearchParams();
data.append("grant_type", "client_credentials");
data.append("scope", "accounting.contacts accounting.transactions accounting.settings accounting.settings.read");
try {
const response = await axios.post(url, data, {
headers: {
Authorization: `Basic ${credentials}`,
"Content-Type": "application/x-www-form-urlencoded",
},
});
return response.data;
} catch (error) {
console.error("Error obtaining access token:", error.response ? error.response.data : error.message);
throw new Error("Failed to obtain access token");
}
};
const storeAccessToken = async (accessToken) => {
const tokenDocRef = firestore.collection("xeroTokens").doc("tokenSet");
await tokenDocRef.set({
...accessToken,
updatedAt: DateTime.now().toISO(),
});
};
const loadAccessToken = async () => {
const tokenDocRef = firestore.collection("xeroTokens").doc("tokenSet");
const tokenDoc = await tokenDocRef.get();
if (tokenDoc.exists) {
const tokenData = tokenDoc.data();
console.log("Token exists");
// const currentTimestamp = new Date().getTime(); // Current time in milliseconds
// const expiresAt = tokenData.expiresAt || 0; // Ensure expiresAt is defined
// if (expiresAt < currentTimestamp) {
// console.log("TOKEN EXPIRED - should get new one now");
// throw new Error("Xero access token has expired. Please re-authenticate.");
// }
return tokenData;
} else {
throw new Error("Xero access token not found. Please authenticate first.");
}
};
const getAccessToken = async () => {
let accessToken;
try {
accessToken = await loadAccessToken();
} catch (error) {
console.log("ERROR IN GET ACCESS TOKEN: ", error);
accessToken = await getClientCredentialsToken();
await storeAccessToken(accessToken);
}
return accessToken;
};
Error:
{
"response": {
"statusCode": 403,
"body": {
"Type": null,
"Title": "Forbidden",
"Status": 403,
"Detail": "AuthenticationUnsuccessful",
"Instance": "43e09fc9-ea0f-4679-b1d5-c907777bacf0",
"Extensions": {}
},
"headers": {
"content-type": "application/json",
"content-length": "150",
"server": "nginx",
"xero-correlation-id": "43e01324-ea0f-4679-b1d5-c912313132",
"x-appminlimit-remaining": "9998",
"expires": "Fri, 28 Jun 2024 13:33:19 GMT",
"cache-control": "max-age=0, no-cache, no-store",
"pragma": "no-cache",
"date": "Fri, 28 Jun 2024 13:33:19 GMT",
"connection": "close",
"x-client-tls-ver": "tls1.3",
"set-cookie": "ak_bmsc=xxxxxxxxxx....; Domain=.xero.com; Path=/; Expires=Fri, 28 Jun 2024 15:33:19 GMT; Max-Age=7200"
},
"request": {
"url": { "protocol": "https:", "port": 443, "host": "api.xero.com", "path": "/api.xro/2.0/Contacts" },
"headers": {
"accept": "application/json",
"content-type": "application/json",
"user-agent": "xero-node-7.0.0",
"xero-tenant-id": "[object Object]",
"authorization": "Bearer xxxxxxxxxxxxxx....",
"content-length": "2",
"accept-encoding": "gzip, compress, deflate, br",
"host": "api.xero.com"
},
"method": "GET"
}
},
"body": {
"Type": null,
"Title": "Forbidden",
"Status": 403,
"Detail": "AuthenticationUnsuccessful",
"Instance": "43e09fc9-ea0f-4679-b1d5-c912313132",
"Extensions": {}
}
}