I try to use the Amazon Sellers Central SPI API. For our most use cases it works. But I have problems to upload a pdf invoice via creatFeed call. The support does not helps in that cases and the documentation is not described very well: https://developer-docs.amazon.com/sp-api/docs/option-3-upload-invoices-through-sp-api-or-seller-central
I tried to take over the java example into php code, but it doesnt work. I get the positive response from the first createFeedDocument request with a valid feedDocumentId and url. If I try to post the next curl request to upload the pdf document to the feedDocumentId, I've got an error.
I hope that somebody can help and solved the same issue in the past. I'm happy to share other amazon spi knowledge.
Here are some details:
URL: https://sellingpartnerapi-eu.amazon.com/feeds/2021-06-30/feeds
HEADER: Array
(
[0] => authorization:AWS4-HMAC-SHA256 Credential=890938122806/20240206/eu-west-1/execute-api/aws4_request,SignedHeaders=host;user-agent;x-amz-access-token;x-amz-date,Signature=ba24f98ea3f09247aeb43bdd1c6d9fcdbcaa530c979a4746931b98aa48475213
[1] => content-type:application/json;charset=utf-8
[2] => host:sellingpartnerapi-eu.amazon.com
[3] => user-agent:Backend mit [email protected]
[4] => x-amz-access-token:Atza|IwEBIC0NgdF25X5W1-8i1KMazp_obJEkr_1tb6DxEXXXXXXXXXXXXXXXXXXXXyGiU3cVAsyiDHZWl_qJZnOr0UTfKCe0CZWRJmlC2v2Rp2nIEsSXXXXXXXXXXXXXXXQMR57FjYlmMCwcq7knR5_02Z3zJtStR-QFejZw63aPE7e-LIePQPYVW7AlIaZpP4LJ4bXXXXXXXXXXXV997VANMEctK_7r5ZJpzwzV_Bqz2wYXXXXXXXXXdWoxrV6iadSx-j3cTzb6LPtgeB-7S2QFu8s7ftf05Xukk0XXXXXXXXXXXIHW0v7_kWFhehVzBhQqYDEklUMRCXaAbXXXXXXX3RWWZKr1K10s7ZQ
[5] => x-amz-date:20240206T165714Z
)
POST-Parameters:
{
"feedType": "UPLOAD_VAT_INVOICE",
"marketplaceIds": [
"A1RKKUPIHCS9HS"
],
"pdfDocument": "%PDF-1.3
3 0 obj
<</Type /Page
/Parent 1 0 R
/Resources 2 0 R
/Contents 4 0 R>>
endobj
4 0 obj
<</Filter /FlateDecode /Length 1096>>
stream
xœÍVKoÛF¾çWÌ1¥Õ>I®•`ùÛk).....(PDF as base64_encode String)",
"ContentMd5": "1B2M2Y8AsgTpgAmY7PhCfg==",
"feedOptions":
{
"OrderId":"404-9999999-3381931",
"DocumentType":"Invoice",
"InvoiceNumber":24010109016131
},
"inputFeedDocumentId":"amzn1.tortuga.4.eu.94cdaac5-977b-4dd4-8788-56cbd7bf5d35.T5U9S8ENNFPG4"
}
Response:{
"errors": [
{
"code": "InvalidInput",
"message": "Could not process payload",
"details": ""
}
]
}
PHP-Code:
$post = '
{
"feedType": "'.$feedType.'",
"marketplaceIds": [
"'.$marketplaceIds.'"
],
"pdfDocument": "'.$FeedContent.'",
"ContentMd5": "'.$ContentMd5.'",
"feedOptions":
{
"OrderId":"'.$feedOptions['OrderId'].'",
"DocumentType":"'.$feedOptions['DocumentType'].'",
"InvoiceNumber":'.$feedOptions['InvoiceNumber'].'
},
"inputFeedDocumentId":"'.$inputFeedDocumentId.'"
}
';
$url = '/feeds/2021-06-30/feeds';
$result = amazonRequest($connect_SPI, $method, $url, $qs, $post);
PHP FUNCTIONS:
// ######################## Standard functions - connection of Amazon API #######################
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;
}
function getAccessToken($connect_SPI) {
$date = gmdate('Ymd\THis\Z');
//Amazon Doku für Tokenverwendung: https://developer-docs.amazon.com/sp-api/docs/connecting-to-the-selling-partner-api#step-1-request-a-login-with-amazon-access-token
//Je Account wird ein eigener Token benötigt - sonst Grant Permission Fehler
$token_cache = $_SERVER["DOCUMENT_ROOT"] . '/temp/amazon/amazon-access-token_'.$connect_SPI['amazon_spi.SPI_IAM_USER_KEY'].'.json';
// 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=' . $connect_SPI['amazon_spi.SPI_APP_REFRESH_TOKEN']
. '&client_id=' . $connect_SPI['amazon_spi.SPI_APP_LWA_ID'] . '&client_secret=' . $connect_SPI['amazon_spi.SPI_APP_LWA_SECRET'];
$url = 'https://api.amazon.com/auth/o2/token';
$header = ['user-agent:' . 'Samurai-Backend mit '.$connect_SPI['amazon_spi.login_web_user']];
$response = httpRequest($url, $post, $header);
// Validate new access token response
if (strpos($response, '{"access_token":') !== 0) {
$out_file = 'Error: Access token response was bad: ' . $response. '\n\r'.' Poststring:'.$post.'\n\r'."URL: ".$url.'\n\r'."Header: ".$header;
if(!file_put_contents($_SERVER['DOCUMENT_ROOT']."/temp/amazon/response/".$date.".log", $out_file)){
echo 'Logfile kann nicht erstellt werden: ' . $_SERVER['DOCUMENT_ROOT']."/temp/amazon/response/".$date.".log";
}
exit('Error: Access token response was bad: ' . $response. '\n\r'.' Poststring:'.$post.'\n\r'."Header: ".print_r($header, true));
}
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'];
}
function amazonRequest($connect_SPI, $method, $path, $qs = '', $post = '') {
// Get access token
$accessToken = getAccessToken($connect_SPI);
// Two formats for date used throughout
$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:' . $connect_SPI['amazon_spi.SPI_serviceUrl'] . "\n" // Header
. 'user-agent:' . 'Samurai-Backend mit '.$connect_SPI['amazon_spi.login_web_user'] . "\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
//. 'x-amzn-api-sandbox: dynamic{' . "\n" // Header
. hash('sha256', $post). "\n" // Hash of the payload (empty string okay)
. '}'. "\n";
//echo $canonicalRequest;
// 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' . $connect_SPI['amazon_spi.SPI_AWS_ACCESS_KEY_ID'], true);
$signKey = hash_hmac('sha256', $connect_SPI['amazon_spi.SPI_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 . '/' . $connect_SPI['amazon_spi.SPI_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=' . $connect_SPI['amazon_spi.SPI_IAM_USER_KEY'] . '/' . $ymd . '/'
. $connect_SPI['amazon_spi.SPI_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:' . $connect_SPI['amazon_spi.SPI_serviceUrl'];
$headers[] = 'user-agent:' . 'Samurai-Backend mit '.$connect_SPI['amazon_spi.login_web_user'];
$headers[] = 'x-amz-access-token:' . $accessToken;
$headers[] = 'x-amz-date:' . $date;
// Run the http request and capture the status code
$status = '';
if(!strpos($path, 'http')){
$fullUrl = 'https://'. $connect_SPI['amazon_spi.SPI_serviceUrl'] . $path . ($qs ? '?' . $qs : '');
}else{ //wenn ein voller Pfad mitgegeben wird diesen verwenden - benötigt u.a. bei createFeedDocument --> URL
$fullUrl = $path . ($qs ? '?' . $qs : '');
}
echo $log_file = "HEADER: " . print_r($headers, true)."\n\r".
"POST: " . $post."\n\r".
"canonicalRequest:".$canonicalRequest."\n\r".
"fullUrl:".$fullUrl."\n\r";
$result = httpRequest($fullUrl, $post, $headers, $status);
echo $log_file .= "Response:".$result;
if(!file_put_contents($_SERVER['DOCUMENT_ROOT']."/temp/amazon/response/".$date.".log", $log_file)){
echo 'Logfile kann nicht erstellt werden: ' . $_SERVER['DOCUMENT_ROOT']."/temp/amazon/response/".$date.".log";
}
if (strpos($result, 'Error:') === 0){
echo "Fehler:". $result;
}
//if (empty($result)) exit('Error: Empty response');
//if ($status != 200 || $status != 202 || $status != '') 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;
}
For a positive response I expect a json like this:
{ "feedId": "23492394" }