How to Make Amazon SP-API Requests in PHP Without SDK
Mar 29, 2023Computer HardwareComments (34)
This post covers how to make requests to the Amazon Selling Partner API (SP-API). The examples used here will be for the getItemOffers operation of the Product Pricing API, but you should be able to adapt what you learn to any of the operations or Selling Partner APIs.

Amazon makes an SDK available for these operations, but doing it yourself only takes about 100 lines of code, and you'll learn a lot more, including how to get access tokens, build canonical requests, sign requests, and create authorizations.

Requirements: Using the SP-API requires that you have accounts with both Amazon AWS and Seller Central, and that you have completed a developer profile in the Developer Central section of Seller Central. Please do these steps first if needed (if this is your first time, choose "private developer" or a similar option). This process may take some time to go through approval.

Region Note: The endpoint, region, and marketplace ID values in the examples are for the US (sellingpartnerapi-na.amazon.com, us-east-1, and ATVPDKIKX0DER respectively). Find other endpoints and regions here and marketplace IDs here if needed.


Step 1: Create IAM User and Policy in AWS


Head over to IAM (Identity and Access Management):
https://console.aws.amazon.com/iam/home

Policy


The first thing to do is create a permission policy. Click Policies under Access Management. Then Create Policy. You should see four entries: Service, Actions, Resources, and Request Conditions. You only need to set data for the first three:

Service: Click Service and it should give you a large list of services. In the search box put in "ExecuteAPI" and that should filter it to just that one result. Click it to set it.

Actions: You can just click the checkbox for "All ExecuteAPI actions".

Resources: Click "Any in this account" checkbox.

Now click Next: Tags at the bottom of the page (nothing needed on this page), and then Next: Review. Give your policy a descriptive name like "SellingPartnerAPI".

User


You now have a policy, which you can assign to a new user. Go to Users under Access Management and click Add Users. Give the user a descriptive name (such as "SellingPartnerAPIUser") then hit Next. On the Set permissions page click "Attach policies directly". Search for your policy ("SellingPartnerAPI") and click the checkbox next to it, then Next, then Create User.

Credentials


You'll need three things from your new IAM User in order to make SP-API calls:

  • ARN
  • Access Key
  • Access Key Secret

On the main Users page, click on your new user and copy down the ARN from the summary.

You now need to create an access key for this user. Click Security Credentials and scroll down to Access Keys. Click Create Access Key. For the "best practices" page click any of the options, or just "Other", then Next. Give the access key a description like "SellingPartnerAPIAccessKey", then Create Access Key. You'll be shown the Access key and Secret Access Key. Copy both down. Note: This is the only time you'll be able to get the secret. If you lose it, you'll have to delete and create a new key.

You can now leave AWS.


Step 2: Create an App in Seller Central


An app is needed to request access tokens, which are used in conjunction with your IAM User credentials to make SP-API calls.

Go to the Developer Central section of Amazon Seller Central:
https://sellercentral.amazon.com/sellingpartner/developerconsole

As long as you are an authorized developer, you'll be able to click Add new app client. Do so now to create a new app.

Give the new app a descriptive name and for the type choose "SP API". More options should pop up now. Enter the IAM ARN that you copied from your IAM User above. This ties the app to your IAM User (this can be edited later). Next click the checkbox (or checkboxes) for any Roles your app needs. These will be restricted to what you had originally chosen when creating your Developer Profile. For this guide we are only using the "Pricing" roll. Then click "No" for the RDT options. Save and exit.

You now have an app and should be back on the main Developer Central page. You need to copy down three things from your new app:

  • LWA Client ID
  • LWA Client Secret
  • Refresh Token

Click "View" under LWA Credentials and copy the ID and Secret. Note that on here Amazon will show you the secret as much as you want. Take care to differentiate the LWA ID and Secret from the IAM User Key and Secret from earlier; they are entirely different.

To get the refresh token you have to click the drop-down arrow next to "Edit App" and choose "Authorize". On the next page click Authorize App. This gives you a Refresh Token. Copy it.

You now have all the information you need to make an SP-API call! You can leave Seller Central.


Step 3: Writing the Code


Your code must have access to five of the six things you had created above:

  • IAM User Access Key
  • IAM User Access Key Secret
  • App LWA Client ID
  • App LWA Client Secret
  • App Refresh Token

(The User ARN is not needed.)

It is highly recommended not to store the secrets or token in plain text, or commit them to a repository. Amazon recommends that you encrypt them. For the below examples you'll just be putting them into constants for ease, since this is just for learning how to make SP-API requests. It is up to you how you want to securely handle these in a real-world application.

