Forum Discussion
Building Custom Connector for Xero API
Hi CalebGuthrie
I did manage to get the connector working, I think the below is the updated code. I'll also update in Github shortly.
Since then, I switched to using OData feeds for Xero data as my connector could only retrieve one payslip at a time. The provider of these feeds is ODataLink. https://odatalink.com/
// This file contains your Data Connector logic
section JaimesXeroConnector;
//JaimesXeroConnector OAuth2 values;
client_id = "3";
client_secret = "v";
redirect_uri = "https://oauth.powerbi.com/views/oauthredirect.html";
token_uri = "https://identity.xero.com/connect/token?=";
authorize_uri = "https://login.xero.com/identity/connect/authorize";
logout_uri = "https://login.microsoftonline.com/logout.srf";
connection_uri = "https://api.xero.com/connections"; //Jaime added 6.1.21
tenantid = Text.From(GetTenantId(connection_uri));
// Login modal window dimensions
windowWidth = 720;
windowHeight = 1024;
//OAuth2 Scopes
scope_prefix = "";
scopes = {
"offline_access",
"openid",
"profile",
"email",
"accounting.transactions.read",
"accounting.settings.read",
"accounting.reports.read",
"accounting.journals.read",
"accounting.contacts.read",
"assets.read",
"payroll.employees.read",
"payroll.payruns.read",
"payroll.payslip.read",
"payroll.settings.read",
"payroll.timesheets.read"
};
GetTenantId = (url as text) =>
let
Source = Web.Contents(url),
ImportedJSON = Json.Document(Source,1252),
ConvertedtoTable = Table.FromList(ImportedJSON, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
ExpandedColumn1 = Table.ExpandRecordColumn(ConvertedtoTable, "Column1", {"id", "authEventId", "tenantId", "tenantType", "tenantName", "createdDateUtc", "updatedDateUtc"}, {"id", "authEventId", "tenantId", "tenantType", "tenantName", "createdDateUtc", "updatedDateUtc"}),
ChangedType = Table.TransformColumnTypes(ExpandedColumn1,{{"id", type text}, {"authEventId", type text}, {"tenantId", type text}, {"tenantType", type text}, {"tenantName", type text}, {"createdDateUtc", type datetime}, {"updatedDateUtc", type datetime}}),
tenantId = Text.From(ChangedType{0}[tenantId])
in
tenantId;
[DataSource.Kind="JaimesXeroConnector", Publish="JaimesXeroConnector.Publish"]
shared JaimesXeroConnector.Contents = (url as text) =>
let
source = Json.Document(Web.Contents(url,[Headers = [#"xero-tenant-id"=tenantid,#"Accept" = "application/json"]]))
in
source;
// Data Source Kind description
JaimesXeroConnector= [
TestConnection = (DataSourcePath) =>
let
json = Json.Document(DataSourcePath),
server = json[server],
database = json[database]
in
try
{ "JaimesXeroConnector.Contents", server, database }
otherwise
let
message = Text.Format("Couldn't find entity.")
in
Diagnostics.Trace(TraceLevel.Error, message, () => error message, true )
,
Authentication = [
OAuth = [
StartLogin=StartLogin,
FinishLogin=FinishLogin,
Refresh=Refresh,
Logout=Logout
]
],
Label = Extension.LoadString("DataSourceLabel")
];
// Data Source UI publishing description
JaimesXeroConnector.Publish = [
Beta = true,
Category = "Other",
ButtonText = { Extension.LoadString("ButtonTitle"), Extension.LoadString("ButtonHelp") },
LearnMoreUrl = "https://powerbi.microsoft.com/",
SourceImage = JaimesXeroConnector.Icons,
SourceTypeImage = JaimesXeroConnector.Icons
];
// Helper functions for OAuth2: StartLogin, FinishLogin, Refresh, Logout
StartLogin = (resourceUrl, state, display) =>
let
authorizeUrl = authorize_uri & "?" & Uri.BuildQueryString([
response_type = "code",
client_id = client_id,
redirect_uri = redirect_uri,
state = state,
scope = GetScopeString(scopes, scope_prefix)
])
in
[
LoginUri = authorizeUrl,
CallbackUri = redirect_uri,
WindowHeight = 720,
WindowWidth = 1024,
Context = null
];
FinishLogin = (context, callbackUri, state) =>
let
// parse the full callbackUri, and extract the Query string
parts = Uri.Parts(callbackUri)[Query],
// if the query string contains an "error" field, raise an error
// otherwise call TokenMethod to exchange our code for an access_token
result = if (Record.HasFields(parts, {"error", "error_description"})) then
error Error.Record(parts[error], parts[error_description], parts)
else
TokenMethod("authorization_code", parts[code])
in
result;
Refresh = (resourceUrl, refresh_token) => TokenMethod("refresh_token", "refresh_token", refresh_token);
Logout = (token) => logout_uri;
// see "Exchange code for access token: POST /oauth/token" for details
TokenMethod = (grantType, code) =>
//TokenMethod = (grantType, tokenField, code) =>
let
query = [
// queryString = [
grant_type = grantType,
// grant_type = "authorization_code",
redirect_uri = redirect_uri,
client_id = client_id,
client_secret = client_secret
],
queryWithCode = if(grantType = "refresh_token") then [refresh_token = code] else [code = code],
// queryWithCode = Record.AddField(queryString, tokenField, code),
tokenResponse = Web.Contents(token_uri, [
Content = Text.ToBinary(Uri.BuildQueryString(query & queryWithCode)),
// Content = Text.ToBinary(Uri.BuildQueryString(queryWithCode)),
// Headers = [
// #"authorization" = Text.Combine({"Basic ", "base64encode(", client_id, ":", client_secret, ")"} ), // JB
// #"Content-type" = "application/x-www-form-urlencoded",
// #"Accept" = "application/json"
// ],
Headers= [#"Content-type" = "application/x-www-form-urlencoded",#"Accept" = "application/json"],
ManualStatusHandling = {400}
]),
body = Json.Document(tokenResponse),
result = if (Record.HasFields(body, {"error", "error_description"})) then
error Error.Record(body[error], body[error_description], body)
else
body
in
result;
Value.IfNull = (a, b) => if a <> null then a else b;
GetScopeString = (scopes as list, optional scopePrefix as text) as text =>
let
prefix = Value.IfNull(scopePrefix, ""),
addPrefix = List.Transform(scopes, each prefix & _),
asText = Text.Combine(addPrefix, " ")
in
asText;
JaimesXeroConnector.Icons = [
Icon16 = { Extension.Contents("JaimesXeroConnector16.png"), Extension.Contents("JaimesXeroConnector20.png"), Extension.Contents("JaimesXeroConnector24.png"), Extension.Contents("JaimesXeroConnector32.png") },
Icon32 = { Extension.Contents("JaimesXeroConnector32.png"), Extension.Contents("JaimesXeroConnector40.png"), Extension.Contents("JaimesXeroConnector48.png"), Extension.Contents("JaimesXeroConnector64.png") }
];- NE1au4 years agoFrequent Visitor
Hi Mark,
Nic here from OdataLink.
First off, (sorry about the plug), but if you want a simple way, try OdataLink.
https://odatalink.com/get-xero-data-in-power-bi/
From Jaime's main code, you'll want to ensure you edit the following two lines at the top of her code. You need to use your own client id and secret you registered with the xero app portal.
client_id = "3"; client_secret = "v";Secondly, you need to use the following redirect uri that jaime used (unsure where she got that from). But this needs to match and be the same as how you setup your app. You might just need to update your xero app listing to use the correct value (it needs to be exactly as per below).
redirect_uri = "https://oauth.powerbi.com/views/oauthredirect.html";From your screenshot, it's most likely related to three two values being wrong (either in your power query, or in the xer app).
Regards
Nic
- Anonymous4 years agoNot applicable
I have changed client I'd and secret.
The issue is redirected URL it's the same in xero API but still facing the issue. I checked on the postman app rest is working fine.
Can't move on odatalink because highly paid.
- NE1au4 years agoFrequent Visitor
Hi Mark,
The main thing would be to check.
1) the client id as that is how the association is made in xero
2) the redirect url needs to match both in xero and your code
3) the scope may be wrong, you coudl try a shorter scope (e.g. bare minimum).
offline_access openid profile email accounting.transactions.readDocumentation/troubleshooting link.
https://developer.xero.com/documentation/guides/oauth2/troubleshooting