ThatSoftware<Dude>

Musings of a .NET Developer, CTO, Tech Enthusiasts

If you have an Amazon Affiliate account then at some point in your life you're probably going to be tired of managing products manually and will want a more automated approach, like I did on Laptop-Info.com.

Lucky for us we have the Amazon Product Advertising API to help us out. Unlucky for us, it is not that simple to incorporate into out projects. Dozens of code samples and hours of research later, I've gotten it to work. If you can see it on an Amazon product page, then you can probably grab it through the API. Amazon offers pretty extensive documentation on how to use the API, but no examples really on how to code it particularly not in C#, so I figured I'd make one since I'm the process of integrating the API onto my websites. I won't cover anything too in depth or complicated, but this should get someone who's new to the API up and running and querying product data in no time.

Documentation was on the low side, at least for C# and so it took some Googling to realize that I needed to just read the 200 page documentation which you can find right here. Had I just jumped on this from the start it would of been a much easier task. However, if you don't feel like reading the 200 pages, then feel free to continue reading here as I've basically just broken the process down as simply as I can.

For this example, I'm going to be making a REST request, but if you so wish you can also use SOAP, which is expained further in the documentation. So let's get to it shall we.

A Sample REST Request

When you make an API call to Amazon, this is what it will look like. Most of the request is self-explanatory, but the tricky part comes when signing the request in order for it to be authenticated by Amazon. Amazon requires that your request be in a certain format and ordered in a certain way before it is encrypted in order to sign your request.

http://webservices.amazon.com/onca/xml?
Service=AWSECommerceService&
AWSAccessKeyId=[Access Key ID]&
AssociateTag=[ID]&
Operation=ItemSearch&
SearchIndex=Apparel&
Keywords=Shirt&
ResponseGroup=Medium&
Timestamp=[YYYY-MM-DDThh:mm:ssZ]&
Signature=[Request Signature]

REST Parameter Breakdown

  • http://webservices.amazon.com/onca/xml
  • Service=AWSECommerceService
  • AWSAccessKeyId
  • AssociateTag
  • Operation
  • Operation Parameters

A few parameters in the request are constant and only require the proper ID's assigned to them, however they MUST be in your request. If you don't have the proper AWS credentials, then sign up for them here. You'll also need your Amazon Associate Tag to continue. So set that up, and you'll be good to go, so read on.

The AWSAccessKey ID is required as it helps identify the request submitter, ie YOU. You receive an AWS Access Key ID when you sign up with Product Advertising API. The other identifier, AssociateTag, is also required. It is an ID for an Associate, obviously stated. If you are an Associate, you must include your Associate tag in each request to be eligible to receive a referral fee for a customer's purchase. You can sign up for free right here.

You must also specify an Operation in your request. This is the name of the function/method/procedure that we're requesting Amazon to provide data for. The following is a list of the current valid operations.

  • CartModify
  • ItemLookup
  • ItemSearch
  • SimilarityLookup
  • BrowseNodeLookup
  • CartAdd
  • CartClear
  • CartCreate
  • CartGet

For this tutorial we'll be using the ItemLookup operation.

Operation parameters are parameters that the operation selected requires and/or are optional. In the example above, the operation parameters for ItemSearch would be SearchIndex and Keywords. Each operation has their own set of parameters and some can even appear multiple times per request.

These are referred to as compound parameters. In this case you specify the multiple parameters by their name (.) and a sequential number starting with 1, for example.


Item.1=1234&
Item.2=2345

In our request this would product multiple items coming back in the response. Note that only some parameters allow this however.


A Few Things To Keep In Mind

  • Parameter names and values are case sensitive.

    Searchindex=Apparel
    SearchIndex=apparel
    
    These two are different parameters, and would both give errors. Amazon follows the standard of having the first letter of each word capitalized. Very important.

  • Parameter values must be URL-encoded.

ResponseGroup

ResponesGroup is an optional parameter for all API operations. It specifies which information should be returned by the request. There are default values that you can specify however, such as:

  • Small
  • Medium
  • Large

Each one of those respectively returns a larger dataset. For the most part you'll want to set your own custom values. When getting product pricing I noticed I need the Large dataset, which adds considerable bulk to each response. However, by setting the ResponseGroup to Offers, I was able to just retrieve the data that I wanted. The full list of ResponseGroup values are in the full documentation.

For someone new to the API, Medium should be just enough in order to get a basic idea of how Amazon returns product data.


AWS Authentication

Authenticating your request will take up about 80% of your time here. Amazon is very specific as to how it want's it's requests encoded and encrypted, and one row in the wrong order will lead to an invalid request. Any messages sent to the Product Advertising API that are not authenticated will be denied. In order to authenticate your request you will need your AWS Identifiers. When you create an AWS account, AWS assigns you a pair of related identifiers:

  • Access Key ID (a 20-character, alphanumeric sequence) For example: AKIAIOSFODNN7EXAMPLE
  • Secret Access Key (a 40-character sequence) For example: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
* Note these are made up...but they will look something like that.

