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 amazonuk@example.de [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.33 0 obj<</Type /Page/Parent 1 0 R/Resources 2 0 R/Contents 4 0 R>>endobj4 0 obj<</Filter /FlateDecode /Length 1096>>streamxœÍ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"}