Migrating From Amazon MWS API to Amazon SP-API

amazon mws api amazon sp-api dotnet

There was always this ongoing joke with the Amazon MWS API. One of the first things you might notice when looking at the docs is Version 2009-01-01. I started working with the API in 2016 or 2017, and even back then 2009 seemed ancient. Here we are in 2022.

  • Imagine having to use an API from 2009…
  • Imagine writing an API in 2009 that is still being used today…

However you want to look at it, I personally was glad for the migration because it meant modernization. Almost all of MWS responses were in XML and made no use of its validation or other features like attributes if at all, so you just end with these huge response sizes. The SP-API now uses JSON responses.

Migration so far has been straightforward. While you might not always be able to reuse every class from your previous library due to naming differences, most of the endpoints are a 1-to-1 conceptual match to the previous API, with some authentication differences.

Authentication

Authenticating requests with the SP-API now requires that you sign requests using AWS SigV4. The process involves signing the request you plan on sending with your AWS secret key so that Amazon can verify the secret key and also your request parameters.

  1. Authenticate with Login With Amazon using your AWS Client ID and Secret Key that is associated to your seller account. I used the client from Amazon. Call this token lwaAccessToken.
  2. Create a canonical request - the request you want to send with its payload hashed.
var requestDate = new DateTime(2022, 1, 1);
var headers = new Dictionary<string, string>
{
    { "host", "sellingpartnerapi-na.amazon.com" },
    { "x-amz-date", requestDate.ToString("yyyy-MM-dd") }
};

string method = "GET";
string uri = HttpUtility.UrlEncode("/finances/v0/financialEventGroups");
string queryParams = HttpUtility.UrlEncode("FinancialEventGroupStartedAfter=2022-01-01&FinancialEventGroupStartedBefore=2022-03-01");
string headerString = string.Join("\n", headers.Select(x => $"{x.Key}:{x.Value}")));
string signedHeaders = string.Join(";", headers.Select(x => x.Key)));
string body = ""; // Empty string for GET
string hashedBody = ComputeSha256Hash(body); // SHA256 Hash of body (Uses System.Security.Cryptography.SHA256)

var parts = new List<string>() { method, uri, queryParams, headerString, signedHeaders, hashedBody };
var canonicalRequest = string.Join("\n", parts);
var canonicalRequestHashed = ComputeSha256Hash(canonicalRequest);

This creates a hashed string of the request we want to send. This is basically establishing a contract with Amazon saying that this hashed string represents what is being sent. When we send the actual request, all parameters must match. If for example, the parameters in the actual request were different, the hash Amazon generates on their end would not match, and they would then know that the request was tampered with.

  1. Now for the signature, we need to build another string with our hashed canonical request.
var stringToSignParts = new List<string>()
{
    "AWS4-HMAC-SHA256", // never changes
    "20220101T000000Z", // Timestamp
    "20220101/us-east-1/execute-api/aws4_request", // Only the date stamp changes
    canonicalRequestHashed
};
var stringToSign = string.Join("\n", stringToSignParts);
  1. To do the signing, we need a key that is computed using HMACSHA256. You need to chain together the key + string value pairs to get the final signing key.
// ComputeHmacSha256(key, stringValue) uses System.Security.Cryptography.HMACSHA256
var dateKey = ComputeHmacSha256(Encoding.UTF8.GetBytes("AWS4" + secretKey), "20220101")
var regionKey = ComputeHmacSha256(dateKey, "us-east-1"); // The region
var awsServiceKey = ComputeHmacSha256(regionKey, "execute-api"); // AWS Service
var finalSigningKey = ComputeHmacSha256(awsServiceKey, "aws4_request"); // Final signing key
  1. The signature is calculated by again using HMACSHA256:
var signature = ComputeHmacSha256(finalSigningKey, stringToSign);
  1. You can then add it to the headers that will be sent in the actual request.
