laravelamazon-web-servicesamazon-s3aws-sdkamazon-selling-partner-api

Upload Feed to Amazon with AWS SP-API


My problem is uploading xml file in url that should be uploaded after feed is created, but every time I am getting error SignatureDoesNotMatch

I am trying to upload xml file on url that I got from request createFeedDocument https://developer-docs.amazon.com/sp-api/docs/feeds-api-v2021-06-30-use-case-guide

public function createFeedDocument($distributionChannelTeamId){
    $response = Http::withHeaders([
        'x-amz-access-token'=> $this->getAccessToken($distributionChannelTeamId),
    ])->post(
        'https://sellingpartnerapi-eu.amazon.com/feeds/2021-06-30/documents',
        [
            "contentType"=> "text/tab-separated-values; charset=UTF-8"
        ]
    );

    Log::channel('amazon')->info('Amazon create feed document response: ', ['response' => $response->json()]);
    if($response->successful()){
        $body = $response->json();
        return $body;
    }else{
        Log::channel('amazon')->info('Amazon create feed document failed for distribution channel team id: ' . $distributionChannelTeamId);
        Log::channel('amazon')->info('Amazon create feed document failed: ', ['response' => $response->json()]);
    }

}`

And then I am getting response like this just on my real selling account (this is example)

{
    "feedDocumentId": "3d4e42b5-1d6e-44e8-a89c-2abfca0625bb",
    "url": "https://d34o8swod1owfl.cloudfront.net/Feed_101__POST_PRODUCT_DATA_.xml"
}

Then I want to createFeed and upload it as docs says:

public function createFeed($distributionChannelTeamId)
{
    $res=$this->createFeedDocument($distributionChannelTeamId);

    $url=$res['url'];
    $feedDocumentId=$res['feedDocumentId'];

    $prefixAmazon = Storage::disk('amazon')->getDriver()->getAdapter()->getPathPrefix();
    $requestXml = file_get_contents($prefixAmazon . 'createFeed.xml');

    $response = Http::withHeaders(['Content-Type' => 'text/xml'
    ])->post($url,[
        'body' => utf8_encode($requestXml)
    ]);

    Log::channel('amazon')->info('Amazon create feed response: ', ['response' => $response]);`

And I am getting this in Log, as request is 403, Amazon create feed response:

{"response":{"Illuminate\Http\Client\Response":" SignatureDoesNotMatchThe request signature we calculated does not match the signature you provided. Check your key and signing method.AKIAX2ZVOZFBB4YFXXHUAWS4-HMAC-SHA256 20231031T161031Z 20231031/eu-west-1/s3/aws4_request 93dfa574b9431ebb05c7c80589f14946f14741d8ca907ea25415a1891c25965aa30675d84223aea473a1772fc2907ce7a9c7b3a4575aad2ee2549665992d866841 57 53 34 2d 48 4d 41 43 2d 53 48 41 32 35 36 0a 32 30 32 33 31 30 33 31 54 31 36 31 30 33 31 5a 0a 32 30 32 33 31 30 33 31 2f 65 75 2d 77 65 73 74 2d 31 2f 73 33 2f 61 77 73 34 5f 72 65 71 75 65 73 74 0a 39 33 64 66 61 35 37 34 62 39 34 33 31 65 62 62 30 35 63 37 63 38 30 35 38 39 66 31 34 39 34 36 66 31 34 37 34 31 64 38 63 61 39 30 37 65 61 32 35 34 31 35 61 31 38 39 31 63 32 35 39 36 35 61POST /3e46e671-9500-49c1-8920-18a4732287c9.amzn1.tortuga.4.eu.T1OBJR31A06AXO X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAX2ZVOZFBB4YFXXHU%2F20231031%2Feu-west-1%2Fs3%2Faws4_request&X-Amz-Date=20231031T161031Z&X-Amz-Expires=300&X-Amz-SignedHeaders=content-type%3Bhost content-type:text/xml host:tortuga-prod-eu.s3-eu-west-1.amazonaws.com content-type;host UNSIGNED-PAYLOAD50 4f 53 54 0a 2f 33 65 34 36 65 36 37 31 2d 39 35 30 30 2d 34 39 63 31 2d 38 39 32 30 2d 31 38 61 34 37 33 32 32 38 37 63 39 2e 61 6d 7a 6e 31 2e 74 6f 72 74 75 67 61 2e 34 2e 65 75 2e 54 31 4f 42 4a 52 33 31 41 30 36 41 58 4f 0a 58 2d 41 6d 7a 2d 41 6c 67 6f 72 69 74 68 6d 3d 41 57 53 34 2d 48 4d 41 43 2d 53 48 41 32 35 36 26 58 2d 41 6d 7a 2d 43 72 65 64 65 6e 74 69 61 6c 3d 41 4b 49 41 58 32 5a 56 4f 5a 46 42 42 34 59 46 58 58 48 55 25 32 46 32 30 32 33 31 30 33 31 25 32 46 65 75 2d 77 65 73 74 2d 31 25 32 46 73 33 25 32 46 61 77 73 34 5f 72 65 71 75 65 73 74 26 58 2d 41 6d 7a 2d 44 61 74 65 3d 32 30 32 33 31 30 33 31 54 31 36 31 30 33 31 5a 26 58 2d 41 6d 7a 2d 45 78 70 69 72 65 73 3d 33 30 30 26 58 2d 41 6d 7a 2d 53 69 67 6e 65 64 48 65 61 64 65 72 73 3d 63 6f 6e 74 65 6e 74 2d 74 79 70 65 25 33 42 68 6f 73 74 0a 63 6f 6e 74 65 6e 74 2d 74 79 70 65 3a 74 65 78 74 2f 78 6d 6c 0a 68 6f 73 74 3a 74 6f 72 74 75 67 61 2d 70 72 6f 64 2d 65 75 2e 73 33 2d 65 75 2d 77 65 73 74 2d 31 2e 61 6d 61 7a 6f 6e 61 77 73 2e 63 6f 6d 0a 0a 63 6f 6e 74 65 6e 74 2d 74 79 70 65 3b 68 6f 73 74 0a 55 4e 53 49 47 4e 45 44 2d 50 41 59 4c 4f 41 44FEWNEPN090HEGZVTvmwWxEA8m500fDCGb9NXTXeYVrE+GRFih0e/3RppPUc243qA5QEA80aCCQsC0MM/h9gNQ4cUYGT0BLYboiz+mw=="}}


