🐱

FileMakerで SP-API の Feeds API を用いてAmazon在庫更新(後編)

2023/04/10に公開約13,800字

はじめに

前編からの続きになります。
https://zenn.dev/ontherocks_plz/articles/1df1da9288c6ba
今回の後編では、Feeds API を実行できる実際のスクリプトステップを元に、その詳細を解説していきたいと思います。

一つ注意点としては、
Feeds API を確実に実行できることを主としていますので、ハードコードしている箇所があります。SP-API の他のAPIでも使えるような応用を効かせたり、今後のAWS署名のバージョンに対応するには、本来は考慮すべき点が色々あることを予め断っておきます。

本題

前編でも記載したように Feeds API では、

  1. POST:在庫更新データ(XML)アップロード用のURLと識別IDの取得
  2. PUT:1.のURLに在庫更新データ(XML)をアップロード
  3. POST:1.の識別IDを用いて2.のアップロードしたデータに対して処理を指定
  4. GET:3.の処理状態を確認し、処理完了時の識別IDを取得
  5. GET:4.の識別IDを用いて、結果レポートIDを取得
  6. GET:5.の結果レポートIDを用いて、結果レポートを取得

これらが一つのセットとなる手順になりますが、その中で2.6.以外の1.3.4.5.で署名付きリクエストが必要になります。が、ぶっちゃけ、署名さえ作成できればそれぞれのリクエスト自体は単純な仕組みです。

POSTの際の、cURLオプションの例をあげてみます。

--request "POST"
--header "Authorization: AWS4-HMAC-SHA256 Credential=××××××××××××/20230402/us-west-2/execute-api/aws4_request, SignedHeaders=content-type;host;x-amz-access-token;x-amz-date, Signature=×××××××××××××××××××××"
--header "content-type:application/json"
--header "host:sellingpartnerapi-fe.amazon.com"
--header "x-amz-access-token:×××××××××××××××××××××××"
--header "x-amz-date:20230402T145138Z"
--data "@$requestBody"

この中の、長い Authorization ヘッダーの最後、Signature=×××××××××××××××××××××署名の箇所になりますが、スクリプトの構造としては cURLオプション を作成するサブスクリプトを作り、Feeds API の各メインスクリプトからそのサブスクリプトを呼び出すという形が、全体としてスッキリすると思います。

サブスクリプトで必要な要素

それぞれの署名を作成する上で、どのリクエストでも変わらず必要になるのが、

  • AccessKey(IAMユーザーに発行するアクセスキー)
  • SecretKey(同上、シークレットキー)
  • AccessToken(リフレッシュトークンを用いて取得)

※各取得方法の詳細は今回は省きます。

この3つです。
そして、それぞれの署名を作成する上でリクエスト毎に変わり、且つ必要になるのが、

  • HTTPMethod(GET、POST、などの種類)
  • CanonicalUri(リクエストURLの一部)
  • SignedHeaders(content-typeやhostなどのヘッダーリスト)
  • requestBody(リクエストする際の実態データ)

この4つです。
ですので、メインスクリプトからは変動するこの4つをサブスクリプトに引き渡し、cURLオプションを作成して貰うことになります。

では、
実際の Feeds API 各メインスクリプト1.~6.を順に見ていきます(サブスクリプトは最後)。

1. POST:在庫更新データ(XML)アップロード用のURLと識別IDの取得

これは、このようになります。

3~6行目:サブスクリプトへ渡す4つの要素の設定

$SignedHeaders
content-type;host;x-amz-access-token;x-amz-date
$requestBody
{"contentType":"text/xml;charset=UTF-8"}

$requestBody の記述に注意です。
content-Type ではありません。ハイフン無しの contentType です。

9行目:サブスクリプトへ渡す引数の準備

$arguments
JSONSetElement ( "" ;

[ "HTTPMethod" ; $HTTPMethod ; 1 ];
[ "CanonicalUri" ; $CanonicalUri ; 1 ];
[ "SignedHeaders" ; $SignedHeaders ; 1 ];
[ "requestBody" ; $requestBody ; 1 ]

 )

12~13行目:サブスクリプトへ引数 $arguments を渡し $cURLoption を作成させます。
16行目:作成された $cURLoption を元にリクエストします。

