Xero API - Standard PHP/cURL Create Invoice

What?
This is an article documenting a generic script that can be used to push an invoice to the demo Xero environment. Following the steps below will connect you to the Demonstration environment of Xero at no cost to you the developer.

How?
Similar to my ZohoAPI script the process is:
  1. Start with a HTML form to add your client ID/client Secret/scopes and Redirect URI. These get stored in a temporary file to retrieve later.
  2. Then you get a button that will return the "CODE" variable via GET (URL). Clicking on this will get the CODE variable and use it to generate a refresh token.
  3. Then with the refresh token generate an access token.
  4. Then with the access token get a connection "TenantID".
  5. Then with the tenantID we can retrieve records and/or finally create an invoice (accounts receivables).
Xero Signup
So first you need to be a user of Xero. For this demonstration, we will sign up to the demo company that Xero has already setup:
  1. Sign up for a free account
    Xero Signup Step 1
  2. You will be emailed an activation link. Copy the link and paste in a browser using a dedicated profile. I am using "Guest" as per my screenshots but you should use the profile that your Xero account will be auto logged in.
  3. Set a password, phone number and click on "Activate Your Account"
    Xero Signup Step 2
  4. You'll be asked to setup your organization but as a developer, just click on the "Try the demo company" link.
    Xero Signup Step 3
  5. Done. Welcome to Xero!
App Registration
As per most third-party APIs, you need to register your app:
  1. Go to the Xero Developer My Apps page
  2. Cilck on "Add App"
  3. Complete the form as appropriate, in my case:
    App Registration Step 1
  4. Generate and copy to an external file the Client ID and Client Secret (not shown again):App Registration Step 2
The App
The following are 2 generic scripts that can be used for any app and should be uploaded to your server that will be executing the scripts.

The redirect_uri.php file I have merely contains the following line:
<?php
	header('Location: https://api.joellipman.com/xero/get_tokens.php?' . http_build_query($_GET));
?>
This simply redirects to our main script and forwards all the variables sent to the redirect_uri script.

The get_tokens.php file is our main file that initially displays a HTML form to enter the client ID/secret and scope:
<?php
/*
	---------------------------------------------------------------------------
	Xero Authorization via OAuth2.0 for REST API v2
	---------------------------------------------------------------------------

    Xero API v2:	https://go.xero.com/Dashboard/
                    https://developer.xero.com/myapps/details?appId=2698e616-ca1e-44ac-9245-5cc27224f21d

	Documentation:	https://developer.xero.com/documentation/oauth2/auth-flow

    Available Scopes
        oauth       offline_access, openid, profile, email
        accounting  accounting.transactions, accounting.transactions.read,
                    accounting.reports.read, accounting.journals.read,
                    accounting.settings, accounting.settings.read,
                    accounting.contacts, accounting.contacts.read,
                    accounting.attachments, accounting.attachments.read
        payroll(AU) payroll.employees, payroll.employees.read, payroll.payruns,
                    payroll.payruns.read, payroll.payslip, payroll.payslip.read,
                    payroll.timesheets, payroll.timesheets.read, payroll.settings,
                    payroll.settings.read
        payroll(UK) payroll.employees, payroll.employees.read, payroll.payruns,
                    payroll.payruns.read, payroll.payslip, payroll.payslip.read,
                    payroll.timesheets, payroll.timesheets.read, payroll.settings,
                    payroll.settings.read
        payroll(NZ) payroll.employees, payroll.employees.read, payroll.payruns,
                    payroll.payruns.read, payroll.payslip, payroll.payslip.read,
                    payroll.timesheets, payroll.timesheets.read, payroll.settings,
                    payroll.settings.read
        files       files, files.read
        assets      assets, assets.read
        projects    projects, projects.read
        payments    paymentservices
        bank feed   bankfeeds
*/
header("Content-Type: text/html");
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

// init
$b_DemoMode = true;
$b_CreateInvoice = false;
$v_AccessTokenPath = $v_RefreshTokenPath = "";
$a_CheckFields = $a_GrantFields = $a_Header = $a_Payload = array();
$v_RootFolder = "../App_Data/ab";  // must be a writeable folder on your server
$v_TmpFolder = $v_RootFolder . "/tmp/xero";
$v_DelFolder = $v_TmpFolder . "/delete_me";
$v_WorkingFolder = $v_TmpFolder;
$v_TmpFile = "grant_info_" . date("YmdH") . ".dat";  // stores the initial form details
$v_AuthEndpoint = "https://login.xero.com/identity/connect/authorize";
$v_TokenEndpoint = "https://identity.xero.com/connect/token";