For these examples and simplicity we'll define this data as such:

define('IAM_USER_KEY', 'YOUR_DATA_HERE');
define('IAM_USER_SECRET', 'YOUR_DATA_HERE');
define('APP_LWA_ID', 'YOUR_DATA_HERE');
define('APP_LWA_SECRET', 'YOUR_DATA_HERE');
define('APP_REFRESH_TOKEN', 'YOUR_DATA_HERE');


Step 3.1: cURL Function


You'll need a basic cURL function that can handle both GET and POST requests. Also for the sake of simplicity any errors we run across will just exit with some details we can use to debug them.

// This function can do GET and POST for many SP-API operations.
// Some SP-API operations use other methods, such as PUT, which
// are not covered in this guide.
function httpRequest($url, $post = '', $header = null, &$status = null) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true,
CURLOPT_CONNECTTIMEOUT => 5,
]);
if ($post) curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
if ($header) curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
$out = curl_exec($ch);
if (curl_errno($ch)) exit('Error: ' . curl_error($ch));
if ($status !== null) $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
return $out;
}


Step 3.2: Settings


You'll need to define some basic settings that will get used in different areas:

// Every request should have a user agent included
define('USER_AGENT', 'My Pricing Tool/1.0 (Language=PHP)');

// The host, endpoint, region, and marketplace ID (change if not in US)
define('HOST', 'sellingpartnerapi-na.amazon.com');
define('ENDPOINT', 'https://' . HOST);
define('REGION', 'us-east-1');
define('MP_ID', 'ATVPDKIKX0DER');


Step 3.3: Requesting Access Tokens


The refresh token is used to get a temporary access token, which is the first request you must make before making any SP-API requests. To be clear, the refresh token itself cannot be used in SP-API requests. It can only be used to get an access token.

The access token is usually valid for 3600 seconds (1 hour) so you can cache it, and only make this request when it is expired or missing. The response you get will be JSON and have an expires_in value, so you can know the exact timestamp it will no longer be valid.

You may want to store the access token in a database. For this example we're just storing it in a secure cache file (preferably something not in public root) as JSON data.

// Path to secure token cache file
define('TOKEN_CACHE', '/path/to/token-cache-file.json');

function getAccessToken() {

// Return existing access token if exists and not expired
if (file_exists(TOKEN_CACHE)) {
$file = file_get_contents(TOKEN_CACHE);
$json = json_decode($file, true);
if ($json && !empty($json['token'])) {
if (!empty($json['expires']) && time() < $json['expires']) {
return $json['token'];
}
}
}

// Otherwise get new access token
$post = 'grant_type=refresh_token&refresh_token=' . APP_REFRESH_TOKEN
. '&client_id=' . APP_LWA_ID . '&client_secret=' . APP_LWA_SECRET;
$url = 'https://api.amazon.com/auth/o2/token';
$response = httpRequest($url, $post, ['user-agent:' . USER_AGENT]);

// Validate new access token response
if (strpos($response, '{"access_token":') !== 0) {
exit('Error: Access token response was bad: ' . $response);
}
if (strpos($response, 'expires_in') === false) {
exit('Error: No "expires_in" found in response: ' . $response);
}
$json = json_decode($response, true);
if (!$json || empty($json['access_token']) || empty($json['expires_in'])) {
exit('Error: Access token JSON decode failure: ' . $response);
}

// Cache access token with an expires timestamp
$cacheData = json_encode([
'token' => $json['access_token'],
'expires' => time() + $json['expires_in'],
]);
file_put_contents(TOKEN_CACHE, $cacheData);

// Return access token
return $json['access_token'];
}

We can now call this getAccessToken function during the next step.


Step 3.4: Amazon SP-API Request Function


We're going to make a generic function that can be used to make most SP-API requests. It just takes in four arguments:

  • $method - Usually GET or POST
  • $path - The path, not including the host/endpoint or query string. For example: "/products/pricing/v0/items/{Asin}/offers"
  • $qs - The query string without a '?' - Optional (usually needed for GET operations)
  • $post - The post data or often called the payload - Optional (usually needed for POST operations)

Inside this function you will do these things, which are the nuts and bolts of the Amazon SP-API request. Amazon's API is one of the more complicated ones, so I have tried to break each step down as logically as I can.

  • Build a canonical request
  • Create a signing key
  • Create a string to be signed
  • Sign the string to create a signature
  • Create an authorization header
  • Create the headers for the cURL request
  • Run the cURL request and validate the response

