Mini Tutorial: The Amazon Marketplace Web Service (MWS) for .NET

For search engines… there’s google.
For buying “stuff” online… there’s Amazon.

Amazon has become synonymous with the web.  A leading force in the eCommerce world.  When a consumer looks to make an online purchase, most will start with Amazon. Luckily, Amazon has a robust set of tools to programatically help you with getting your warehouse up and running.  Unfortunately, it’s rather difficult, and rather slow (we’ll touch on that later).  I guess that’s to be expected if you’ve got millions of companies constantly hitting their servers to update their products, inventories, and prices.

I was amazed at the lack of documentation, and help in general about using their API.  Left to my own devices, I concocted my own set of .NET tools to make uploading to Amazon relatively painless (although patience is still required).

Getting Started

The first step is to download the XSD schema files for what Amazon expects when you send feeds off to them.  The Schema files can be found on the Amazon MWS Documentation Page under the title “Selling on Amazon Guide to XML“.

Amazon has already made this difficult, no?

Next – Convert the XSD to a class that you can use to code with in your project.  For more information on how to do it.  Check out the XML Schema Definition Tool page as a starting point.

Now that we’ve got our Amazon classes, it’s time to start putting values into it.

First off, add the following using statements:

using MarketplaceWebService;
using MarketplaceWebService.Model;
using MarketplaceWebServiceProducts;
using MarketplaceWebServiceProducts.Model;

For everyone Amazon storefront enabled with MWS, there are 4 different keys that tell Amazon EXACTLY which store you’re uploading data to. These are:

– AccessKeyId
– SecretAccessKeyId
– MerchantId
– MarketplaceId

You’ll need all four of these keys, and you will have them e-mailed to you once you’ve successfully signed up your store front for Amazon MWS.

First here’s a typical Amazon page, shortened to show only what we’re currently affecting

I’ll quickly brisk through a few of the key fields, and the fields I’ve had to use so far that I’ve found troublesome.  All fields are passed in as strings unless otherwise noted.

amazonProduct.Condition = new ConditionInfo { ConditionType = ConditionType.New },

In some ways, Amazon made building data to feed through their API rather easy.  Many fields are enumerated leaving very little room for error.  ConditionType on it’s own has
11 different enumerations to choose from.

amazonProduct.ExternalProductUrl

If you have your own e-commerce website already, pass in the products Url to Amazon!  This doesn’t affect what your consumers do at all, it’s used mainly for management in your backend.

amazonProduct.SKU

Your store/companies own personal Sku for that product.
(1)

amazonProduct.StandardProductID = new StandardProductID();

This is a necessary field and could be the cause of headaches if you don’t assign this field correctly.
There are two properties you must push through to StandardProductId, the first is Type which is an enumeration, your options are:

StandardProductIdType.EAN
StandardProductIdType.ASIN
StandardProductIdType.GTIN
StandardProductIdType.ISBN
StandardProductIdType.UPC

These are all standardized identifier types. (Excluding ASIN to an extent) and using these is paramount.  For one it’s required and Amazon will reject your feeds if you don’t fill out the StandardProductId Property.  Two – it helps with price comparisons for your consumers, and when you get far enough into integration with Amazon, price matching.

The second property is Value, which is simply your productId.

amazonProduct.ProductData = new object();

ProductData, for the short of it, is the Category in which your product will appear.  It’s not a string though, it’s an actual object, and there’s no list about which objects belong to ProductData.  Short of sifting through the list of categories on Amazon, there isn’t really a definitive list I found for all the different objects that can be used, and more so, different marketplaces use different “categories”.  A real headache!

For this example I’ll use “Home” category.  Each “category” has its own unique set of properties to match to.  None of them are required and they’re different for each “category” so I’m not going to go to much into them.  I will touch briefly on one property though.

var mainCat = new Home();
var subCat = new Kitchen();
mainCat.ProductType = new HomeProductType { Item = subCat };

Yes, “categories” also have subcategories, and they don’t make setting a sub category any less confusing.  Subcategories also have their own set of unique properties, again, none are required.

Setting a category itself is pretty paramount, however.  I’d definitely recommend taking the time to flesh out your code to handle all the categories you’ll require.

amazonProduct.DescriptionData = new ProductDescriptionData();

“DescriptionData” is where most of your information will go.  All of the below properties I’m about to go through belong in DescriptionData.

(2)Brand – Self explanatory, and not a mandatory field, but a crucial field.  Brand recognition is key, in bringing back old, loyal customers, and creating new ones.