headers.Add("Authorization", $"AWS4-HMAC-SHA256 Credential={lwaAccessToken}/20220101/us-east-1/execute-api/aws4_request, SignedHeaders=host;x-amz-date, Signature={signature}"

The request is now ready to send.

Generating client libraries

Unlike the MWS API, there is no .NET library to use. So instead you need to generate the request/response objects using swagger-codegen.

You may be able to write some of the objects manually, but at least for the Finances API, you should use codegen because it has 79 objects. Not a surprise to me though because the MWS API was the same. Any class you see suffixed with Event is a different financial event that can occur for an Amazon Seller. Part of me wishes that these transactions could be normalized to a simple class, but there are so many different types of line items that introducing these types sort of makes sense.

AdjustmentEvents represent situations where Amazon might credit your account due to something like a lost item. ShipmentEvents are customer orders, which have line items for each product and fee.

To generate these, you need java and maven installed. You also need a copy of the swagger-codegen repo and a copy of the Finances API model (or whichever API model you want to generate files for). Then from the swagger-codegen repo root:

java -jar modules/swagger-codegen-cli/target/swagger-codegen-cli.jar generate -i path/to/model/finances.json -l csharp -o path/to/output/finances-api

The result is below. Note that by default, swagger-codegen adds its own namespace and uses Newtonsoft.Json. I simply did a text replacement on all of these with our own namespace and System.Text.Json.

Finances API models

AdjustmentEvent.cs
AdjustmentEventList.cs
AdjustmentItem.cs
AdjustmentItemList.cs
AffordabilityExpenseEvent.cs
AffordabilityExpenseEventList.cs
ChargeComponent.cs
ChargeComponentList.cs
ChargeInstrument.cs
ChargeInstrumentList.cs
CouponPaymentEvent.cs
CouponPaymentEventList.cs
Currency.cs
DebtRecoveryEvent.cs
DebtRecoveryEventList.cs
DebtRecoveryItem.cs
DebtRecoveryItemList.cs
DirectPayment.cs
DirectPaymentList.cs
Error.cs
ErrorList.cs
FBALiquidationEvent.cs
FBALiquidationEventList.cs
FeeComponent.cs
FeeComponentList.cs
FinancialEventGroup.cs
FinancialEventGroupList.cs
FinancialEvents.cs
ImagingServicesFeeEvent.cs
ImagingServicesFeeEventList.cs
ListFinancialEventGroupsPayload.cs
ListFinancialEventGroupsResponse.cs
ListFinancialEventsPayload.cs
ListFinancialEventsResponse.cs
LoanServicingEvent.cs
LoanServicingEventList.cs
NetworkComminglingTransactionEvent.cs
NetworkComminglingTransactionEventList.cs
PayWithAmazonEvent.cs
PayWithAmazonEventList.cs
ProductAdsPaymentEvent.cs
ProductAdsPaymentEventList.cs
Promotion.cs
PromotionList.cs
RemovalShipmentAdjustmentEvent.cs
RemovalShipmentAdjustmentEventList.cs
RemovalShipmentEvent.cs
RemovalShipmentEventList.cs
RemovalShipmentItem.cs
RemovalShipmentItemAdjustment.cs
RemovalShipmentItemList.cs
RentalTransactionEvent.cs
RentalTransactionEventList.cs
RetrochargeEvent.cs
RetrochargeEventList.cs
SAFETReimbursementEvent.cs
SAFETReimbursementEventList.cs
SAFETReimbursementItem.cs
SAFETReimbursementItemList.cs
SellerDealPaymentEvent.cs
SellerDealPaymentEventList.cs
SellerReviewEnrollmentPaymentEvent.cs
SellerReviewEnrollmentPaymentEventList.cs
ServiceFeeEvent.cs
ServiceFeeEventList.cs
ShipmentEvent.cs
ShipmentEventList.cs
ShipmentItem.cs
ShipmentItemList.cs
ShipmentSettleEventList.cs
SolutionProviderCreditEvent.cs
SolutionProviderCreditEventList.cs
TaxWithheldComponent.cs
TaxWithheldComponentList.cs
TaxWithholdingEvent.cs
TaxWithholdingEventList.cs
TaxWithholdingPeriod.cs
TrialShipmentEvent.cs
TrialShipmentEventList.cs