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;
}
}
-
Please edit your question to add new information In the comments it is hard to read and easily overlooked.Klaus Gütter– Klaus Gütter2025年07月19日 18:36:53 +00:00Commented Jul 19, 2025 at 18:36
-
You should only post payload you should add authorization header... Also content type should be json not urlencodedSelvin– Selvin2025年07月19日 19:10:03 +00:00Commented Jul 19, 2025 at 19:10
-
@Selvin thanks - worked that out at the same time you posted but no improvement :-(OMS Master– OMS Master2025年07月19日 19:20:32 +00:00Commented Jul 19, 2025 at 19:20
-
please see my revised code - same problem error is invalid_requestOMS Master– OMS Master2025年07月19日 19:25:34 +00:00Commented Jul 19, 2025 at 19:25
-
1Consider 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.halfer– halfer2025年07月21日 00:06:00 +00:00Commented Jul 21, 2025 at 0:06
1 Answer 1
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;
}
}
1 Comment
Explore related questions
See similar questions with these tags.