Note that recent policy changes have made it so that you can no longer retrieve your Secret Access Key from AWS, so jot it down and don't lose it. But you can always just delete that particular key and request a new one if that does happen.


Authentication Parameters

We'll touch upon the last two parameters in our original example above:

Timestamp
Signature

Signature - Required. A signature is created by using the request type, domain, the URI, and a sorted string of every parameter in the request (except the Signature parameter itself). Once this is properly formatted, you create a base64-encoded HMAC_SHA256 signature using your AWS secret key and the result. This is the signature that you will submit with your request. Sounds easy, I know.

Timestamp — Required. The time stamp you use in the request must be a dateTime object, with the complete date plus hours, minutes, and seconds. This is a fixed -length subset of the format defined by ISO 8601, represented in Universal Time (GMT): YYYY-MM-DDThh:mm:ssZ (where T and Z are literals).


Create the Signature Parameter

The basic steps for creating the signature for our request is as follows:
1. Create a request without the signature.
2. Create your hmac-sha signature based on that request and your secret key from AWS.
3. Attach your signature to your request and you're good to go

In order to accomplish that we must do the following.


Create Canonicalized String

1. Sort the querystring components by parameter name using natural byte ordering. Note that this is NOT alphabetical ordering, as lowercase parameters will appear after uppsercase. Also note that so far all parameters start with a capital letter, but this may change at some point so better safe than sorry.
2. Url encode the parameter name and values with percent encoding.
3. Do not encode any unreserved characters that RFC 3986 defines such as:

  • Uppercase and lowercase characters
  • Numeric characters
  • hyphen ( - )
  • period ( . )
  • underscore ( _ )
  • tilde ( ~ )

4. Percent encode extended utf-8 characters in the form %xy%za
5. Percent encode the space character as %20
6. Percent encode all other characters with %xy, hex values

The final string to sign should follow the following pseudo code, with the canonicalized string we just created getting appended to the end.

string HTTPVerb = "GET";
string ValueOfHostHeaderInLowercase = "webservices.amazon.com";
string HTTPRequestURI = “/onca/xml”;
  
StringToSign = HTTPVerb + "\n" +
ValueOfHostHeaderInLowercase + "\n" +
HTTPRequestURI + "\n" +
CanonicalizedQueryString (from the preceding step)

Calculate The Signature

Calculate an RFC 2104-compliant HMAC with the string you just created, your Secret Access Key as the key, and SHA256 as the hash algorithm. This various depending on programming framework. Down below you can see a simple C# implementation, but the same can be done in Java and PHP.


Convert the Resulting Value to Base64

You then use the resulting value as the value of the Signature request parameter and you are done.

There's tons more information to learn in order to use the API more effectively, such as all the different operations and parameters and nuances in such. However, getting the code out of the way makes it a much easier task to complete. The only thing that needs to change now is your request parameters.


The Code

To help facilitate future API calls we will be wrapping the encoding and encryption work into its own helper class.


public class SignedRequestHelper
{
    private string strAccessKeyId = string.Empty;
    private string strEndpoint = string.Empty;
    private string strAssociateTag = string.Empty;
    private byte[] strSecret;
    private const string REQUEST_URI = "/onca/xml";
    private const string REQUEST_METHOD = "GET";
    private HMAC signer;

    public SignedRequestHelper(string strAccessKeyId, string strSecret, string strEndpoint, string strAssociateTag)
    {
        this.strEndpoint = strEndpoint;
        this.strAccessKeyId = strAccessKeyId;
        this.strSecret = Encoding.UTF8.GetBytes(strSecret);
        this.strAssociateTag = strAssociateTag;
        this.signer = new HMACSHA256(this.strSecret);
    }

    // this will sign our request and create our signature
    public string Sign(IDictionary<string, string> request)
    {
        // Use a SortedDictionary to get the parameters in naturual byte order, as
        // required by AWS.
        ParamComparer pc = new ParamComparer();
        SortedDictionary<string, string> sortedMap = new SortedDictionary<string, string>(request, pc);

        // Add the AWSAccessKeyId and Timestamp to the requests.
        sortedMap["AWSAccessKeyId"] = strAccessKeyId;
        sortedMap["AssociateTag"] = strAssociateTag;
        sortedMap["Timestamp"] = this.GetTimestamp();

        // Get the canonical query string
        string canonicalQS = this.ConstructCanonicalQueryString(sortedMap);

        // Derive the bytes needs to be signed.
        StringBuilder builder = new StringBuilder();
        builder.Append(REQUEST_METHOD)
            .Append("\n")
            .Append(strEndpoint)
            .Append("\n")
            .Append(REQUEST_URI)
            .Append("\n")
            .Append(canonicalQS);

        string stringToSign = builder.ToString();
        byte[] toSign = Encoding.UTF8.GetBytes(stringToSign);

        // Compute the signature and convert to Base64.
        byte[] sigBytes = signer.ComputeHash(toSign);
        string signature = Convert.ToBase64String(sigBytes);

        // now construct the complete URL and return to caller.
        StringBuilder qsBuilder = new StringBuilder();
        qsBuilder.Append("http://")
            .Append(strEndpoint)
            .Append(REQUEST_URI)
            .Append("?")
            .Append(canonicalQS)
            .Append("&Signature=")
            .Append(this.PercentEncodeRfc3986(signature));

        return qsBuilder.ToString();
    }