Solution

  • I found solution for this create product feed in Laravel with Amazon SP-API (https://github.com/amazon-php/sp-api-sdk):

    composer require amazon-php/sp-api-sdk
    composer require nyholm/psr7 -W
    
    
    use AmazonPHP\SellingPartner\AccessToken;
    use AmazonPHP\SellingPartner\Model\Feeds\CreateFeedDocumentSpecification;
    use AmazonPHP\SellingPartner\Model\Feeds\CreateFeedSpecification;
    use AmazonPHP\SellingPartner\Regions;
    use AmazonPHP\SellingPartner\SellingPartnerSDK;
    use App\Models\Amazon\AmazonAuth;
    use Exception;
    use GuzzleHttp\Handler\CurlFactory;
    use Illuminate\Support\Facades\Log;
    use Illuminate\Support\Facades\Http;
    use Illuminate\Support\Facades\App;
    use Carbon\Carbon;
    use Illuminate\Support\Facades\Storage;
    use GuzzleHttp\Client;
    use AmazonPHP\SellingPartner\Api\FeedsApi\FeedsSDK;
    use AmazonPHP\SellingPartner\Configuration;
    use AmazonPHP\SellingPartner\Exception\ApiException;
    use AmazonPHP\SellingPartner\Exception\InvalidArgumentException;
    use AmazonPHP\SellingPartner\HttpFactory;
    use AmazonPHP\SellingPartner\HttpSignatureHeaders;
    use AmazonPHP\SellingPartner\ObjectSerializer;
    use Psr\Http\Client\ClientExceptionInterface;
    use Psr\Http\Client\ClientInterface;
    use Psr\Http\Message\RequestInterface;
    use Psr\Log\LoggerInterface;
    use AmazonPHP\SellingPartner\OAuth;
    use Buzz\Client\Curl;
    use Nyholm\Psr7\Factory\Psr17Factory;
    use Psr\Log\NullLogger;
    
    
    public function getAccessToken($channelId)
      {
    
        $amazonAuth = AmazonAuth::where('channel_id', $channelId)->first();  //my local database channel
        $isExpired = Carbon::parse($amazonAuth->expires_at)->isPast();
        $token = null;
    
        if ($isExpired) {
    
          $data = [
            'grant_type' => 'refresh_token',
            'refresh_token' => $amazonAuth->refresh_token,
            'client_id' => TenantService::getLwaClientId(),
            'client_secret' => TenantService::getLwaSecret()
          ];
    
          $response = Http::asForm()->post(
            'https://api.amazon.com/auth/o2/token',
            $data
          );
    
    
          if ($response->successful()) {
            $body = $response->json();
            $amazonAuth->access_token = $body['access_token'];
            $amazonAuth->expires_at = now()->addSeconds($body['expires_in'])->toDateTimeString();
            $amazonAuth->save();
    
            $token = $body['access_token'];
          } else {
            Log::channel('amazon')->info('Amazon access token refresh failed for amazon auth id: ' . $amazonAuth->id);
           
          }
    
        } else {
          $token = $amazonAuth->access_token;
        }
    
        if ($token) {
          $accessToken = new AccessToken(
            $token,
            $amazonAuth->refresh_token,
            'refresh_token',
            (int) $amazonAuth->expires_in,
            'refresh_token'
          );
    
          return $accessToken;
        }
    
    
      }
    
    public function createFeedTest()
      {
    
        $client = new Client();
        $factory = new Psr17Factory();
        // $httpFactory = new HttpFactory($factory, $factory);
        $region = 'eu';
        $accessToken = $this->getAccessToken(150);
        $logger = new NullLogger();
        $configuration = Configuration::forIAMUser(
          $getLwaClientId,
          $getLwaSecret,
          $getAwsAccessKey,
          $getAwsSecretKey,
        );
        $sdk = SellingPartnerSDK::create($client, $factory, $factory, $configuration, $logger);
        $region = Regions::EUROPE;
    
        $specification = new CreateFeedDocumentSpecification;
        $feedDoc = $sdk->feeds()->createFeedDocument(
          $accessToken,
          $region,
          $specification->setContentType('text/xml; charset=utf-8')
        );
        $feedDocID = $feedDoc['feed_document_id'];
        $urlFeedUpload = $feedDoc['url'];
    
        $fileContent = '<?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>MYMERCHANTTOKEN</MerchantIdentifier>
                </Header>
                <MessageType>Product</MessageType>
                <PurgeAndReplace>false</PurgeAndReplace>
                <Message>
                  <MessageID>1</MessageID>
                  <OperationType>Update</OperationType>
                  <Product>
                    <SKU>56789</SKU>
                    <StandardProductID>
                      <Type>ASIN</Type>
                      <Value>B0EXAMPLEG</Value>
                    </StandardProductID>
                    <ProductTaxCode>A_GEN_NOTAX</ProductTaxCode>
                    <DescriptionData>
                      <Title>Example Product Title</Title>
                      <Brand>Example Product Brand</Brand>
                      <Description>This is an example product description.</Description>
                      <BulletPoint>Example Bullet Point 1</BulletPoint>
                      <BulletPoint>Example Bullet Point 2</BulletPoint>
                      <MSRP currency="USD">25.19</MSRP>
                      <Manufacturer>Example Product Manufacturer</Manufacturer>
                      <ItemType>example-item-type</ItemType>
                      <CountryOfOrigin>DE</CountryOfOrigin>
                      <UnitCount>1</UnitCount>
                      <PPUCountType>stück</PPUCountType>
                      <IsExpirationDatedProduct>false</IsExpirationDatedProduct>
                    </DescriptionData>
                    <ProductData>
                      <Health>
                        <ProductType>
                          <HealthMisc>
                            <Ingredients>Example Ingredients</Ingredients>
                            <Directions>Example Directions</Directions>
                          </HealthMisc>
                        </ProductType>
                      </Health>
                    </ProductData>
                    <IsHeatSensitive>false</IsHeatSensitive>
    
                  </Product>
                </Message>
              </AmazonEnvelope>';
    
        dump($feedDoc);
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $urlFeedUpload);
        curl_setopt($curl, CURLOPT_UPLOAD, true);
        curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);
        curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: text/xml; charset=utf-8'));
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($curl, CURLOPT_BINARYTRANSFER, 1);
        curl_setopt($curl, CURLOPT_HEADER, false);
        curl_setopt($curl, CURLOPT_PUT, 1);
        curl_setopt($curl, CURLOPT_INFILE, fopen('data://text/plain,' . $fileContent, 'r'));
        curl_setopt($curl, CURLOPT_INFILESIZE, strlen($fileContent));
        #Only use below option on TEST environment if you have a self-signed certificate!!! On production this can cause security issues
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
        $response = curl_exec($curl);
        dump($response);
        curl_close($curl);
        $specificationNewFeed = new CreateFeedSpecification([
          'feed_type' => 'POST_PRODUCT_DATA',
          'marketplace_ids' => ['A1PA6795UKMFR9'],
          'input_feed_document_id' => $feedDocID
    
        ]);
    
    
        $responseFeed = $sdk->feeds()->createFeed(
          $accessToken,
          $region,
          $specificationNewFeed
        );
        dd($responseFeed);
      }