-5

I am new to this. I am trying to interact with Xero APIs (identity initially) in C# without using the sdk. I did this successfully in Google Apps Script using UrlFetchApp.fetch but now here is my C# code at the moment...

 private static async Task<string> XeroConnect()
 {
 var valueBytes = Encoding.UTF8.GetBytes(xeroClientId + ":" + xeroSecretKey);
 var connectParams = new {
 method = "post",
 headers = new { 
 Authorization = "Basic " + Convert.ToBase64String(valueBytes)
 },
 payload = new {
 grant_type = "client_credentials",
 scope = "accounting.transactions accounting.contacts"
 }
 };
 using (var client = new HttpClient())
 {
 using (var content = new StringContent(JsonConvert.SerializeObject(connectParams), Encoding.UTF8, "application/json"))
 {
 content.Headers.Clear();
 content.Headers.Add("Content-Type", "application/x-www-form-urlencoded");
 HttpResponseMessage response = await client.PostAsync(xeroConnectAPI, content);
 response.EnsureSuccessStatusCode();
 dynamic responseValues = JsonConvert.DeserializeObject(response.Content.ToString());
 if (responseValues.HasErrors || responseValues.HasValidationErrors)
 {
 throw new Exception(response.Content.ToString());
 }
 return responseValues.access_token;
 }
 }
 }

Just get a 400 error with no additional info. I tried lots of different ways using PostAsJsonAsync etc but same response every time.

Here is the Google Apps script code...


function xeroConnect() {
 let step = 0;
 try
 {
 const constants = globalConstants();
 step = 10;
 const params = {
 method: "post",
 headers: {Authorization: "Basic " + Utilities.base64Encode(constants.xeroClientId + ":" + constants.xeroSecretKey)},
 payload: {
 "grant_type": "client_credentials",
 "scope": "accounting.transactions accounting.contacts",
 }
 };
 step = 20;
 let response = UrlFetchApp.fetch(constants.xeroConnectAPI, params);
 step = 25;
 if (!response) { throw "No response"; }
 step = 30;
 let responseValues = JSON.parse(response.getContentText());
 step = 40;
 if (responseValues.HasErrors || responseValues.HasValidationErrors)
 {
 throw(response.getContentText());
 }
 step = 50;
 return responseValues.access_token;
 }
 catch (error)
 {
 let errorString = "ERROR: xeroConnect; error=" + JSON.stringify(error) + ";step=" + step;
 Logger.log(errorString);
 return errorString;
 }
}

Here is my revised code...

 private static async Task<string> XeroConnect()
 {
 var valueBytes = Encoding.UTF8.GetBytes(xeroClientId + ":" + xeroSecretKey);
 var payload = new
 {
 grant_type = "client_credentials",
 scope = "accounting.transactions accounting.contacts"
 };
 using (var client = new HttpClient())
 {
 client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(valueBytes));
 HttpResponseMessage response = await client.PostAsJsonAsync(xeroConnectAPI, payload);
 //response.EnsureSuccessStatusCode();
 var responseContent = await response.Content.ReadAsStringAsync();
 dynamic responseValues = JsonConvert.DeserializeObject(response.Content.ToString());
 if (responseValues.HasErrors || responseValues.HasValidationErrors)
 {
 throw new Exception(response.Content.ToString());
 }
 return responseValues.access_token;
 }
 }

here is the working code...

 private static async Task<string> XeroConnect()
 {
 var valueBytes = Encoding.UTF8.GetBytes(xeroClientId + ":" + xeroSecretKey);
 using (var client = new HttpClient())
 {
 client.Timeout = TimeSpan.FromSeconds(300);
 client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
 client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(valueBytes));
 var content = new MultipartFormDataContent();
 content.Add(new StringContent("client_credentials"), "grant_type");
 content.Add(new StringContent("accounting.transactions accounting.contacts"), "scope");
 var header = new ContentDispositionHeaderValue("form-data");
 content.Headers.ContentDisposition = header;
 var response = await client.PostAsync(xeroConnectAPI, content);
 response.EnsureSuccessStatusCode();
 var responseContent = await response.Content.ReadAsStringAsync();
 dynamic responseValues = JsonConvert.DeserializeObject(responseContent);
 return responseValues.access_token;
 }
 }
halfer
20.2k20 gold badges111 silver badges208 bronze badges
asked Jul 19, 2025 at 17:55
9
  • Please edit your question to add new information In the comments it is hard to read and easily overlooked. Commented Jul 19, 2025 at 18:36
  • You should only post payload you should add authorization header... Also content type should be json not urlencoded Commented Jul 19, 2025 at 19:10
  • @Selvin thanks - worked that out at the same time you posted but no improvement :-( Commented Jul 19, 2025 at 19:20
  • please see my revised code - same problem error is invalid_request Commented Jul 19, 2025 at 19:25
  • 1
    Consider posting the solution as an answer, an in that answer explain what bit you changed that caused it to work. It might be useful for a future reader. Commented Jul 21, 2025 at 0:06

1 Answer 1

0

here is the working code...

Not sure why only this approach works as the JSON way should have I believe but it seems this call to the Xero identity API requires form-data and NOT JSON.

 private static async Task<string> XeroConnect()
 {
 var valueBytes = Encoding.UTF8.GetBytes(xeroClientId + ":" + xeroSecretKey);
 using (var client = new HttpClient())
 {
 client.Timeout = TimeSpan.FromSeconds(300);
 client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
 client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(valueBytes));
 var content = new MultipartFormDataContent();
 content.Add(new StringContent("client_credentials"), "grant_type");
 content.Add(new StringContent("accounting.transactions accounting.contacts"), "scope");
 var header = new ContentDispositionHeaderValue("form-data");
 content.Headers.ContentDisposition = header;
 var response = await client.PostAsync(xeroConnectAPI, content);
 response.EnsureSuccessStatusCode();
 var responseContent = await response.Content.ReadAsStringAsync();
 dynamic responseValues = JsonConvert.DeserializeObject(responseContent);
 return responseValues.access_token;
 }
 }
answered Jul 22, 2025 at 9:15
Sign up to request clarification or add additional context in comments.

1 Comment

Thank you for contributing to the Stack Overflow community. This may be a correct answer, but it’d be really useful to provide additional explanation of your code so developers can understand your reasoning. This is especially useful for new developers who aren’t as familiar with the syntax or struggling to understand the concepts. Would you kindly edit your answer to include additional details for the benefit of the community?

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.