Search form

PHP: Implementing the Amazon Product Advertising API

This is the code for my basic implementation of the Amazon Product Advertising API.

Update 9/2016: Switched to using ASIN due to too many conflicts in ISBN/EISBN numbers. Both methods are provided below.

When I finish each book, I add its ASIN or ISBN-10 number to my PHP script. The script then performs an ItemLookup request against Amazon's Godzilla-sized products database, and Amazon returns each book's information in XML format, and finally my script iterates over the results and prints them in a simple table.

In order to run an ItemLookup you need to set up an Amazon Associates account and then register to use the Product Advertising API. Requests and results are exchanged here via REST (the onca/xml URI), although Amazon also supports SOAP (onca/soap) for SOAP/WSDL environments.

PHP's basic built-in SimpleXML library works fine for processing the results, although you can certainly use DOMDocument or an equivalent; both SimpleXML and DOMDocument are based on libxml so their behavior is similar. Finally, it's worth reading over the documentation regarding request limits to understand how Amazon throttles incoming traffic.

Performance and Accuracy Concerns

Rather than performing a separate request for each item you want to display, it's better to consolidate your requests into batch jobs or requests for multiple item IDs. Here I'm performing up to 10 (the maximum allowed) ItemLookup operations per request. Since this cuts the number of requests by up to 90%, your product page will load much faster, and you're much less likely to see Amazon's 503 errors caused by overrunning the per-second request limit.

Another issue here is that querying by ISBN sometimes returns multiple editions of a given book, including Kindle editions, so that instead of the expected 10 results you may receive dozens. Kindle editions with blank ISBN fields are included by default, so if you don't want the duplicates you need to exclude them somehow. One workaround for this is to query by Amazon's unique identifier (ASIN).

Another workaround is to filter the returned XML to eliminate <Item> tags with blank or duplicate ISBN numbers. I use the latter method with a simple trick where I place the returned ISBNs in an array and then test the subsequent item ISBNs against the array contents to skip iterations of duplicates. One downside to this approach is that the non-duplicate may not be the desired edition of a given book. Amazon prioritizes the editions of books that are in-print and more likely to sell -- that is the point of the Product Advertising system, after all -- so if you're merely attempting to sell as many items as possible this is probably a workable solution. I'm trying to display the book I actually read here, though, so I may need to adjust the results further or switch to ASIN.

Here's some sample XML returned by an ItemLookup:

<?xml version="1.0" ?>
<ItemLookupResponse
    xmlns="http://webservices.amazon.com/AWSECommerceService/2011-08-01">
    <OperationRequest>
        <HTTPHeaders>
            <Header Name="UserAgent" Value="Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36"></Header>
        </HTTPHeaders>
        <RequestId>aff5de19-dbac-44f8-84d9-b901111f7f2f</RequestId>
        <Arguments>
            <Argument Name="AWSAccessKeyId" Value="ackThbbbt"></Argument>
            <Argument Name="AssociateTag" Value="geofstra-20"></Argument>
            <Argument Name="IdType" Value="ISBN"></Argument>
            <Argument Name="ItemId" Value="0802779239"></Argument>
            <Argument Name="Operation" Value="ItemLookup"></Argument>
            <Argument Name="ResponseGroup" Value="Images,ItemAttributes,Offers"></Argument>
            <Argument Name="SearchIndex" Value="Books"></Argument>
            <Argument Name="Service" Value="AWSECommerceService"></Argument>
            <Argument Name="Timestamp" Value="2016-03-29T23:45:28.000Z"></Argument>
            <Argument Name="Signature" Value="ackThbbbt"></Argument>
        </Arguments>
        <RequestProcessingTime>0.0327330000000000</RequestProcessingTime>
    </OperationRequest>
    <Items>
        <Request>
            <IsValid>True</IsValid>
            <ItemLookupRequest>
                <IdType>ISBN</IdType>
                <ItemId>0802779239</ItemId>
                <ResponseGroup>Images</ResponseGroup>
                <ResponseGroup>ItemAttributes</ResponseGroup>
                <ResponseGroup>Offers</ResponseGroup>
                <SearchIndex>Books</SearchIndex>
                <VariationPage>All</VariationPage>
            </ItemLookupRequest>
        </Request>
        <Item>
            <ASIN>0802779239</ASIN>
            <DetailPageURL>http://www.amazon.com/Maos-Great-Famine-Devastating-Catastrophe/dp/0802779239%3FSubscriptionId%3DAKIAIBUBR6FVJSMAIMMA%26tag%3Dgeofstra-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D165953%26creativeASIN%3D0802779239</DetailPageURL>
            <ItemLinks>
                <ItemLink>
                    <Description>Technical Details</Description>
                    <URL>http://www.amazon.com/Maos-Great-Famine-Devastating-Catastrophe/dp/tech-data/0802779239%3FSubscriptionId%3DAKIAIBUBR6FVJSMAIMMA%26tag%3Dgeofstra-20%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D0802779239</URL>
                </ItemLink>