リクエストURL
"https://sellingpartnerapi-fe.amazon.com" & $CanonicalUri
リクエストオプションの設定
$cURLoption

成功すると、このように feedDocumentId(3.で必要) と url(2.で必要) が返却されます。

{
	"feedDocumentId" : "amzn××××××××××××××××××××××××××××××××",
	"url" : "https://tortuga-prod-fe.s3-us-west×××××××××××××××××××××××××××××"
}

2. PUT:1.のURLに在庫更新データ(XML)をアップロード

これは、単純に1.で返却されたURLにxmlを $requestBody としてPUTするだけですが、xmlデータの作り方はMWSの時と全く同じです。ですので、こちらのAmazonのxmlデータの箇所を参考にして下さい。
https://zenn.dev/ontherocks_plz/articles/3df44a4c8d345f#amazon

PUT時の URLから挿入 のリクエストオプションは、このようになります。

リクエストオプションの設定
List ( 

"--request " & Quote ( "PUT" );
"--dump-header " & Quote ( "$dumpHeader" );
"--header " & Quote ( "content-type:text/xml;charset=UTF-8" );
"--data " & Quote ( "@$requestBody" )

 )

$dumpHeader として返却ヘッダーを取得格納していますが、PUTの場合、エラーでなければレスポンスボディは空です。ですので、一応ヘッダーを取得しHTTPコードを確認するようにしています。

$dumpHeaderの例
HTTP/1.1 200 OK
x-amz-id-2: ×××××××××××××××××××××=
x-amz-request-id: ×××××××××
Date: Mon, 10 Apr 2023 05:57:23 GMT
x-amz-version-id: ××××××××××××××××××
x-amz-expiration: expiry-date="Mon, 10 Jul 2023 00:00:00 GMT", rule-id="Rule for: NinetyDays"
x-amz-server-side-encryption: aws:kms
x-amz-server-side-encryption-aws-kms-key-id: arn:aws:kms:us-west-2:××××××××××××××××××
x-amz-server-side-encryption-bucket-key-enabled: true
ETag: "××××××××××××××××××"
Server: AmazonS3
Content-Length: 0

成功すれば、このように HTTP/1.1 200 OK となります。

3. POST:1.の識別IDを用いて2.のアップロードしたデータに対して処理を指定

これは、このようになります。

4行目:$CanonicalUri
6行目:$requestBody

この2行が1.の時と変わりますので、注意です。

$requestBody
JSONSetElement ( "" ;

[ "feedType" ; "POST_INVENTORY_AVAILABILITY_DATA" ; 1 ];
[ "marketplaceIds[0]" ; "A1VC38T7YXB528" ; 1 ];
[ "inputFeedDocumentId" ; "amzn××××××××××××××××××××××××××××××××" ; 1 ]

 )

marketplaceIds キーの値は、複数の国に出店しているのであれば、各国のIDを配列に順に入れる必要があります。日本でしか展開していなければ上記の記述で問題なしです。
https://developer-docs.amazon.com/sp-api/docs/marketplace-ids
inputFeedDocumentId キーの値には、1.で取得した feedDocumentId を設定します。

9~13行目:サブスクリプトへ引数を渡して返却させる、というどれも共通の処理です。
16行目:1.と同じです(URLから挿入 のレスポンス格納先ターゲット変数は変えてます)。

リクエストに成功すると、

{
	"feedId" : "469999457"
}

こんなレスポンスになり、feedId(4.で必要)が返却されます。

閑話休題

取り合えず、ここまでどうでしょうか。
肝心の署名作成を含むサブスクリプトの紹介がまだですので、アレかもしれませんが、1.~3.を順に行うことで一先ずは、在庫更新データをAmazonにぶん投げた状態になります。

そのぶん投げたデータが正常に処理されたかどうかは、4.~6.を行って確認する必要があります。
実際に、xmlデータを作成する際にSKUの記述が間違っていて更新されていなかったというのがありました。1.~3.が問題なく行えてても、正常に処理されるとは限りません。

4. GET:3.の処理状態を確認し、処理完了時の識別IDを取得

Feeds API は非同期処理ですので、更新処理の確認が必要になります。更新負荷が高い場合は、処理にやや時間がかかるようです。ですので、この4.の処理確認を行うのは3.の後、少し置いた方が良いです。