I have heavily commented the below function to help you understand each step:

function amazonRequest($method, $path, $qs = '', $post = '') {

// Get access token
$accessToken = getAccessToken();

// Two formats for date used throughout the function, in GMT.
// Make sure your server's time is accurate or else requests may fail.
$date = gmdate('Ymd\THis\Z');
$ymd = gmdate('Ymd');

// Build a canonical request. This is just a highly-structured and
// ordered version of the request you will be making. Each part is
// newline-separated. The number of headers is variable, but this
// uses four headers. Headers must be in alphabetical order.
$canonicalRequest = $method . "\n" // HTTP method
. $path . "\n" // Path component of the URL
. $qs . "\n" // Query string component of the URL (without '?')
. 'host:' . HOST . "\n" // Header
. 'user-agent:' . USER_AGENT . "\n" // Header
. 'x-amz-access-token:' . $accessToken . "\n" // Header
. 'x-amz-date:' . $date . "\n" // Header
. "\n" // A newline is needed here after the headers
. 'host;user-agent;x-amz-access-token;x-amz-date' . "\n" // Header names
. hash('sha256', $post); // Hash of the payload (empty string okay)

// Create signing key, which is hashed four times, each time adding
// more data to the key. Don't ask me why Amazon does it this way.
$signKey = hash_hmac('sha256', $ymd, 'AWS4' . IAM_USER_SECRET, true);
$signKey = hash_hmac('sha256', REGION, $signKey, true);
$signKey = hash_hmac('sha256', 'execute-api', $signKey, true);
$signKey = hash_hmac('sha256', 'aws4_request', $signKey, true);

// Create a String-to-Sign, which indicates the hash that is used and
// some data about the request, including the canonical request from above.
$stringToSign = 'AWS4-HMAC-SHA256' . "\n"
. $date . "\n"
. $ymd . '/' . REGION . '/execute-api/aws4_request' . "\n"
. hash('sha256', $canonicalRequest);

// Sign the string with the key, which will create the signature
// you'll need for the authorization header.
$signature = hash_hmac('sha256', $stringToSign, $signKey);

// Create Authorization header, which is the final step. It does NOT use
// newlines to separate the data; it is all one line, just broken up here
// for easier reading.
$authorization = 'AWS4-HMAC-SHA256 '
. 'Credential=' . IAM_USER_KEY . '/' . $ymd . '/'
. REGION . '/execute-api/aws4_request,'
. 'SignedHeaders=host;user-agent;x-amz-access-token;x-amz-date,'
. 'Signature=' . $signature;

// Create the header array for the cURL request. The headers must be
// in alphabetical order. You must include all of the headers that were
// in the canonical request above, plus you add in the authorization header
// and an optional content-type header (for POST requests with JSON payload).
$headers = [];
$headers[] = 'authorization:' . $authorization;
if ($post) $headers[] = 'content-type:application/json;charset=utf-8';
$headers[] = 'host:' . HOST;
$headers[] = 'user-agent:' . USER_AGENT;
$headers[] = 'x-amz-access-token:' . $accessToken;
$headers[] = 'x-amz-date:' . $date;

// Run the http request and capture the status code
$status = '';
$fullUrl = ENDPOINT . $path . ($qs ? '?' . $qs : '');
$result = httpRequest($fullUrl, $post, $headers, $status);

// Validate the response
if (strpos($result, 'Error:') === 0) exit($result);
if (empty($result)) exit('Error: Empty response');
if ($status != 200) exit('Error: Status code ' . $status . ': ' . $result);
if (strpos($result, '{') !== 0) exit('Error: Invalid JSON: ' . $result);

// Decode json and return it
$json = json_decode($result, true);
if (!$json) exit('Error: Problem decoding JSON: ' . $result);
return $json;
}


Step 3.5: Make the getItemOffers Request


With the generic Amazon SP-API request function made, we can use it to call most any SP-API operation. Here is a small function that calls the getItemOffers operation. It just takes in the ASIN that you want to get the list of offers for.

// Use the 'getItemOffers' operation to get offers for a single ASIN
function getOffersForAsin($asin) {
$method = 'GET';
$url = '/products/pricing/v0/items/' . $asin . '/offers';
$qs = 'Asin=' . $asin . '&ItemCondition=New&MarketplaceId=' . MP_ID;
return amazonRequest($method, $url, $qs);
}

Note: The Amazon docs may not show it, but the query string arguments likely need to be in alphabetical order, as they are above.