etc.

Here's the PHP for querying/rendering by ASIN:

<?php

$booksarray = array(
       array("0375719334","2/2016",4),   // Counter-Clock
       array("0394856228","2/2016",3),   // Hitchcock Supernatural
       array("1857988809","1/2016",3),   // Second Variety
       array("0806511532","1/2016",3),   // Short Happy Life Brown Oxford
       array("0806513284","1/2016",4),   // Eye of the Sibyl
       array("0806512768","1/2016",3),   // Collected Stories of PKD Vol 4
       array("B001BF44PA","1/2016",3),   // PKD Reader
       array("B008P96TI0","1/2016",4)    // Collected Stories of PKD
);

$querystrings = array('');
$params = array();
$pairs = array();
$signed_urls = array();
$returned_asins = array();

// Your AWS Access Key ID, as taken from the AWS Your Account page
$aws_access_key_id = "ackThbbtt";

// Your AWS Secret Key corresponding to the above ID, as taken from the AWS Your Account page
$aws_secret_key = "double-secret-ackThbbtt";

// The region you are interested in
$endpoint = "webservices.amazon.com";

$uri = "/onca/xml";

// Build an array of ASINs, in sets of 10, as strings with comma separators
$j = 0;
for ($i = 0; $i < count($booksarray); $i++) {
    if ($i > 0 && $i % 10 == 0) {
        // Element index is divisible by 10, so we need a new array element
        array_push($querystrings,''); // get rid of undefined offset warning
        $j++;
    }
    $querystrings[$j] .= $booksarray[$i][0].',';
}

// Build query strings using comma-separated $querystrings
for ($k = 0; $k < count($querystrings); $k++) {
    array_push($params, array(
        "Service" => "AWSECommerceService",
        "Operation" => "ItemLookup",
        "AWSAccessKeyId" => $aws_access_key_id,
        "AssociateTag" => "geofstra-20",
        "ItemId" => rtrim($querystrings[$k],','),
        "IdType" => "ASIN",
        "ResponseGroup" => "Images,ItemAttributes",
        )
    );
}

//print_r($params);

// Sign each query string
foreach ($params as $param) {
    // Set current timestamp if not set
    if (!isset($param["Timestamp"])) {
        $param["Timestamp"] = gmdate('Y-m-d\TH:i:s\Z');
    }
    
    // Sort the parameters by key
    ksort($param);

    $pairs = array();

    foreach ($param as $key => $value) {
        array_push($pairs, rawurlencode($key)."=".rawurlencode($value));
    }

    // Generate the canonical query
    $canonical_query_string = join("&", $pairs);

    // Generate the string to be signed
    $string_to_sign = "GET\n".$endpoint."\n".$uri."\n".$canonical_query_string;

    // Generate the signature required by the Product Advertising API
    $signature = base64_encode(hash_hmac("sha256", $string_to_sign, $aws_secret_key, true));

    // Generate the signed URL
    $request_url = 'http://'.$endpoint.$uri.'?'.$canonical_query_string.'&Signature='.rawurlencode($signature);

    //echo "<p>Signed URL: \"".$request_url."\"</p>";

    array_push($signed_urls, $request_url);
}