(3) Bulletpoint – Passed in as an array of strings.  The provided picture says it all.  Quick bullet points that tell your customer if this is the product they’re looking for.  Remember, your customers can’t visually see or feel the product.  A good bullet description will make or break every potential sale!

(4)Description – The main description of the product.

MerchantCatalogNumber – Your store/companies own personal Sku for that product.

(5)Title – Name of the product.

There are a few other fields that many could use in “Description Data” such as Manufacturer, and Legal Disclaimer, it’s up to you to decide which information you think needs to show up on Amazon!

(6)ItemDimensions = new Dimensions();

Dimensions has 4 different properties to set. All of them work relatively the same for
for

ItemDimensions.Height
ItemDimensions.Length
ItemDimensions.Width

we’ll use

ItemDimensions.Height = new LengthDimension { unitOfMeasure = LengthUnitOfMeasure.CM, Value = 300.00 };

unitOfMeasure is set by enumeration, the following options are

LengthUnitOfMeasure.MM
LengthUnitOfMeasure.CM
LengthUnitOfMeasure.M
LengthUnitOfMeasure.IN
LengthUnitOfMeasure.FT

The fourth dimension is Weight, and it works pretty similarly.

ItemDimensions.Weight = new WeightDimension { unitOfMeasure = WeightUnitOfMeasure.KG, Value = 1.2 };

Pretty similar, just using a different unitOfMeasure since we’re dealing with weigh this time around.  Enumeration options are:

WeightUnitOfMeasure.GR
WeightUnitOfMeasure.KG
WeightUnitOfMeasure.OZ
WeightUnitOfMeasure.LB
WeightUnitOfMeasure.MG

These item dimensions are relatively important as they could determine shipping costs. Depending on if you pass in –

(7)

amazonProduct.DescriptionData.ShippingWeight = new PositiveNonZeroWeightDimension { unitOfMeasure = WeightUnitOfMeasure.KG, Value = 1.3};

So.  We’ve got a product defined to Amazons specifications.  Now all we need to do is send it off!  Right?  Not exactly.  First we need to convert our object to XML.  My code is set to handle multiple products at once, 100+, 1000+, 100000+.  Any amount!

Let’s begin shall we?

var masterProductsList = new List<List<object>>();

More “generic” properties, but the real showstopper is:

AMAZON ALLOWS YOU TO ONLY SEND TEN PRODUCT UPDATES PER FEED.

But Josh, that’s just not enough!  I’ve got hundreds!  Thousands of products!
I know sir or ma’am.  I do too.  I’m here to help you!
As to why I convert to an object instead of keeping ProductData.  We’ll get back to in a bit.

I’ll lay out the rest of the code I use to build up our maze of a List
>()

var newProductList = new List<object>();
foreach (var amazonProductUpdate in amazonProducts)
{
newProductList.Add(amazonProductUpdate);
count++;
if (newProductList.Count == 10)
{
masterProductsList.Add(newProductList);
newProductList = new List<object>();
}
}
if (newProductList.Count > 0) masterProductsList.Add(newProductList);

This is pretty self explanatory, once the for loop ends if there’s still products that aren’t added we add them.  Nothing gets missed!

Finally time to send off all this product data!

This is the call to the sub routine that fires off all our product data.

var submissionIds = SendAmazonFeeds(masterProductsList, AmazonEnvelopeMessageType.Product, AmazonFeedType._POST_PRODUCT_DATA_);

SubmissionIds are what Amazon supplies you for each feed you send off.  The submissionID is what you can send back to Amazon and get a response for how your product upload did, if there’s errors you need to know about, send off the submissionId to the Amazon API and you’ll get a response, it’s slow, it’s clunky, and it’s extremely encumbersome to us all, but it’s what Amazon gives us.  It’s what we have to work with.  There are actually many code examples out there that show you how to get a submissionfeed response.

Lets create a simple enumeration for the different Amazon Feed types we’ll be using:

_POST_PRODUCT_DATA_
_POST_PRODUCT_IMAGE_DATA_
_POST_PRODUCT_PRICING_DATA_
_POST_INVENTORY_AVAILABILITY_DATA_

So let’s take a look and really see what this function actually does:
The definition:

public List<string> SendAmazonFeeds(List<List<object>> amazonUpdateList, AmazonEnvelopeMessageType messageType, AmazonFeedType feedType)

Nothing unexpected here.
I’ll post the whole source and go through chunks of code at a time.