    // Current time in IS0 8601 format as required by Amazon
    private string GetTimestamp()
    {
        DateTime currentTime = DateTime.UtcNow;
        string timestamp = currentTime.ToString("yyyy-MM-ddTHH:mm:ssZ");
        return timestamp;
    }

    // 3986 percent encode string
    private string PercentEncodeRfc3986(string str)
    {
        str = HttpUtility.UrlEncode(str, System.Text.Encoding.UTF8);
        str = str.Replace("'", "%27").Replace("(", "%28").Replace(")", "%29").Replace("*", "%2A").Replace("!", "%21").Replace("%7e", "~").Replace("+", "%20");

        StringBuilder sbuilder = new StringBuilder(str);
        for (int i = 0; i < sbuilder.Length; i++)
        {
            if (sbuilder[i] == '%')
            {
                if (Char.IsLetter(sbuilder[i + 1]) || Char.IsLetter(sbuilder[i + 2]))
                {
                    sbuilder[i + 1] = Char.ToUpper(sbuilder[i + 1]);
                    sbuilder[i + 2] = Char.ToUpper(sbuilder[i + 2]);
                }
            }
        }
        return sbuilder.ToString();
    }

    // Consttuct the canonical query string from the sorted parameter map.
    private string ConstructCanonicalQueryString(SortedDictionary<string, string> sortedParamMap)
    {
        StringBuilder builder = new StringBuilder();

        if (sortedParamMap.Count == 0)
        {
            builder.Append("");
            return builder.ToString();
        }

        foreach (KeyValuePair<string, string> kvp in sortedParamMap)
        {
            builder.Append(PercentEncodeRfc3986(kvp.Key));
            builder.Append("=");
            builder.Append(PercentEncodeRfc3986(kvp.Value));
            builder.Append("&");
        }
        string canonicalString = builder.ToString();
        canonicalString = canonicalString.Substring(0, canonicalString.Length - 1);
        return canonicalString;
    }
}

// To help the SortedDictionary order the name-value pairs in the correct way.
class ParamComparer : IComparer<string>
{
    public int Compare(string p1, string p2)
    {
        return string.CompareOrdinal(p1, p2);
    }
}

You can copy this class as is, as it is generic enough. And the only thing left is to create your request and sign it. You can do that as follows:


private void MakeRequest()
{
    SignedRequestHelper helper = new SignedRequestHelper("AWSAccessKey", "AWSSecret", "AWSEndpoint", "AssociateTag");
    IDictionary<string, string> r1 = new Dictionary<string, String>();
    r1["Service"] = "AWSECommerceService";
    r1["Operation"] = "ItemLookup";
    r1["ItemId"] = ASIN;
    r1["ItemType"] = "ASIN";
    r1["ResponseGroup"] = "Offers";
    r1["Version"] = "2009-01-06";

    string strRequestUrl = helper.Sign(r1);
    string price= GetPrice(strRequestUrl);
}

We'll keep our request simple. We're doing a ItemLookup with the ID matching the ASIN passed in and we're asking Amazon to return us the Offers ResponseGroup. This function just creates and signs our request. The actual call is made in the GetPrice() function:


private string GetPrice(string url)
{
    string strResult = string.Empty;

    try
    {
        WebRequest request = HttpWebRequest.Create(url);
        WebResponse response = request.GetResponse();
        XmlDocument doc = new XmlDocument();
        doc.Load(response.GetResponseStream());

        XmlNodeList errorMessageNodes = doc.GetElementsByTagName("Message");
        if (errorMessageNodes != null && errorMessageNodes.Count > 0)
        {
            String message = errorMessageNodes.Item(0).InnerText;
            return "Error: " + message + " (but signature worked)";
        }

        // custom to whatever ResponseGroup you chose
        XmlNodeList el = doc.GetElementsByTagName("Price");
        XmlNode node = el[0];
        XmlNode node2 = node["FormattedPrice"];
        string price = node2.InnerText;
        response.Close();
            
        return price;
    }

    catch (Exception ex)
    {
        return ex.Message;
    }

    return strResult;
}


At this stage, you can really do whatever you need to do with the XmlDocument above. I'd suggest requesting a Large ResponseGroup and then taking a look at the full XML being returned.

It's been a long and arduous road, but we've made it. We made an Amazon API call and returned the price of a product. But at least now we won't have to worry about it anymore. Like ever. Until a future API update that is. The SignedRequestHelper class does all of the heavy lifting for us, all we have to do is specify what we want, and then parse that data accordingly.

I hope that this helped anyone out there looking to implement Amazon's API and is having trouble finding a solution online like I was. Happy coding!

"sometimes you have to delete, to find your answer"
Copyright © 2017 ThatSoftwareDude.com
humans.txt