echo '<table class="booklist">';
echo '<tr>';
echo '<th class="bookdetails">Title/Author/Publisher/ISBN</th>';
echo '<th>Buy</th>';
echo '<th>Pages</th>';
echo '<th>Date Completed</th>';
echo '<th>Rating (of 5)</th>';
echo '</tr>';

// Iterate over the <item> tags in each set of 10 results, print non-duplicates
$j = 0; // counter to keep track of valid items
foreach ($signed_urls as $signed_url) {
  
    $xmldoc = file_get_contents("$signed_url");
    $xml = new SimpleXMLElement($xmldoc);
    $count = $xml->Items->Item->count();
    for ($i = 0; $i < $count; $i++) { 

        $cur_isbn = "";
        // Cast this to string, otherwise you get an array of SimpleXML objects
        $cur_asin = (string) $xml->Items->Item[$i]->ASIN;
        //echo $cur_asin . " ";

        // If ASIN isn't blank or duplicate, print the book's book's details 
        if ($cur_asin && !(in_array($cur_asin, $returned_asins))) {
            
            // Determine ISBN, if possible    
            if (empty($xml->Items->Item[$i]->ItemAttributes->ISBN)) {
                $cur_isbn = $xml->Items->Item[$i]->ItemAttributes->EISBN;
            } else {
                $cur_isbn = $xml->Items->Item[$i]->ItemAttributes->ISBN;
            }

            echo '<tr>';
            echo '<td class="booktitle"><strong>Title: </strong>'.$xml->Items->Item[$i]->ItemAttributes->Title;
            echo '<br /><strong>Author: </strong>'.$xml->Items->Item[$i]->ItemAttributes->Author;
            echo '<br /><strong>Publisher: </strong>'.$xml->Items->Item[$i]->ItemAttributes->Publisher;
            echo '<br /><strong>ISBN: </strong>'.$cur_isbn.'</td>';
            echo '<td><a href="'.$xml->Items->Item[$i]->DetailPageURL.'">'.
                   '<img src="'.$xml->Items->Item[$i]->MediumImage->URL.'" /></a></td>';
            echo '<td class="pages">'.$xml->Items->Item[$i]->ItemAttributes->NumberOfPages.'</td>';
            echo '<td class="date">'.$booksarray[$j][1].'</td>';
            echo '<td class="rating">'.$booksarray[$j][2].'</td>';
            echo '</tr>';
            array_push($returned_asins, $cur_asin);
            $j++;
        }
    }
}

?>

And here's the PHP I use for rendering by ISBN-10:

<?php

$booksarray = array(
       array("014018726X","3/2016",2.5),
       array("0307450767","2/2016",3),
       array("1560255021","2/2016",4),
       array("0375719334","2/2016",4),
       array("0394826760","2/2016",3),
       array("1857988809","1/2016",3),
       array("0806511532","1/2016",3),
       array("0806513284","1/2016",4),
       array("0806512768","1/2016",3),
       array("0806518561","1/2016",3),
       array("0375421513","1/2016",4)
);

$querystrings = array('');
$params = array();
$pairs = array();
$signed_urls = array();
$returned_asins = array();

// Your AWS Access Key ID, as taken from the AWS Your Account page
$aws_access_key_id = "ackThbbtt";

// Your AWS Secret Key corresponding to the above ID, as taken from the AWS Your Account page
$aws_secret_key = "double-secret-ackThbbtt";

// The region you are interested in
$endpoint = "webservices.amazon.com";

$uri = "/onca/xml";

// Build an array of ISBNs, in sets of 10, as strings with comma separators
$j = 0;
for ($i = 0; $i < count($booksarray); $i++) {
    if ($i > 0 && $i % 10 == 0) {
        // Element index is divisible by 10, so we need a new array element
        array_push($querystrings,''); // get rid of undefined offset warning
        $j++;
    }
    $querystrings[$j] .= $booksarray[$i][0].',';
}