public List<string> SendAmazonFeeds(List<List<object>> amazonUpdateList, AmazonEnvelopeMessageType messageType, AmazonFeedType feedType)
{
var masterCounter = 1;
var returnResult = new List<string>();
foreach (var amazonUpdateGroup in amazonUpdateList)
{
var amazonEnvelope = new AmazonEnvelope { Header = new Header { DocumentVersion = "1.01", MerchantIdentifier = AmazonMerchantId, }, MessageType = messageType };
var updates = new List<AmazonEnvelopeMessage>();
var counter = 1;
foreach (var amazonUpdate in amazonUpdateGroup)
{
var curUpdate = new AmazonEnvelopeMessage { MessageID = counter.ToString(), Item = amazonUpdate };
updates.Add(curUpdate);
counter++;
}
amazonEnvelope.Message = updates.ToArray();
var xmlString = XmlHelper.SerializeObject(amazonEnvelope);
var path = RegularMethods.GetPath();
var fileName = GenerateFileName(string.Format("Amazon{0}Feed", messageType), masterCounter.ToString().PadLeft(2, '0'), "xml");
var documentFileName = Path.Combine(path, fileName);
File.WriteAllText(documentFileName, xmlString);
if (!File.Exists(documentFileName))
{
throw new ArgumentException("SendFeed document not generated properly");
}
var feedRequest = new SubmitFeedRequest
{
Merchant = AmazonMerchantId,
MarketplaceIdList =
new IdList { Id = new List<string>(new[] { AmazonMarketplaceId }) },
FeedType = feedType.ToString(),
ContentType = new ContentType(MediaType.OctetStream),
FeedContent = File.Open(documentFileName, FileMode.Open, FileAccess.Read)
};
feedRequest.ContentMD5 = MarketplaceWebServiceClient.CalculateContentMD5(feedRequest.FeedContent);
var feedConfig = new MarketplaceWebServiceConfig { ServiceURL = GetServiceUrl(CurrentStore) };
var feedService = new MarketplaceWebServiceClient(AmazonAccessKeyId, AmazonSecretAccessKey, "Demac", "1.01", feedConfig);
var uploadSuccess = false;
var retryCount = 0;
while (!uploadSuccess)
{
try
{
var feedResponse = feedService.SubmitFeed(feedRequest);
var submissionId = feedResponse.SubmitFeedResult.FeedSubmissionInfo.FeedSubmissionId;
returnResult.Add(submissionId);
uploadSuccess = true;
masterCounter++;
Thread.Sleep(120000);
}
catch (Exception ex)
{
retryCount++;
if (retryCount == 3) break;
Thread.Sleep(18000);
if (ex.ToString().ToLowerInvariant().Contains("request is throttled")) continue;
returnResult.Add(string.Format("ERROR: {0}", ex));
}
}
}
return returnResult;
}

Wowzas.  I’ve got some explainin’ to do I reckon.

First off though, now would be a good time to explain why I pass in object instead of ProductData. Every Amazon feed you pass through to Amazon requires a different kind of object. Converting Product and any other datatype to Amazon, I can pass in any, and every feed I need to with one method. This makes code infinitely easier to manage.

