I am trying to implement xero account software in my php project. I have to create an invoice with an attachment. I am using xero api for this. Initial authentication is working well. Means if I authenticate first time then it generates all the tokens and those tokens are being saved into the table. Below is the code for authenticate: xero_callback.php is: This is working fine.
<?php
session_start();
require 'db.php'; // Include database connection file
define('CLIENT_ID', 'xxxxx');
define('CLIENT_SECRET', 'xxxxx');
define('REDIRECT_URI', 'http://localhost/xero_testing/xero_callback.php');
if (!isset($_GET['code'])) {
die("No authorization code received.");
}
$code = $_GET['code'];
$tokenUrl = ";;
$data = [
"grant_type" => "authorization_code",
"code" => $code,
"redirect_uri" => REDIRECT_URI,
"client_id" => CLIENT_ID,
"client_secret" => CLIENT_SECRET
];
$ch = curl_init($tokenUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/x-www-form-urlencoded"]);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
if (!isset($result["access_token"])) {
die("Failed to get access token.");
}
$accessToken = $result["access_token"];
$refreshToken = $result["refresh_token"];
$expiresIn = $result["expires_in"];
// Get Tenant ID
$ch = curl_init(";);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $accessToken",
"Content-Type: application/json"
]);
$tenantResponse = curl_exec($ch);
curl_close($ch);
$tenantData = json_decode($tenantResponse, true);
$tenantId = $tenantData[0]["tenantId"] ?? null;
if (!$tenantId) {
die("Failed to retrieve Tenant ID.");
}
// Store in Database
$db->query("INSERT INTO xero_tokens (tenant_id, access_token, refresh_token, expires_in)
VALUES ('$tenantId', '$accessToken', '$refreshToken', '$expiresIn')");
echo "Successfully connected to Xero! Tenant ID: $tenantId";
?>
Now below is the code to create invoice and update access token when it expires. After authenticating first time everything is working. But when access token gets expired after 30 mins and then if I regenerate the access token using the refresh token then it gives error: below is the code for create invoice:
<?php
require 'db.php';
$result = $db->query("SELECT * FROM xero_tokens ORDER BY created_at DESC LIMIT 1");
$xero = $result->fetch_assoc();
$accessToken = $xero['access_token'];
$tenantId = $xero['tenant_id'];
$refreshToken = $xero['refresh_token'];
$record_id = $xero['id'];
$contactID = getContactID('John Doe', $accessToken, $tenantId);
$invoiceData = [
"Type" => "ACCREC",
"Contact" => ["ContactID" => $contactID],
"LineItems" => [[
"Description" => "Consulting Service",
"Quantity" => 1,
"UnitAmount" => 200.00,
"AccountCode" => "200"
]],
"Date" => date("Y-m-d"),
"DueDate" => date("Y-m-d", strtotime("+30 days")),
"InvoiceNumber" => "INV-" . time()
];
$headers = [
"Authorization: Bearer $accessToken",
"Xero-tenant-id: $tenantId",
"Content-Type: application/json",
"Accept: application/json",
];
$ch = curl_init(".xro/2.0/Invoices");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($invoiceData));
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode == 401 || $httpCode == 403) {
refreshAccessToken($refreshToken, $record_id);
// Fetch the latest access token from DB after refreshing
$result = $db->query("SELECT * FROM xero_tokens ORDER BY created_at DESC LIMIT 1");
$xero = $result->fetch_assoc();
$accessToken = $xero['access_token'];
$tenantId = $xero['tenant_id'];
$headers = [
"Authorization: Bearer $accessToken",
"Xero-tenant-id: $tenantId",
"Content-Type: application/json",
"Accept: application/json",
];
$ch = curl_init(".xro/2.0/Invoices");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($invoiceData));
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
}
curl_close($ch);
$responseData = json_decode($response, true);
$invoiceId = $responseData['Invoices'][0]['InvoiceID'] ?? null;
if (!$invoiceId) {
die("Failed to create Invoice.");
}
echo "Invoice Created: ID = $invoiceId";
$path = 'ALK.pdf';
$filename = 'ALK.pdf';
if (file_exists($path)) {
$fileHandle = fopen($path, 'r');
$fileSize = filesize($path);
$ch = curl_init(".xro/2.0/Invoices/$invoiceId/Attachments/$filename");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_UPLOAD, true);
curl_setopt($ch, CURLOPT_INFILE, $fileHandle);
curl_setopt($ch, CURLOPT_INFILESIZE, $fileSize);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $accessToken",
"Xero-tenant-id: $tenantId",
"Content-Type: application/octet-stream",
"Content-Length: $fileSize"
]);
$attachmentResponse = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
fclose($fileHandle);
curl_close($ch);
if ($httpCode == 200) {
echo "Attachment Uploaded Successfully!";
} else {
echo "Error uploading attachment: ";
print_r(json_decode($attachmentResponse, true));
}
} else {
echo "File does not exist at path: " . $path;
}
function getContactID($contactName, $accessToken, $tenantId) {
$headers = [
"Authorization: Bearer $accessToken",
"Accept: application/json"
];
$ch = curl_init(";);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 200) {
echo "Access Token is valid.";
print_r($response);
} else {
echo "Access Token is invalid or expired. HTTP Code: $httpCode";
}
//exit;
$contactName = urlencode($contactName);
$url = ".xro/2.0/Contacts?where=Name.Contains(\"$contactName\")";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $accessToken",
"Xero-tenant-id: $tenantId",
"Content-Type: application/json",
"Accept: application/json"
]);
$response = curl_exec($ch);
$data = json_decode($response, true);
if (isset($data['Contacts'][0]['ContactID'])) {
return $data['Contacts'][0]['ContactID']; // Return ContactID if found
} else {
return null; // Contact not found
}
}
function refreshAccessToken($refreshToken, $record_id) {
global $db;
$clientId = 'xxxx';
$clientSecret = 'xxxxx';
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => '',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'refresh_token',
'client_id' => $clientId,
'client_secret' => $clientSecret,
'refresh_token' => $refreshToken
]),
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded'
],
));
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
if ($httpCode == 200) {
$tokenData = json_decode($response, true);
if (isset($tokenData['access_token'], $tokenData['refresh_token'])) {
// Update the database with the new tokens
$newAccessToken = $tokenData['access_token'];
$newRefreshToken = $tokenData['refresh_token'];
$updateQuery = $db->prepare("UPDATE xero_tokens SET access_token = ?, refresh_token = ?, created_at = NOW() WHERE id = ?");
$updateQuery->bind_param("ssi", $newAccessToken, $newRefreshToken, $record_id);
$updateQuery->execute();
if ($updateQuery->affected_rows > 0) {
echo "Access token refreshed successfully!";
} else {
echo "Failed to update token in the database.";
}
} else {
echo " Refresh token response missing expected fields.";
}
} else {
echo "Failed to refresh access token. HTTP Code: $httpCode";
}
}
function saveNewToken($accessToken, $refreshToken, $record_id)
{
global $db;
$stmt = $db->prepare("UPDATE xero_tokens SET access_token = ?, refresh_token = ?, created_at = NOW() WHERE id = ?");
$stmt->bind_param("ssi", $accessToken, $refreshToken, $record_id);
$stmt->execute();
}
?>
Error is:
Access Token is valid.[]403
{"Type":null,"Title":"Forbidden","Status":403,"Detail":"AuthenticationUnsuccessful","Instance":"1111eace-cfc0-413b-8111-5c53832769b7","Extensions":{}}