Step 4: Full Code Example


You can download the full code example here.
Comments (34)
Add a Comment
Max   Feb 23, 2024
excelent! i made some similar code a few days ago. but... the process to login...with OAuth Login URI and OAuth Redirect URI do you have an example of it?
Christopher   Feb 21, 2024
Hello! Great tutorial. I wonder is this still applicable now that Amazon changed their authorization way and no longer requires AWS IAM and Signature Version 4?
Michael   Dec 28, 2023
This code works great and is much simpler than any i have seen, question do you have any articles on getItemsOffersBatch. or could i contact you directly to discuss batch calls
Zynmuse   Oct 17, 2023
Si anyone else having the problem "The encrypted feed document contents were not uploaded to S3" when callinf CreateFeed operation?
Andrew   Oct 14, 2023
How to use NextToken? Thank you!!!
Zynmuse   Oct 06, 2023
Final part - to create the feed - (as per Lionel Aug 25, 2023 comment below). My code is $method = 'POST'; $url = '/feeds/2021-06-30/feeds'; $qs = ''; $post = '{"feedType":"POST_PRODUCT_PRICING_DATA","marketplaceIds":["'.MP_ID.'"],"inputFeedDocumentId":"'.$feedDocumentId.'"}'; $Response = amazonRequest($method, $url, $qs, $post); I always get the error 'The encrypted feed document contents were not uploaded to S3.' My price update doesn't make it to the amazon listing. Also don't know how to find the feedId in order to check if it has been processed and if the document had any errors. Any help would be appreciated.
oldcoder2   Oct 04, 2023
Lionel's comment helped me greatly to get Feeds API upgrade completed. More details on the upload for PHP cURL: you must use PUT, not POST, with CURLOPT_PUT, CURLOPT_INFILE, and CURLOPT_INFILESIZE options to upload the feed file to the temporary presigned url that was returned with the earlier POST to get the feedDocumentId. The headers must only include the exact content-type and user-agent that was Posted to get the feedDocumentId. Exact means no differences whatsoever even in capitals vs lower case. For contentType: to get the feedDocumentId, I used: $post_param = '{"contentType":"text/tab-separated-values; charset=UTF-8"}'; To do the upload : $headers[] = 'content-type:text/tab-separated-values; charset=UTF-8'; Successful upload returns success status and empty result.
Zynmuse   Oct 02, 2023
Does anyone have php code for PUT/POST operation to upload a feed to amazon. It is suggested below that people have it working, but the code for the PUT operation to copy the file to the FeedDocument url once it has been obtained is not given.
swindler   Oct 01, 2023
Works perfect, THX!
Ajar   Sep 28, 2023
Unfortuantely we were never able to get the PUT/POST working on the Listings API using this sample code and PHP. Interestingly enough we ported the example to Perl and the PUT/POST works flawlessly without any modifications, so I'm not sure if its an issue with the PHP implimentation that the Amazon API doesn't like or what. If anyone is interested in doing this in Perl speak up... with enough interest I might write a tutorial.... Cheers, Ajar
JHERSEY29   Sep 27, 2023
$qs = 'Asin=' . $asin . '&ItemCondition=New&MarketplaceId=' . MP_ID; should now be $qs = 'Asin=' . $asin . '&ItemCondition=New&MarketplaceIds=' . MP_ID; There is an s at the end of MarketplaceId. Not sure if Amazon changed this recently or something I did required the s. I also used my role key and role secret instead of the user role and user secret. Works great! Thanks you for the sample.
Gator   Sep 20, 2023
This page has been awesome!! Got all the GET stuff working. Stuck on POST. Right now I just want to createReport. Specially the GET_FLAT_FILE_OPEN_LISTINGS_DATA one. I'm using the following $method = 'POST'; $url = '/reports/2021-06-30/reports'; $qs = ''; $post = '{"reportTypes": ["GET_FLAT_FILE_OPEN_LISTINGS_DATA"], "marketplaceIds": ["' . MP_ID . '"]"}'; $response = amazonRequest($method, $url, $qs, $post); But I get this error - One or more required parameters missing, reportTypes
JN   Sep 20, 2023
hello. Using the above source, $method = 'PUT'; I would appreciate it if you could tell me what part I need to modify to use the putListingsItem function.
Ajar   Sep 19, 2023
Thanks for putting this together... It has been a big help and a timesaver. Your code seems to work for most everything except when using the Listings API and submitting a POST with json in the body, I get a 403 Unauthorized reply. Because of this we are unable to update listings or add new listings. Has anyone gotten this to work posting JSON code to the listings API? Cheers, Ajar
Beppe   Sep 14, 2023
Thank you! Awesome!
Ashish   Sep 01, 2023
Thank You So Much !! It really helped.
leonardo santoro   Aug 29, 2023
Hi Lionel, We are having major difficulties with submitting feeds. How do you upload the file? do you have any code? Thank you
Lionel   Aug 25, 2023
For those having issues with POST requests and Feeds API, I found the solution after long search: Here is the code I use: $method = 'POST'; $url = '/feeds/2021-06-30/documents'; $qs = ''; $post = '{"contentType": "text/tab-separated-values; charset=UTF-8"}'; $response = $this->amazonRequest($method, $url, $qs, $post); Then upload the file to the URL using PUT request (don't forget to use set "content-type" header and also "user-agent"). Finally to create the feed: $method = 'POST'; $url = '/feeds/2021-06-30/feeds'; $qs = ''; $post = '{"feedType": "POST_FLAT_FILE_INVLOADER_DATA", "marketplaceIds": ["'.$marketplaceId.'"], "inputFeedDocumentId":"'.$documentId.'"}'; $response = $this->amazonRequest($method, $url, $qs, $post); I hope it will save some time to others ;-)
Lionel   Aug 24, 2023
A big, big, BIG THANK YOU. I am in a rush to update my code from MWS to SP6API, and your functions saved me hours, clarifying how to send requests properly, how to generate token etc. If one day you come to Varna (Bulgaria) I pay you a restaurant :)
Martin   Aug 15, 2023
Thank you. Great help getting started migrating from MWS
emirhan   Aug 01, 2023
thank you sir. great document.
Rudi   Jul 28, 2023
I have found that I do not need the authorization at all. For example, I only set the 5 header entries for POST: aadd(aHeaders,{"content-type",'application/json;charset=utf-8'}) // 'application/json aadd(aHeaders,{"host",trim(AWSHost)}) aadd(aHeaders,{"user-agent",trim(AWSUserAgent)}) aadd(aHeaders,{"x-amz-access-token",trim(cAccessToken)}) aadd(aHeaders,{"x-amz-date",cDate}) cRequest := AWSEndpoint+cPath oResponse := oHttp:Execute(cRequest,'POST',cPost,aHeaders) // :Execute(xURI, [cCommand], [xPostContent], [aHTTPHeaders]) (my programming language is Xbase++) Of course you have to calculate the access token first, but that's not a problem. When uploading feeds you get a 'predefined' url assigned, this had to be sent wi
Mike   Jul 25, 2023
Works fine with the GET example, doesn´t with POST POST method: "Access to requested resource is denied". Does anyone got this to work with POST requests?
Rudi   Jul 18, 2023
Hi Nick, I programmed your instructions with Xbase++. Works great, except for the POST method. When I try to retrieve reports with the POST method, I always get the error: "Website temporarily unavailable". The same goes for the GET method. If I omit the 'authorization' header, the GET method works fine, but the POST method returns the error "Access to requested resource is denied."
Senthil   Jun 19, 2023
How to use Feeds API request for Uploading Documents ?
Thomas   Jun 17, 2023
Is there a good way to use Guzzle instead of curl?
Scott   May 02, 2023
Figured it out. I had to add an additional call to AssumeRole. Now sure how/why it worked the first time not doing that, but it works now. Appreciate the Help!
Nick   May 02, 2023
If you're receiving odd access errors that don't make sense, try waiting 24 hours and seeing if it resolves. I've seen issues resolved by just waiting when searching for my own issues (which in some cases resolved after waiting a day).
Scott   May 02, 2023
Worked initially, now all I get is: "Access to requested resource is denied." I'm 99% sure nothing changed on my end. Any ideas?
Nick   May 02, 2023
Also, no, this article does not cover RDT (Restricted Data Token) for handling sensitive customer data.
Nick   May 02, 2023
Thank you for catching that Hubert. I have made a change so that the marketplace ID is in a separate constant and noted above.
Hubert   May 01, 2023
Do you try to generate RDT token, to have full address of customers ?
Hubert   May 01, 2023
Very useful. Note : - there is a delay, between user/app creation, and possibility to use it - be carefull, MarketPlaceId is hardcoded in function getOffersForAsin(). I missed it and spend a lot of time to understand the problem. Thanks very much for the article.
Chris Lee   Apr 30, 2023
I just want to thank you for posting this !