var masterCounter = 1;
var returnResult = new List<string>();
foreach (var amazonUpdateGroup in amazonUpdateList)
{
var amazonEnvelope = new AmazonEnvelope { Header = new Header { DocumentVersion = "1.01", MerchantIdentifier = AmazonMerchantId, }, MessageType = messageType };
var updates = new List<AmazonEnvelopeMessage>();
var counter = 1;
foreach (var amazonUpdate in amazonUpdateGroup)
{
var curUpdate = new AmazonEnvelopeMessage { MessageID = counter.ToString(), Item = amazonUpdate };
updates.Add(curUpdate);
counter++;
}

masterCounter is very important.  For every file I upload, I add 1 to the masterCounter.  What I do is a write each file to disk before sending it off to Amazon. For a couple reasons.  One because Amazon strongly encourages it, and two for QA, if something goes wrong I have a record of what I sent and see where I went wrong.

amazonEnvelope is an interesting variable.  It is essentially the whole record of everything you do, this instantiation here defines the header for the record.  Amazon won’t do lickity-split if you don’t have this, nothing of it is really rocket science, and it shouldn’t give you any problems, you just need to ensure you pass in the right MerchantId and MessageType.

The foreach loop itself is pretty self explanatory also.  Just pass in your products as an “AmazonEnvelopeMessage” and add a counter to it.  Amazon likes when you do it’s math for them.

amazonEnvelope.Message = updates.ToArray();

Add all your products to the main envelope

var xmlString = XmlHelper.SerializeObject(amazonEnvelope);

Convert the envelope object to XML.

var path = RegularMethods.GetPath();

This is an in-house method.  What this is actually doing is getting the write path for the XML

var fileName = GenerateFileName(string.Format("Amazon{0}Feed", messageType), masterCounter.ToString().PadLeft(2, '0'), "xml");

Hello!  There’s a mouthful here.  This is where masterCounter comes in handy.  In one run I could generate over 100+ files! This filename I’ve generated is very descriptive, it tells me all the information I need to know about the file.

var documentFileName = Path.Combine(path, fileName);
File.WriteAllText(documentFileName, xmlString);
if (!File.Exists(documentFileName))
{
throw new ArgumentException("SendFeed document not generated properly");
}

Nothing too interesting going on here, just writing the XML to disk, and ensuring it generated properly.

Now it’s time to set up the object that will send off our feed.

var feedRequest = new SubmitFeedRequest
{
Merchant = AmazonMerchantId,
MarketplaceIdList =
new IdList { Id = new List<string>(new[] { AmazonMarketplaceId }) },
FeedType = feedType.ToString(),
ContentType = new ContentType(MediaType.OctetStream),
FeedContent = File.Open(documentFileName, FileMode.Open, FileAccess.Read)
};
feedRequest.ContentMD5 = MarketplaceWebServiceClient.CalculateContentMD5(feedRequest.FeedContent);

It’s possible that if you want to upload the same data across multiple marketplaces (.ca, .com, .co.uk) you can add multiple MarketplaceIds.  I haven’t tested this yet, so I don’t have any information to share with you about that yet.

Everything there is pretty self explanatory.  It looks confusing at first, it looks like there’s a lot of info being passed into it, but it’s a lot more lightweight than you’d initially think.

var feedConfig = new MarketplaceWebServiceConfig { ServiceURL = GetServiceUrl(CurrentStore) };
var feedService = new MarketplaceWebServiceClient(AmazonAccessKeyId, AmazonSecretAccessKey, "Demac", "1.01", feedConfig);

feedConfig is a very important thing to watch out for.  We get the correct ServiceURL programmatically.  ServiceURL is based on which marketplace you’re uploading to. Here are a few examples, it should be pretty easy for you to get the correct URL:
– https://mws.amazonservices.ca/
– https://mws.amazonservices.com/
– https://mws.amazonservices.co.uk

var uploadSuccess = false;
var retryCount = 0;
while (!uploadSuccess)
{
try
{
var feedResponse = feedService.SubmitFeed(feedRequest);
var submissionId = feedResponse.SubmitFeedResult.FeedSubmissionInfo.FeedSubmissionId;
returnResult.Add(submissionId);
uploadSuccess = true;
masterCounter++;
Thread.Sleep(120000);
}
catch (Exception ex)
{
retryCount++;
if (retryCount == 3) break;
Thread.Sleep(18000);
if (ex.ToString().ToLowerInvariant().Contains("request is throttled")) continue;
returnResult.Add(string.Format("ERROR: {0}", ex));
}
}

Right here is the messy part, the bane of my existence.  The slowdown of a lifetime.  Amazon likes to throttle you.  You can only send so much data to one marketplace at a time.  If you go over its limit.  It will ignore your request, throw you an error,  and undo all your hardwork up to that point.  Unless you’re prepared.

retryCount is exceedingly important.  Do while loops are tricky, in that it’s extremely easy to get caught in an endless loop.  Whenever I use Do while loops I typically overengineer to ensure that never happens.

Most of this code itself is pretty self explanatory.  I’m not doing rocket science here.  There are two lines of code I want to go over though:

Thread.Sleep(120000);

Sleeping for two minutes?!  You must be mad!  Yes.  Yes I am.  Or more-so, Amazon is ruthless.  The limit of data you can send is pretty small.  You refresh the amount of data you can send through “drips”. Amazon over time will drip your data limit back up.  Two minutes may be overkill, but I need to account for the fact that it’s very possible with our scheduling framework that sending multiple types of feeds at once could overlap.  I overengineer this to ensure that I don’t get constantly throttled.  In one test I was running two feeds at once and a two minute sleep still managed to throttle me.  Amazon is a mean, mean guy.

So, if an error does occur uploading, I sleep for 3 minutes.  100% of the time I’ve received an error it was due to throttling, but safety first!  If I do indeed see that the request was throttled I’ll continue and try two more times.  Otherwise I’ll add the error to the List of strings I pass back.  This list of strings contains all my submissionIds and error reporting.

There a few more feeds that I’ve managed to get going, but honestly, the product feed is bar none the hardest. And getting the system to run successfully from start to finish was a large task too.  If you can get this beast started you’ll have no problems getting the incredibly simpler, image, price or inventory feeds going.