// Build query strings using comma-separated $querystrings
for ($i = 0; $i < count($querystrings); $i++) {
    array_push($params, array(
        "Service" => "AWSECommerceService",
        "Operation" => "ItemLookup",
        "AWSAccessKeyId" => $aws_access_key_id,
        "AssociateTag" => "geofstra-20",
        "ItemId" => rtrim($querystrings[$i],','),
        "IdType" => "ISBN",
        "ResponseGroup" => "Images,ItemAttributes",
        "SearchIndex" => "Books"
        )
    );
}

// Sign each query string
foreach ($params as $param) {
    // Set current timestamp if not set
    if (!isset($param["Timestamp"])) {
        $param["Timestamp"] = gmdate('Y-m-d\TH:i:s\Z');
    }
    
    // Sort the parameters by key
    ksort($param);

    foreach ($param as $key => $value) {
        array_push($pairs, rawurlencode($key)."=".rawurlencode($value));
    }

    // Generate the canonical query
    $canonical_query_string = join("&", $pairs);

    // Generate the string to be signed
    $string_to_sign = 'GET\n'.$endpoint.'\n'.$uri.'\n'.$canonical_query_string;

    // Generate the signature required by the Product Advertising API
    $signature = base64_encode(hash_hmac('sha256', $string_to_sign, $aws_secret_key, true));

    // Generate the signed URL
    $request_url = 'http://'.$endpoint.$uri.'?'.$canonical_query_string.'&Signature='.rawurlencode($signature);

    //echo '<p>Signed URL: '.$request_url.'</p>';

    array_push($signed_urls, $request_url);
}

echo '<table class="booklist">';
echo '<tr>';
echo '<th class="bookdetails">Title/Author/Publisher/ISBN</th>';
echo '<th>Buy</th>';
echo '<th>Pages</th>';
echo '<th>Date Completed</th>';
echo '<th>Rating (of 5)</th>';
echo '</tr>';

// Iterate over the <item> tags in each set of 10 results, print non-duplicates
$j = 0; // Counter to keep track of valid items
foreach ($signed_urls as $signed_url) {
    $xmldoc = file_get_contents("$signed_url");
    $xml = new SimpleXMLElement($xmldoc);
    $count = $xml->Items->Item->count();
    for ($i = 0; $i < $count; $i++) { 
        // Cast this to string, otherwise you get an array of SimpleXML objects
        $cur_isbn = (string) $xml->Items->Item[$i]->ItemAttributes->ISBN;

        // If ISBN isn't blank or duplicate, print the book's details
        if ($cur_isbn && !(in_array($cur_isbn, $returned_isbns))) {
            echo '<tr>';
            echo '<td class="booktitle"><strong>Title: </strong>'.$xml->Items->Item[$i]->ItemAttributes->Title;
            echo '<br /><strong>Author: </strong>'.$xml->Items->Item[$i]->ItemAttributes->Author;
            echo '<br /><strong>Publisher: </strong>'.$xml->Items->Item[$i]->ItemAttributes->Publisher;
            echo '<br /><strong>ISBN: </strong>'.$xml->Items->Item[$i]->ItemAttributes->ISBN.'</td>';
            echo '<td><a href="'.$xml->Items->Item[$i]->DetailPageURL.'">'.
                   '<img src="'.$xml->Items->Item[$i]->MediumImage->URL.'" /></a></td>';
            echo '<td class="pages">'.$xml->Items->Item[$i]->ItemAttributes->NumberOfPages.'</td>';
            echo '<td class="date">'.$booksarray[$j][1].'</td>';
            echo '<td class="rating">'.$booksarray[$j][2].'</td>';
            echo '</tr>';
            array_push($returned_isbns, $cur_isbn);
            $j++;
        }
    }
}

?>

Categories: