Using the PHP Amazon API to Update Stock

In February we showed you how to integrate stock sharing with eBay, now we get to grips with Amazon’s API to do the same.

If you’ve implemented eBay stock sharing then prepare for a bit more work here as there are a few more obstacles in the way with this integration (including the terrible documentation) as Amazon throttle how much data you can send to, and receive from, them. In this post we are only providing the code to update stock; you will need to define the processes around it to adhere to Amazon’s rules.

Updating Amazon

I recommend storing all stock sold in a [stock sold] table then running through it every 20 minutes, pulling 15 products out at a time to inform Amazon to update. This means you will stay within Amazon’s throttle limits. It does of course mean that your stock levels won’t be 100% accurate all the time. Thanks Amazon.

First we need to assign the relevant details to the following variables (use the ones Amazon has given you), set up some parameters and place them in an array to be used later on.

$amazonSellerId         = '[use your details]';
$amazonAWSAccessKeyId   = '[use your details]';
$amazonSecretKey        = '[use your details]';
$amazonMarketPlaceId    = '[use your details]';

$param = array();
$param['AWSAccessKeyId']            = $amazonAWSAccessKeyId;
$param['Action']                    = "SubmitFeed";
$param['Merchant']                  = $amazonSellerId;
$param['FeedType']                  = "_POST_INVENTORY_AVAILABILITY_DATA_";
$param['SignatureMethod']           = "HmacSHA256";
$param['SignatureVersion']          = "2";
$param['Timestamp']                 = gmdate("Y-m-d\TH:i:s.\\0\\0\\0\\Z", time());
$param['Version']                   = "2009-01-01";
$param['MarketplaceIdList.Id.1']    = $amazonMarketPlaceId;
$param['PurgeAndReplace']           = "false";

$url = array();
foreach($param as $key => $val){
    $key = rawurlencode($key);
    $val = rawurlencode($val);
    $url[] = "{$key}={$val}";
}

Note the variables you’ve already populated, such as your AWS access key. The “FeedType” tells Amazon which of their many feeds we are wanting to use.

Then, looping through each product that needs to be updated we build up the “messages”. Each message tells Amazon which product we want to update based on its SKU. The MessageID needs to be unique for each message in the call (I just start at 1 and increment by one for each message). Quantity is the amount by which to reduce the stock i.e. how many have been sold.

$message .= <<<EOF
<Message>
    <MessageID>$i</MessageID>
    <OperationType>Update</OperationType>
    <Inventory>
        <SKU>$amazon_sku</SKU>
        <Quantity>$quantity</Quantity>
    </Inventory>
</Message>
EOF;

Then we incorporate the message into the complete XML.

$amazon_feed = <<<EOF
<?xml version="1.0" encoding="utf-8"?>
<AmazonEnvelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="amzn-envelope.xsd">
    <Header>
        <DocumentVersion>1.01</DocumentVersion>
        <MerchantIdentifier>$amazonSellerId</MerchantIdentifier>
    </Header>
    <MessageType>Inventory</MessageType>
    $message
</AmazonEnvelope>
EOF;

Just a bit more messing around here, setting up the headers and incorporating the $url array we created earlier.

sort($url);

$arr   = implode("&", $url);

$sign  = "POST\n";
$sign .= "mws-eu.amazonservices.com\n";
$sign .= "/Feeds/{$param['Version']}\n";
$sign .= $arr;

$signature      = hash_hmac("sha256", $sign, $amazonSecretKey, true);
$httpHeader     = array();
$httpHeader[]   = "Transfer-Encoding: chunked";
$httpHeader[]   = "Content-Type: application/xml";
$httpHeader[]   = "Content-MD5: ".base64_encode(md5($amazon_feed, true));
$httpHeader[]   = "Expect:";
$httpHeader[]   = "Accept:";

$signature = urlencode(base64_encode($signature));

$link  = "https://mws-eu.amazonservices.com/Feeds/{$param['Version']}?";
$link .= $arr."&Signature=".$signature;

And finally we get to actually send the XML to Amazon, using cURL.