3~6行目:今までとガラっと変わりますので注意です。
4行目:$CanonicalUri は、3.で返却された feedId を結合したものになります。
5行目:GET ですので content-type ヘッダーは不要です。
6行目:当然、$requestBody は空です(が、この空が必要です)。
9~13行目:サブスクリプトへ引数を渡して返却させる、というどれも共通の処理です。
16行目:ここも特に何も変わりません(ターゲット変数は変えてます)。

リクエストURL
"https://sellingpartnerapi-fe.amazon.com" & $CanonicalUri
リクエストオプションの設定
$cURLoption

リクエストが成功した場合、次のようなレスポンスになります。

{
	"createdTime" : "2023-04-10T05:57:25+00:00",
	"feedId" : "469999457",
	"feedType" : "POST_INVENTORY_AVAILABILITY_DATA",
	"marketplaceIds" : [ "A1VC38T7YXB528" ],
	"processingEndTime" : "2023-04-10T05:58:10+00:00",
	"processingStartTime" : "2023-04-10T05:57:31+00:00",
	"processingStatus" : "DONE",
	"resultFeedDocumentId" : "amzn1.tortuga.××××××××××××××"
}

resultFeedDocumentId に注目して下さい。
このキーがレスポンスに含まれていない場合は、まだ3.の処理が完了していないことになります。その場合のレスポンスは次のようになります。

{
	"createdTime" : "2023-04-10T05:57:25+00:00",
	"feedId" : "469999457",
	"feedType" : "POST_INVENTORY_AVAILABILITY_DATA",
	"marketplaceIds" : [ "A1VC38T7YXB528" ],
	"processingStatus" : "IN_QUEUE"
}

resultFeedDocumentId キーの値が、次の5.で必要になります。

5. GET:4.の識別IDを用いて、結果レポートIDを取得


4行目:4.で返却された resultFeedDocumentId を結合したものになります。
5行目:GET なので content-type ヘッダーはふy...
6行目:当然、$requestBody は空です(が、この空がひつy...)。
9~13行目:サブスクリプトへ引数を渡して返却させる、というどれも共通の処理です。
16行目:特に何も変わりません(ターゲット変数は変えてます)。

リクエストに成功すると、

{
	"feedDocumentId" : "amzn1.tortuga.××××××××××××××",
	"url" : "https://tortuga-prod-fe.s××××××××××××××××××××××××"
}