// your own values here or leave blank
$v_PresetClientID = "";  // can be overwritten in initial form
$v_PresetClientSecret = "";  // can be overwritten in initial form
$v_ExistingInvoiceID = "";  // id of 1 invoice record to test data retrieval
$v_YourXeroContactID = "";  // id of 1 contact record to create invoice in this contacts name
$v_RedirectUri = "https://" . $_SERVER['SERVER_NAME'] . "/xeroapi/redirect_uri.php";  // has to be https protocol
$v_Scope = "offline_access openid profile email accounting.transactions accounting.contacts accounting.attachments accounting.settings.read files assets";  // can be overwritten in initial form

// create temp folder if not exists
if (!file_exists($v_TmpFolder)) {
    mkdir($v_TmpFolder, 0777, true);
}
if (!file_exists($v_DelFolder)) {
    mkdir($v_DelFolder, 0777, true);
}

// if client id in url then get and store
if(isset($_GET['client_id'])){
    if($_GET['client_id'] != ""){
        file_put_contents($v_TmpFolder . "/" . $v_TmpFile, json_encode($_GET, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
    }
}

// begin HTML output
echo "<html><head><title>JoelLipman - Xero API v2 Tokenizer</title><style>";
$v_AppStyle = "body{font-family:Arial,Verdana,Sans-serif;background-color:#ccc;}select,option,input.txt{padding:5px 10px;outline:0;border:1px solid#ccc;border-radius:5px;}input.btn{padding:10px 20px;outline:0;color: #fff;border-color: #5cac00;background-color: #7bd000;text-shadow: 0 1px rgba(0,0,0,0.1);background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiP…dpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==);background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #7bd000),color-stop(100%, #6bb101));background: -webkit-linear-gradient(#7bd000,#6bb101);background: -webkit-linear-gradient(#7bd000, #6bb101);background: linear-gradient(#7bd000,#6bb101);border-radius:2px;cursor:pointer;}input.txt{width:400px;}form{margin:20px;}h3{margin:0;}select{float:right;}td{padding:2px 5px;}label{display:inline-block;width:100px;}div.panel{background-color:#fff;border-radius:10px;margin:20px 10px;padding:20px 10px;-webkit-box-shadow:5px 5px 5px 0px rgba(51,51,51,0.3);-moz-box-shadow:5px 5px 5px 0px rgba(51,51,51,0.3);box-shadow:5px 5px 5px 0px rgba(51,51,51,0.3);}form{margin:0;padding:0;}#copyright{bottom:0;margin:0 auto;text-align:center;width:97%;margin-bottom:10px;}#copyright a{color:#999;text-decoration:none;font-size:10pt;line-height:20px;}#menu_bottom a{color:#f00;text-decoration:none;bottom:30;font-size:10pt;line-height:20px;}.centered{text-align:center;margin:0 auto;}h5{color:#999;font-weight:100;font-size:10pt;margin-bottom:5px;margin-top:10px;}tt.source{color:#fff;background-color:#000;}tt.target{background-color:yellow;}tt.bold{color:red;font-weight:700;}ul{margin:10px 0 0 85px;font-size:75%;color:#666;padding:0;}pre{margin:0;}.match{background-color:red;color:#fff;}";
echo $v_AppStyle . "</style></head><body>";
if(isset($_GET['scope']) && file_exists($v_TmpFolder . "/" . $v_TmpFile)){
    //
    // 0. initialize vars
    //
    echo "<div class='panel'>";
    echo "<h3>0. INITIALIZE...</h3>";
    echo "<h5>Info:</h5>";
    if(file_exists($v_TmpFolder . "/" . $v_TmpFile) && isset($_GET['code'])){
        $m_GrantInfo = json_decode(file_get_contents($v_TmpFolder . "/" . $v_TmpFile), true);
        $a_CheckFields['client_id'] = $m_GrantInfo['client_id'];
        $a_CheckFields['client_secret'] = $m_GrantInfo['client_secret'];
        $a_CheckFields['scope'] = $m_GrantInfo['scope'];
        $a_CheckFields['redirect_uri'] = $v_RedirectUri;
        rename($v_TmpFolder . "/" . $v_TmpFile, $v_DelFolder . "/" . $v_TmpFile);
    }else{
        $a_CheckFields['client_id'] = $_GET['client_id'];
        $a_CheckFields['client_secret'] = $_GET['client_secret'];
        $a_CheckFields['scope'] = $_GET['scope'];
        $a_CheckFields['redirect_uri'] = $v_RedirectUri;
    }
    $a_InitFields['client_id'] = $a_CheckFields['client_id'];
    $a_InitFields['client_secret'] = $a_CheckFields['client_secret'];
    $a_InitFields['scope'] = $a_CheckFields['scope'];
    $a_InitFields['redirect_uri'] = $a_CheckFields['redirect_uri'];
    $a_Info = array();
    $a_Info['api']['header'] = false;
    $a_Info['api']['ssl_check'] = false;
    $a_Info['demo'] = $b_DemoMode ? "<tt class='bold'>true</tt>" : "<tt class='bold'>false</tt>";
    echo "<pre>" . json_encode(array_merge($a_InitFields,$a_Info), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
    //
    // 1. get grant token
    //
    echo "</div><div class='panel'>";
    echo "<h3>1. GRANT TOKEN...</h3>";
    if($a_CheckFields['client_id']!="")
    {
        $v_GrantTokenPath = $v_RootFolder . "/" . $a_CheckFields['client_id'] . "/grant_token.dat";
        $v_RefreshTokenPath = $v_RootFolder . "/" . $a_CheckFields['client_id'] . "/refresh_token.dat";
        $v_AccessTokenPath = $v_RootFolder . "/" . $a_CheckFields['client_id'] . "/access_token.dat";
        $v_WorkingFolder = $v_RootFolder . "/" . $a_CheckFields['client_id'];
        if (!file_exists($v_WorkingFolder)) {
            mkdir($v_WorkingFolder, 0777, true);
        }
    }
    echo "<h5>Info:</h5>";
    $a_Info = array();
    $a_Info['api']['endpoint'] = $v_AuthEndpoint;
    $a_Info['api']['method'] = "GET";
    $a_Info['api']['header'] = false;
    $a_Info['api']['ssl_check'] = false;
    $a_Info['expires'] = "<tt class='bold'>" . "in 1 to 10 minutes" . "</tt>";
    $a_Info['date_created'] = file_exists($v_GrantTokenPath) ? date("Y-m-d H:i:s P", filectime($v_GrantTokenPath)) : date("Y-m-d H:i:s P");
    $a_Info['date_modified'] = file_exists($v_GrantTokenPath) ? date("Y-m-d H:i:s P", filemtime($v_GrantTokenPath)) : date("Y-m-d H:i:s P");
    $a_Info['date_expires'] = file_exists($v_GrantTokenPath) ? date("Y-m-d H:i:s P", filemtime($v_GrantTokenPath) + 300) : date("Y-m-d H:i:s P", strtotime(time() + 300));
    echo "<pre>" . json_encode($a_Info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
    echo "<h5>Request (GET):</h5>";
    $a_Request = array();
    $a_Request['client_id'] = $a_CheckFields['client_id'];
    $a_Request['redirect_uri'] = $a_CheckFields['redirect_uri'];
    $a_Request['scope'] = $a_CheckFields['scope'];
    $a_Request['response_type'] = "code";
    $a_Request['access_type'] = "offline";
    $a_Request['prompt'] = "consent";
    $a_Request['state'] = "123";
    echo "<pre>" . json_encode($a_Request, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
    $v_GrantTokenUrl = $v_AuthEndpoint . "?" . http_build_query($a_Request);
    if(isset($_GET['code']))
    {
        echo "<h5>Response (GET):</h5>";
        $a_Response = array();
        $a_Response['state'] = $_GET['state'];
        $a_Response['code'] = $_GET['code'];
        $a_Response['scope'] = $_GET['scope'];
        $a_Response['session_state'] = $_GET['session_state'];
        if($v_GrantTokenPath != "" && $b_DemoMode){
            file_put_contents($v_GrantTokenPath, json_encode($a_Response, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
        }
        foreach($a_Response as $v_Key => $v_Value){
            if($v_Key == "code"){
                $a_Response['code'] = "<tt class='source'>" . $v_Value . "</tt>";
            }
        }
        echo "<pre>" . json_encode($a_Response, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        //
        // 2. get refresh token
        //
        echo "</div><div class='panel'>";
        echo "<h3>2. REFRESH TOKEN...</h3>";
        echo "<h5>Info:</h5>";
        $a_Info = array();
        $a_Info['api']['endpoint'] = $v_TokenEndpoint;
        $a_Info['api']['method'] = "POST";
        $a_Info['api']['header'] = false;
        $a_Info['api']['ssl_check'] = false;
        $a_Info['expires'] = "<tt class='bold'>" . "The refresh token expires after everytime it has been used." . "</tt>";
        $a_Info['date_created'] = file_exists($v_RefreshTokenPath) ? date("Y-m-d H:i:s P", filectime($v_RefreshTokenPath)) : date("Y-m-d H:i:s P");
        $a_Info['date_modified'] = file_exists($v_RefreshTokenPath) ? date("Y-m-d H:i:s P", filemtime($v_RefreshTokenPath)) : date("Y-m-d H:i:s P");
        echo "<pre>" . json_encode($a_Info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Request (POST):</h5>";
        $a_Payload = array();
        $a_Payload['code'] = $_GET['code'];
        $a_Payload['client_id'] = $a_CheckFields['client_id'];
        $a_Payload['client_secret'] = $a_CheckFields['client_secret'];
        $a_Payload['redirect_uri'] = $v_RedirectUri;
        $a_Payload['grant_type'] = "authorization_code";
        $h_Curl=curl_init();
        $a_CurlOptions = array(
            CURLOPT_URL => $v_TokenEndpoint,
            CURLOPT_CUSTOMREQUEST => "POST",
            CURLOPT_POSTFIELDS => $a_Payload,
            CURLOPT_HEADER => 0,
            CURLOPT_VERBOSE => 0,
            CURLOPT_RETURNTRANSFER => 1,
            CURLOPT_SSL_VERIFYPEER => 0,
            CURLOPT_SSL_VERIFYHOST => 0
        );
        curl_setopt_array($h_Curl, $a_CurlOptions);
        $v_RefreshCurl = curl_exec($h_Curl);
        curl_close($h_Curl);
        foreach($a_Payload as $v_Key => $v_Value){
            if($v_Key == "code"){
                $a_Payload['code'] = "<tt class='target'>" . $v_Value . "</tt>";
            }
        }
        echo "<pre>" . json_encode($a_Payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Response (POST):</h5>";
        $a_RefreshResponse = json_decode($v_RefreshCurl, true);
        if($v_RefreshTokenPath != ""){
            file_put_contents($v_RefreshTokenPath, json_encode($a_RefreshResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
        }
        $v_RefreshToken = $a_RefreshResponse['refresh_token'];
        foreach($a_RefreshResponse as $v_Key => $v_Value){
            if($v_Key == "refresh_token"){
                $a_RefreshResponse['refresh_token'] = "<tt class='source'>" . $v_Value . "</tt>";
            }
        }
        echo "<pre>" . json_encode($a_RefreshResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        //
        // 3. get access token
        //
        echo "</div><div class='panel'>";
        echo "<h3>3. ACCESS TOKEN...</h3>";
        echo "<h5>Info:</h5>";
        $a_Info = array();
        $a_Info['api']['endpoint'] = $v_TokenEndpoint;
        $a_Info['api']['method'] = "POST";
        $a_Info['api']['header'] = false;
        $a_Info['api']['ssl_check'] = false;
        $a_Info['expires'] = "<tt class='bold'>" . "in 30 minutes after creation" . "</tt>";
        $a_Info['date_created'] = file_exists($v_AccessTokenPath) ? date("Y-m-d H:i:s P", filectime($v_AccessTokenPath)) : date("Y-m-d H:i:s P");
        $a_Info['date_modified'] = file_exists($v_AccessTokenPath) ? date("Y-m-d H:i:s P", filemtime($v_AccessTokenPath)) : date("Y-m-d H:i:s P");
        $a_Info['date_expires'] = file_exists($v_AccessTokenPath) ? date("Y-m-d H:i:s P", filemtime($v_AccessTokenPath) + 1800) : date("Y-m-d H:i:s P", strtotime(time() + 1800));
        echo "<pre>" . json_encode($a_Info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Request (POST):</h5>";
        $a_Payload = array();
        $a_Payload['refresh_token'] = $v_RefreshToken;
        $a_Payload['client_id'] = $a_CheckFields['client_id'];
        $a_Payload['client_secret'] = $a_CheckFields['client_secret'];
        $a_Payload['redirect_uri'] = $v_RedirectUri;
        $a_Payload['grant_type'] = "refresh_token";
        $h_Curl=curl_init();
        $a_CurlOptions = array(
            CURLOPT_URL => $v_TokenEndpoint,
            CURLOPT_CUSTOMREQUEST => "POST",
            CURLOPT_POSTFIELDS => $a_Payload,
            CURLOPT_HEADER => 0,
            CURLOPT_VERBOSE => 0,
            CURLOPT_RETURNTRANSFER => 1,
            CURLOPT_SSL_VERIFYPEER => 0,
            CURLOPT_SSL_VERIFYHOST => 0
        );
        curl_setopt_array($h_Curl, $a_CurlOptions);
        $v_AccessCurl = curl_exec($h_Curl);
        curl_close($h_Curl);
        $a_AccessResponse = json_decode($v_AccessCurl, true);
        if($v_AccessTokenPath != ""){
            file_put_contents($v_AccessTokenPath, json_encode($a_AccessResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
        }
        foreach($a_Payload as $v_Key => $v_Value){
            if($v_Key == "refresh_token"){
                $a_Payload['refresh_token'] = "<tt class='target'>" . $v_Value . "</tt>";
            }
        }
        echo "<pre>" . json_encode($a_Payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Response (POST):</h5>";
        $v_AccessToken = $a_AccessResponse['access_token'];
        foreach($a_AccessResponse as $v_Key => $v_Value){
            if($v_Key == "access_token"){
                $a_AccessResponse['access_token'] = "<tt class='source'>" . $v_Value . "</tt>";
            }
        }
        echo "<pre>" . json_encode($a_AccessResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        //
        // 4. get connections
        //
        echo "</div><div class='panel'>";
        echo "<h3>4. CONNECTIONS...</h3>";
        echo "<h5>Info:</h5>";
        $a_Info = array();
        $a_Info['api']['endpoint'] = "https://api.xero.com/connections";
        $a_Info['api']['method'] = "GET";
        $a_Info['api']['header'] = true;
        $a_Info['api']['ssl_check'] = false;
        $a_Info['date'] = date("Y-m-d H:i:s P");
        echo "<pre>" . json_encode($a_Info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Request (GET):</h5>";
        $a_Header = array();
        $a_Header[] = "Authorization: Bearer " . $v_AccessToken;
        $a_Header[] = "Content-Type: application/json;charset=UTF-8";
        $h_Curl=curl_init();
        $a_CurlOptions = array(
            CURLOPT_URL => $a_Info['api']['endpoint'],
            CURLOPT_CUSTOMREQUEST => "GET",
            CURLOPT_HEADER => 1,
            CURLOPT_HTTPHEADER => $a_Header,
            CURLOPT_VERBOSE => 0,
            CURLOPT_RETURNTRANSFER => 1,
            CURLOPT_SSL_VERIFYPEER => 0,
            CURLOPT_SSL_VERIFYHOST => 0
        );
        curl_setopt_array($h_Curl, $a_CurlOptions);
        $v_ConnectionCurl = curl_exec($h_Curl);
        $a_Header[0] = "Authorization: Bearer <tt class='target'>" . $v_AccessToken . "</tt>";
        echo "<pre>" . json_encode($a_Header, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Response (GET):</h5>";
        if(stripos($v_ConnectionCurl, '"tenantId"')>=0){
            $v_ConnectionStart = stripos($v_ConnectionCurl, '[');
            $v_ConnectionStr = trim(substr($v_ConnectionCurl, $v_ConnectionStart));
            $a_ConnectionResponse = json_decode($v_ConnectionStr, true);
            foreach($a_ConnectionResponse as $v_Key => $v_Value){
                if($v_Key == "tenantId"){
                    $v_TenantID = $a_ConnectionResponse[0]['tenantId'];
                    $a_ConnectionResponse[0]['tenantId'] = "<tt class='source'>" . $a_ConnectionResponse[0]['tenantId'] . "</tt>";
                }
            }
            echo "<pre>" . json_encode($a_ConnectionResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        }else{
            echo "<pre>" . $v_ConnectionCurl . "</pre>";
        }
        //
        // 5. get data sample (first page of invoices)
        //
        echo "</div><div class='panel'>";
        echo "<h3>5. DATA SAMPLE...</h3>";
        echo "<h5>Info:</h5>";
        $a_Info = array();
        $a_Info['api']['endpoint'] = "https://api.xero.com/api.xro/2.0/Invoices/<tt class='match'>" . $v_ExistingInvoiceID . "</tt>";
        $v_ApiEndpoint = "https://api.xero.com/api.xro/2.0/Invoices/" . $v_ExistingInvoiceID;
        $a_Info['api']['method'] = "GET";
        $a_Info['api']['header'] = true;
        $a_Info['api']['ssl_check'] = false;
        $a_Info['date'] = date("Y-m-d H:i:s P");
        echo "<pre>" . json_encode($a_Info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Request (GET):</h5>";
        $a_Header = array();
        $a_Header[] = "Authorization: Bearer " . $v_AccessToken;
        $a_Header[] = "Accept: application/json";
        $a_Header[] = "Xero-tenant-id: " . $v_TenantID;
        $h_Curl=curl_init();
        $a_CurlOptions = array(
            CURLOPT_URL => $v_ApiEndpoint,
            CURLOPT_CUSTOMREQUEST => "GET",
            CURLOPT_HEADER => 1,
            CURLOPT_HTTPHEADER => $a_Header,
            CURLOPT_VERBOSE => 0,
            CURLOPT_RETURNTRANSFER => 1,
            CURLOPT_SSL_VERIFYPEER => 0,
            CURLOPT_SSL_VERIFYHOST => 0
        );
        curl_setopt_array($h_Curl, $a_CurlOptions);
        $v_DataCurl = curl_exec($h_Curl);
        foreach($a_Header as $v_Value){
            if(stripos($v_Value, "Xero-tenant-id")>=0){
                $a_Header[2] = str_replace("Xero-tenant-id: ","Xero-tenant-id: <tt class='target'>", $v_Value) . "</tt>";
            }
        }
        echo "<pre>" . json_encode($a_Header, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Response (GET):</h5>";
        if(stripos($v_DataCurl, '"ProviderName"')>0){
            $v_DataStart = stripos($v_DataCurl, '{');
            $v_DataStr = trim(substr($v_DataCurl, $v_DataStart));
            $a_DataResponse = json_decode($v_DataStr, true);
            foreach($a_DataResponse as $v_Key => $v_Value){
                if($v_Key == "Invoices"){
                    $a_Invoices = $v_Value;
                    foreach($a_Invoices as $v_Key2 => $v_Value2){
                        if($v_Key2 == "InvoiceID"){
                            $a_DataResponse['Invoices'][0]['InvoiceID'] = "<tt class='match'>" . $a_DataResponse['Invoices'][0]['InvoiceID'] . "</tt>";
                        }
                    }
                }
            }
            echo "<pre>" . json_encode($a_DataResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        }else{
            echo "<pre>" . $v_DataCurl . "</pre>";
        }
        //
        // 6. create sales invoice (receivables)
        //
        echo "</div><div class='panel'>";
        echo "<h3>6. PUSH SALES INVOICE...</h3>";
        echo "<h5>Info:</h5>";
        $a_Info = array();
        $a_Info['api']['endpoint'] = "https://api.xero.com/api.xro/2.0/Invoices";
        $a_Info['api']['method'] = "POST";
        $a_Info['api']['header'] = true;
        $a_Info['api']['ssl_check'] = false;
        $a_Info['date'] = date("Y-m-d H:i:s P");
        echo "<pre>" . json_encode($a_Info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Request (POST):</h5>";
        $a_Header = array();
        $a_Header[] = "Authorization: Bearer " . $v_AccessToken;
        $a_Header[] = "Accept: application/json";
        $a_Header[] = "Xero-tenant-id: " . $v_TenantID;
        echo "<pre>" . json_encode($a_Header, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        $a_Payload = array();
        $a_Payload['Type'] = "ACCREC";
        $a_Payload['Contact']['ContactID'] = $v_YourXeroContactID ;
        $a_Payload['Date'] = date("Y-m-d");
        $a_Payload['DueDate'] = date("Y-m-d", strtotime("+1 day"));
        // sample 1 line item: enter your own description, quantity, unit amount and discount rate
        $a_LineItems = array();
        $a_LineItems[0]['Description'] = "Consulting services as agreed (20% off standard rate)";
        $a_LineItems[0]['Quantity'] = "5";
        $a_LineItems[0]['UnitAmount'] = "199.99";
        $a_LineItems[0]['AccountCode'] = "200";
        $a_LineItems[0]['DiscountRate'] = "10";
        $a_Payload['LineItems'] = $a_LineItems;
        $v_DataCurl = "";
        if($b_CreateInvoice){
            $h_Curl=curl_init();
            $a_CurlOptions = array(
                CURLOPT_URL => $a_Info['api']['endpoint'],
                CURLOPT_HEADER => 1,
                CURLOPT_HTTPHEADER => $a_Header,
                CURLOPT_POST => 1,
                CURLOPT_POSTFIELDS => json_encode($a_Payload),
                CURLOPT_VERBOSE => 0,
                CURLOPT_RETURNTRANSFER => 1,
                CURLOPT_SSL_VERIFYPEER => 0,
                CURLOPT_SSL_VERIFYHOST => 0
            );
            curl_setopt_array($h_Curl, $a_CurlOptions);
            $v_DataCurl = curl_exec($h_Curl);
        }
        echo "<pre>" . json_encode($a_Payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Response (POST):</h5>";
        if($b_CreateInvoice){
            if(stripos($v_DataCurl, '"ProviderName"')>0){
                $v_DataStart = stripos($v_DataCurl, '{');
                $v_DataStr = trim(substr($v_DataCurl, $v_DataStart));
                $a_DataResponse = json_decode($v_DataStr, true);
                foreach($a_DataResponse as $v_Key => $v_Value){
                    if($v_Key == "Invoices"){
                        $a_Invoices = $v_Value;
                        foreach($a_Invoices as $v_Key2 => $v_Value2){
                            if($v_Key2 == "InvoiceID"){
                                $a_DataResponse['Invoices'][0]['InvoiceID'] = "<tt class='match'>" . $a_DataResponse['Invoices'][0]['InvoiceID'] . "</tt>";
                            }
                        }
                    }
                }
                echo "<pre>" . json_encode($a_DataResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
            }else{
                echo "<pre>" . $v_DataCurl . "</pre>";
            }
        }else{
            $a_Info = array();
            $a_Info['MODE'] = "Test";
            $a_Info['Request'] = "Do NOT create invoice";
            echo "<pre>" . json_encode($a_Info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        }
        echo "</div>";
    }
    else
    {
        echo "<br /><form>";
        echo '<input type="button" value="Click here to open App Permissions »" onclick="javascript:top.location.href=\'' . $v_GrantTokenUrl . '\';" class="btn" style="margin-left: 70px;" />';
        echo "</form>
        <ul>
        <li>Login as a Xero User with access to the API</li>
        <li>Read and click on accept to allow the app access.</li>
        </ul><br />";
    }

    // END
    echo "</div><div class='panel'>";
    echo "<h5>Script finished.</h5>";
    echo "<div id='menu_bottom' style='position:normal'><a href='./get_tokens.php' target='_top'>» Start Over</a></div></div>";
    echo "<div id='copyright' style='position:normal'><a href='//joellipman.com/' target='_top'>Copyright © ".date("Y")." Joel Lipman Ltd</a></div></div>";
}else{
    echo "
<div class='panel'><form method='get' action='get_tokens.php'>
    <h3>Configure App</h3>
    <table>
    <tr><td colspan='2' class='centered'><br /><input type='button' value='Open Zoho App Registration' onclick='javascript:openAppConsole();' class='btn' /></td></tr>
    <tr><td colspan='2'> </td></tr>
    <tr><td>Client ID</td><td><input name='client_id' value='" . $v_PresetClientID . "' class='txt' onclick='this.select();' /></td></tr>
    <tr><td>Client Secret</td><td><input name='client_secret' value='" . $v_PresetClientSecret . "' class='txt' onclick='this.select();' /></td></tr>
    <tr><td>Scopes</td><td><input name='scope' value='" . $v_Scope . "' class='txt' onclick='this.select();' /></td></tr>
    <tr><td>Redirect URI</td><td><input name='redirect_uri' value='" . $v_RedirectUri . "' class='txt' onclick='this.select();' /></td></tr>
    <tr><td colspan='2' style='text-align:center;'><input type='submit' name='submit' value='Get Grant Token' class='btn' /></td></tr>
    </table>
</form></div>
<script>
function openAppConsole() {
    var e = document.getElementById('tldSelect');
    var strOption = e.options[e.selectedIndex].value;
    window.open('https://developer.xero.com/myapps', '_blank', 'location=yes,height=600,width=800,scrollbars=yes,status=yes');
}
</script>
";
    echo "<div id='copyright' style='position:fixed'><a href='//joellipman.com/' target='_top'>Copyright © ".date("Y")." Joel Lipman Ltd</a></div>";
}
echo "</body></html>";


Usage:
I've censored any keys and identifying data in the following screenshots but yours will be different values anyway.
  1. Modify the redirect_uri.php file with the web url of your get_tokens.php file.
  2. Modify the get_tokens.php file as follows:
    • Change $b_CreateInvoice to true if you want the script to create an invoice in Xero's Demo Company (leave as "false" if you are still testing authorization and data retrieval).
    • Change $v_RootFolder to a writeable folder on your server where tokens will be stored.
    • Change $v_PresetClientID to the Client ID used for your app (or leave blank and enter it into the HTML form)
    • Change $v_PresetClientSecret to Client Secret for your app (or leave blank and enter into the initial HTML form)
    • Change $v_ExistingInvoiceID to a sample invoice ID in the Xero Demo Company instance (used for testing data retrieval of 1 invoice record). Should be a hexadecimal value like: 7946c88d-d19b-428e-88d3-8622ef656b0c
    • Change $v_YourXeroContactID to a sample customer ID that we will create an invoice for (used for creating 1 invoice record). Should be a hexadecimal value like: 2b818f62-bfb8-4901-9d80-9d6afd152e9c
    • Change $v_RedirectURI to the web URL of your redirect_uri.php file (can be overwritte in the HTML form)
    • Change $v_Scope if you want but the current value as per this script is enough to carry out this demonstration.
  3. Upload both scripts to the same folder on your server.
  4. Browse to the get_tokens.php file with your Xero login profile.
  5. Enter your Client ID and Client Secret in the initial HTML form and submit the form by clicking on "Get Grant Token":
    App Usage Step 1
  6. Click on the button "Click here to open app permissions":
    App Usage Step 2
  7. Xero will now ask you to allow your app to access data:
    App Usage Step 3
  8. After which you will be redirected back to our script:
    App Usage Step 4
    Legend:
    1. This panel displays what was submitted in the initial HTML form.
    2. This panel displays what was sent to get the Grant Token (or CODE variable) and highlight in black the value we will use for the next request.
    3. This panel displays our request to get a Refresh Token (highlighted in black).
    4. This panel displays our request to get an Access Token (highlighted in black).
    5. This panel displays our connection request to get the TenantID (highlighted in black).
    6. Using both the Access Token and the Tenant ID, we can retrieve 1 invoice record.
    7. Using both the Access Token and the Tenant ID, we can push an invoice into Xero.

Additional Note(s):
  • You may note that I haven't followed the documentation when it comes to including the Client ID and Client Secret into the header (base64encode). Maybe this will be necessary at some point but I had adapted the above script to Xero from Zoho and it worked... So the access token is added to the header and a standard Authorization Bearer request.
  • The initial HTML form in get_tokens.php will create a file called grant_info.dat in your token folder. This is used just to cache the HTML values you submitted. You should not have 2 developers using this same script, same app in the same Xero instance at the same time as only 1 grant_info.dat file is created and overwritten whenever the script is executed.
  • As of today 10th January 2020, Xero still have not resolved the issue that the refresh_token expires immediately after use and is overwritten with another one. You will need to store that one but everytime you use it and generate another access/refresh token, you will need to store the new value in a file for later use.
  • Xero requires this is done over the HTTPS protocol (HTTP Secure). However I have tested this on client's servers that do not have SSL. Run the script and when prompted that the target site is not secure, click on "Advanced" and "Proceed Anyway".

Development/Testing/Demo Company to LIVE/Production/Your Company
So if you followed the above instructions the app you have just made will create invoices in the "Demo Company" instance of the client's Xero Instance. Let's say you've made it all work swimmingly and tested thoroughly, you will now want to push the invoices to the client's LIVE Xero instance. The process is quite simple, you need to create a new app and when authorizing with a Xero login, you will be asked which company you want the app to access as per the following screenshot:
From Demo Company to Production


Source(s):

Related Articles

Joes Revolver Map

Joes Word Cloud

following   database   joomla   search   version   solution   name   example   need   table   system   select   code   parameter   added   error   list   website   used   value   could   using   content   where   date   uploaded   site   time   file   windows   server   note   would   display   zoho   source   mysql   program   script   work   data   find   license   user   files   page   report   google   first   copy   JoelLipman.Com

Accreditation

Badge - Certified Zoho Creator Associate
Badge - Certified Zoho Creator Associate

Donate & Support

If you like my content, and would like to support this sharing site, feel free to donate using a method below:

Paypal:
Donate to Joel Lipman via PayPal

Bitcoin:
Donate to Joel Lipman with Bitcoin - Valid till 8 May 2022 3QnhmaBX7LQSRsC9hh6Je9rGQKEGNQNfPb
© 2021 Joel Lipman .com. All Rights Reserved.