Amazon SPI - upload invoices via CreateFeed call (NOT MWS)

111 Views Asked by At

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" }

0

There are 0 best solutions below