このようなレスポンスになります。
feedDocumentId は4.のIDです、つまりこのIDの結果レポートは url で取得して下さい。ということになります。ちなみに、このURLには有効期限があります。もたもたしていると、叩いてもエラーが返却されます(

6. GET:5.の結果レポートIDを用いて、結果レポートを取得

もう後は、5.の url を叩くだけです。
一つ注意点があるとすれば、URLから挿入 は、デフォルトでは POST になります。きちんと GET を指定してあげましょう。

リクエストオプション
"--request " & Quote ( "GET" )

成功すれば、結果がxmlで返却されます。

<?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.02</DocumentVersion>
		<MerchantIdentifier>××××××××</MerchantIdentifier>
	</Header>
	<MessageType>ProcessingReport</MessageType>
	<Message>
		<MessageID>1</MessageID>
		<ProcessingReport>
			<DocumentTransactionID>469999457</DocumentTransactionID>
			<StatusCode>Complete</StatusCode>
			<ProcessingSummary>
				<MessagesProcessed>1</MessagesProcessed>
				<MessagesSuccessful>1</MessagesSuccessful>
				<MessagesWithError>0</MessagesWithError>
				<MessagesWithWarning>0</MessagesWithWarning>
			</ProcessingSummary>
		</ProcessingReport>
	</Message>
</AmazonEnvelope>

DocumentTransactionID は、3.で返却された feedId ですね。

署名というラスボスを含むcURLoption作成

さて、やってまいりました。
1.3.4.5.と何度も呼び出されていた $cURLoption を作成するサブスクリプト。
むしろ、こっちが本体と言っても過言ではありません。

でも、
このサブスクリプトをしっかり作っておくことで、1.3.4.5.のスクリプトがスッキリします。と言うか...カスタム関数化するのが一番いいかもしれません(

※カスタム関数化したものは、またいずれ掲載します。
※しました(
https://zenn.dev/ontherocks_plz/articles/b29311d89a472e

ではスクリプトを見ていきます。
こちらの公式のリファレンスと変数名称や手順などを合わせていますので、見比べながら進めてみて下さい。
https://docs.aws.amazon.com/ja_jp/general/latest/gr/create-signed-request.html


3~9行目:メインスクリプトから受け取った引数を、それぞれセットします。
12~14行目:どんな時も変わらず必要になる要素です。

手順1:正規リクエストを作成する

19行目:テキストを挿入 を使って正規リクエストのフォーマット $CanonicalRequestFormat を設定します。後に、これに対して全置換をかけて正規リクエスト $CanonicalRequest を作成します。

$CanonicalRequestFormat
~HTTPMethod
~CanonicalUri

~CanonicalHeader

~SignedHeaders
~HashedPayload

21行目:$x_amz_date、日付(ISO8601)になります。ゼロ埋め処理もされている、前編で紹介させて頂いた株式会社フルーデンス 小巻さんの計算式を利用させて頂きます。

$x_amz_date
Let ( 

[
  ~timestamp_local = Get ( タイムスタンプ ) ;
  ~timestamp = GetAsTimestamp ( Int ( Get ( 現在の時刻 UTC ミリ秒 ) / 1000 ) ) ;
  ~date = GetAsDate ( ~timestamp ) ;
  ~dateY = Year ( ~date ) ;
  ~dateM = Right ( "0" & Month ( ~date ) ; 2 ) ;
  ~dateD = Right ( "0" & Day ( ~date ) ; 2 ) ;
  ~time = GetAsTime ( ~timestamp ) ;
  ~timeH =  Right ( "0" & Hour ( ~time ) ; 2 ) ;
  ~timeM = Right ( "0" & Minute ( ~time ) ; 2 ) ;
  ~timeS = Right ( "0" & Seconds ( ~time ) ; 2 )
] ;

  ~dateY & ~dateM & ~dateD & "T" & ~timeH & ~timeM & ~timeS & "Z"

)

23行目:$CanonicalHeader を作成します。引数として受け取る $SignedHeaderscontent-type;host;x-amz-access-token;x-amz-date といった形ですので、これを改行区切りに変更し、それぞれ値を全置換で設定します。

$CanonicalHeader
Substitute ( $SignedHeaders ; 

[ ";" ;];
[ "content-type" ; "content-type:application/json" ];
[ "host" ; "host:sellingpartnerapi-fe.amazon.com" ];
[ "x-amz-access-token" ; "x-amz-access-token:" & $AccessToken ];
[ "x-amz-date" ; "x-amz-date:" & $x_amz_date  ]

 )

例えば、POST の場合 $CanonicalHeader の中はこのようになります。

計算結果の例
content-type:application/json
host:sellingpartnerapi-fe.amazon.com
x-amz-access-token:××××××××××××××××××
x-amz-date:20230410T132507Z

GET の場合、$SignedHeaders の中に content-type がないのでこうなります。

計算結果の例
host:sellingpartnerapi-fe.amazon.com
x-amz-access-token:××××××××××××××××××
x-amz-date:20230410T132507Z

25行目:$requestBody のハッシュ $HashedPayload(小文字の 16 進数文字)を求めます。GET の時は当然 $requestBody は空ですが、その「空」でのハッシュが必要になる為、空で問題ありません。

28行目:$CanonicalRequestFormat に全置換をかけて $CanonicalRequest を設定します。

$CanonicalRequest
Substitute ( $CanonicalRequestFormat ;

[ "~HTTPMethod" ; $HTTPMethod ];
[ "~CanonicalUri" ; $CanonicalUri ];
[ "~CanonicalHeader" ; $CanonicalHeader ];
[ "~SignedHeaders" ; $SignedHeaders ];
[ "~HashedPayload" ; $HashedPayload ];

[; Char ( 10 ) ]

 )

改行コードが違う為、Char ( 10 ) への置換を忘れないで下さい。

手順2:正規リクエストのハッシュを作成する

33行目:正規リクエスト $CanonicalRequest のハッシュ $HashedCanonicalRequest(小文字の 16 進数文字)を求めます。

手順3:署名文字列を作成する

38行目:$StringSignFormat 全置換用フォーマット作成

$StringSignFormat
~Algorithm
~RequestDateTime
~CredentialScope
~HashedCanonicalRequest

40行目:$StringSignFormat に全置換をかけて $StringSign を作成

$StringSign
Substitute ( $StringSignFormat ;

[ "~Algorithm" ; "AWS4-HMAC-SHA256" ] ;
[ "~RequestDateTime" ; $x_amz_date ] ;
[ "~CredentialScope" ; Left ( $x_amz_date ; 8 ) & "/us-west-2/execute-api/aws4_request" ];
[ "~HashedCanonicalRequest" ; $HashedCanonicalRequest ] ;

[; Char ( 10 ) ]

 )

こちらも、改行コードが違う為 Char ( 10 ) への置換を忘れないで下さい。

手順4:署名を計算する

45行目:署名 $CalculateTheSignature の計算をします。

$CalculateTheSignature
Let ( 

[
	~hmacDate = CryptAuthCode ( Left ( $x_amz_date ; 8 ) ; "SHA256" ; "AWS4" & $SecretKey );
	~hmacRegion = CryptAuthCode ( "us-west-2" ; "SHA256" ; ~hmacDate );
	~hmacService = CryptAuthCode ( "execute-api" ; "SHA256" ; ~hmacRegion );
	~hmacSigning = CryptAuthCode ( "aws4_request" ; "SHA256" ; ~hmacService )
];

Lower ( HexEncode ( CryptAuthCode ( $StringSign ; "SHA256" ; ~hmacSigning ) ) )

 )

もうファルシのルシがコクーンでパージですが、頑張りましょう。

auth

50行目:ようやく Authorization ヘッダーの値が完成です。

$auth
"AWS4-HMAC-SHA256 " & 
"Credential=" & $AccessKey & "/" & Left ( $x_amz_date ; 8 ) & "/us-west-2/execute-api/aws4_request," & 
"SignedHeaders=" & $SignedHeaders & "," & 
"Signature=" & $CalculateTheSignature

一つ罠がありまして、リファレンスには「アルゴリズム(AWS4-HMAC-SHA256)と Credential の間にカンマはありません。」と記載がありますが、カンマはなくとも半角開けずに詰め詰めにするとエラーになります ガッデム!

そして伝説へ・・・

56行目:メインスクリプトへ返却する $cURLoption の設定

$cURLoption
List ( 

"--request " & Quote ( $HTTPMethod );
"--header " & Quote ( "Authorization: " & $auth );
"--header " & Substitute ( $CanonicalHeader ;;& "--header " );
Case ( not IsEmpty ( $requestBody ) ; "--data " & Quote ( "@$requestBody" ) )

 )

Substitute ( $CanonicalHeader ; ¶ ; ¶ & "--header " ) としているのは、$CanonicalHeader を利用して、header を作る為です。

$CanonicalHeaderの例
content-type:application/json
host:sellingpartnerapi-fe.amazon.com
x-amz-access-token:××××××××××××××××××
x-amz-date:20230410T132507Z

"--header " & Substitute ( $CanonicalHeader ; ¶ ; ¶ & "--header " ) の計算結果の例
--header content-type:application/json
--header host:sellingpartnerapi-fe.amazon.com
--header x-amz-access-token:××××××××××××××××××
--header x-amz-date:20230410T132507Z

まとめ

Feeds API は、一括でSKUの在庫更新できる利点があります。その代わり、手順が多いです。Feeds API を組み上げるのはちょっと辛いな...という方は、PATCH 処理を検討してみてはいかがでしょうか。レート制限に注意しループで回すだけですので、まだお手軽です。

ちなみに、Feeds API も当然レート制限がありますので注意です(

AccessKey・SecretKey・AccessToken の取得の仕方、それからPATCH 処理などは、また機会があれば、記事化したいと思います。

おわりに

いかがだったでしょうか。
AWS のラスボスの署名に比べたら、他の auth なんてもう何ともないですね、過程が天と地ほどの差、何が来ても驚かない。

でも、何が辛かったかって SP-API のリファレンスが全編英語...これが一番辛かったですね。
高校で赤点を取ってたくらい英語が本当に苦手ですから(

でも、悲しいけどEC通販土方なのよね、乗り越えるしかない。

もし不明点があれば、TwitterでコソーリDM下さい!

それでは
Let's enjoy FileMaker!

Discussion

ログインするとコメントできます