$ch = curl_init($link);
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeader);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $amazon_feed);
$response = curl_exec($ch);
$info = curl_getinfo($ch);
$errors = curl_error($ch);
curl_close($ch);

$xml = simplexml_load_string($response);
$result = $xml->children()->SubmitFeedResult->
         children()->FeedSubmissionInfo->
         children()->FeedProcessingStatus;

If the almost-impossible-to-find $result is “_SUBMITTED_” then it’s worked, hurray! If not, good luck.

I would then update the [stock sold] table so we know which products have been updated and we don’t attempt to update them again.

Updating Website

Now we can update Amazon when we’ve sold something on our site, let’s see how we update our website when something’s sold on Amazon.

We need to import orders. For this we need the help of Amazon’s PHP SDK, MarketplaceWebServiceOrders, which you can find here (note: at time of writing this is the latest version, if a new version is out it should be linked to from here).

Firstly you’ll need to edit the config file (config.inc.php) to include your details.

Then use the below code to request all “unshipped” and “partially shipped” orders from your preferred $date onwards.

$serviceUrl = "https://mws-eu.amazonservices.com/Orders/2013-09-01";

$config = array (
	"ServiceURL" => $serviceUrl,
	"ProxyHost" => null,
	"ProxyPort" => -1,
	"MaxErrorRetry" => 3,
);

$service = new MarketplaceWebServiceOrders_Client(
	AWS_ACCESS_KEY_ID,
	AWS_SECRET_ACCESS_KEY,
	APPLICATION_NAME,
	APPLICATION_VERSION,
	$config);

$request = new MarketplaceWebServiceOrders_Model_ListOrdersRequest();
$request->setSellerId(MERCHANT_ID);

$request->setCreatedAfter(new DateTime($date));

$marketplaceIdList = new MarketplaceWebServiceOrders_Model_MarketplaceIdList();
$marketplaceIdList->setId(array(MARKETPLACE_ID));
$request->setMarketplaceId($marketplaceIdList);

$orderStatuses = new MarketplaceWebServiceOrders_Model_OrderStatusList();
$orderStatuses->setStatus(array("Unshipped", "PartiallyShipped"));
$request->setOrderStatus($orderStatuses);
invokeListOrders($service, $request);

The function invokeListOrders is as below.

function invokeListOrders(MarketplaceWebServiceOrders_Interface $service, $request){
    $response = $service->listOrders($request);

    if($response->isSetListOrdersResult()){
        $listOrdersResult = $response->getListOrdersResult();

        if($listOrdersResult->isSetOrders()){
            $orders = $listOrdersResult->getOrders();
            $orderList = $orders->getOrder();

            foreach($orderList as $order){
                $shippingAddress = $order->getShippingAddress();

                $name = $shippingAddress->getName();
                $addr1 = $shippingAddress->getAddressLine1();
                $addr2 = $shippingAddress->getAddressLine2();
                $addr3 = $shippingAddress->getAddressLine3();
                $city = $shippingAddress->getCity();
                $county = $shippingAddress->getStateOrRegion();
                $postcode = $shippingAddress->getPostalCode();
                $country = $shippingAddress->getCountryCode();
                $tel = $shippingAddress->getPhone();
                $orderTotal = $order->getOrderTotal();
                $total = $orderTotal->getAmount();

                $amazonid = $order->getAmazonOrderId();

                $request = new MarketplaceWebServiceOrders_Model_ListOrderItemsRequest();
                $request->setSellerId(MERCHANT_ID);
                $request->setAmazonOrderId($order->getAmazonOrderId());
                invokeListOrderItems($service, $request, $amazonid);
            }
        }
    }
}

That loops through all orders matching the status and date limit previously set. You should store these details in your database so you can check whether you’ve imported the order or not before changing stock (it’s not in the code example above but if you have already imported, don’t call invokeListOrderItems); I don’t know of a way to only import “new” orders, so Amazon will send you everything from the date you specify.

The function invokeListOrderItems gets all items/products from each order, as can be seen here:

function invokeListOrderItems(MarketplaceWebServiceOrders_Interface $service, $request, $amazonid){
    global $basket;

    $response = $service->listOrderItems($request);
    if($response->isSetListOrderItemsResult()){
        $listOrderItemsResult = $response->getListOrderItemsResult();
        if($listOrderItemsResult->isSetOrderItems()){
            $orderItems = $listOrderItemsResult->getOrderItems();
            $orderItemList = $orderItems->getOrderItem();

            foreach($orderItemList as $orderItem){
                $product_name = $orderItem->getTitle();
                if($orderItem->isSetSellerSKU()){
                    $product_stockcode = $orderItem->getSellerSKU();
                }
                $itemPrice = $orderItem->getItemPrice();
                $quantity = $orderItem->getQuantityOrdered();
                $product_price = $itemPrice->getAmount()/$quantity;
            }
        }
    }
}

As you can see that rather laborious task eventually yields the product information that we need to update the website’s stock – $product_stockcode and $quantity.

You can also access the product name and price amongst numerous other things. Note that the price, which you may or may nor need, is the price of the individual item multiplied by the number of that item purchased, so to get the individual item price we need to divide by the number purchased.

This code is free to use at your own discretion. It comes without warranty. Please feel free to feedback any edits.


We'd love to hear from you!

If you think Bronco has the skills to take your business forward then what are you waiting for?

Get in Touch Today!

Discussion

Write a comment...
  • Saty Jardim

    Hello. This is an excellent article. Much more explanatory and simple than the poor and confusing Amazon documentation. Would you be able to tell me if it is possible to update inventory and price in the same XML at the same time?

    • Chris Antcliff

      Thanks for the comment Saty. I haven’t had to do that, but I believe you’d need to make a separate call as POST_INVENTORY_AVAILABILITY_DATA just deals with stock/availability.

  • Rashed

    Hello Chris, I have already developed an inventory software using laravel and mysql. I want to integrate amazon with my inventory. Whenever anything will be sold in the Amazon same time my inventory stock will be updated using Amazon API. Is this possible.

    • Chris Antcliff

      Hi Rashed,
      Yes, that’s the process the “Updating Website” part of this blog post explains.

  • LR

    It is so much easier to update the Quantity and/or Price using a plain-text feed.

    You’ll still use the SubmitFeed API, but set the feed type to “_POST_FLAT_FILE_PRICEANDQUANTITYONLY_UPDATE_DATA_” and set the feed content (in $amazon_feed) to a tab-delimited list having columns sku (required) and quantity and/or price, depending on what you want to update; include the column headers in the first line. To NOT update a field, leave it *empty* (not zero!). (It’s easiest to build/PDO-fetch an array of the lines, then implode(“\n”, $list) the array; just initialize the array with the columns you intend to send.)

    This technique avoids limiting the update to 15 items every 20 minutes and a clunky XML feed, but you still have to deal with the per-hour limit on calls to SubmitFeed. Feeds having up to 50,000 rows are processed with minimal or no delay, but if you have more items to update (congratulations! and) you should just queue multiple feeds. YMMV but depending on the size of the file, it takes up to 15 minutes for changes to be sync’d to Seller Central, but the dot-com site updates real-time as the file is processed.

    For the second part of the post, where you update your local order data and reduce the local snapshot of available stock: Instead of setCreatedAfter use setLastUpdatedAfter, which will only return orders that have “changed” since the given timestamp; and implement logic to create or update your local record of the order, and decrease stock for the quantity change (so that you don’t over-reduce available stock when multiple units of a single sku get sent in several shipments).

    You should also include “Shipped” in the criteria for orderStatuses, to get verification of the final quantity shipped (and to get orders that haven’t been seen in “Unshipped” status); also, for completeness, include the Amazon-misspelled status “Canceled” so you can increase the available stock for units that were ordered but didn’t get shipped.

    • Chris Antcliff

      Thanks for the explanation – can’t say I ever came across any of that in Amazon’s documentation!

  • Rob Bose

    Method for updating Amazon stock does not work as there is no parameter for:

    AWS secret key.

    Response will always be 403.

    • Chris Antcliff

      Hi Rob,
      Are you setting $amazonSecretKey in $signature? Probably a bit buried in the code and not explained, sorry.

      I’ve edited the top of the post to include the variables you need to set.

Add a Comment