Mark Ku's Blog

Background

This is my second time working on an Amazon Pay integration. The first time was right after I joined the company, helping to fix an issue where Amazon Pay was constantly dropping orders. This time, I'm re-integrating Amazon Pay for a German e-commerce site, and I've taken this opportunity to refine the integration techniques.

Integration Flowchart

Amazon Pay Checkout is actually quite similar to PayPal Express Checkout. Amazonpay integration flow

First, Obtain the Necessary Amazon Pay Keys

Next, go to the seller central > select Sandbox View > Integration central

Find Integration central
Find Integration central

I have accounts for two different countries, and after comparing them, I found a bug here. One of the accounts couldn't find the "Self-Developed" option, so the "Create keys" button (for uploading a public key) wasn't displayed.

Integration central - 1 If you don't have the "Self-developed" dropdown option, you can select "Woo Commerce" instead. Integration central - 2

Following the official Doc, generate an RSA public and private key.

ssh-keygen -t rsa -b 2048 -m PKCS8 -f privateKey.pem
ssh-keygen -f privateKey.pem -e -m PKCS8 > publicKey.pub

The first option seems to be broken; it only generates a public key and doesn't let you download the private key. Please choose the second option and upload your Amazon Pay public key yourself.

Upload public key
Upload public key

On this screen, you'll find most of the keys you need.

All key in here
All key in here

However, you'll need to find the Store ID and Client Secret on a different page.

store id and secret
store id and secret

Important Notes for Testing

  • Because Amazon Pay's payment flow involves multiple redirects, you need HTTPS and your own domain. I used a Cloudflare Tunnel as a reverse proxy for testing.
  • It's quite convenient that the production and test keys are the same. To use the Sandbox environment, you just need to set isSandbox to true during initialization.
  • Regarding test accounts, go to the seller central, switch to Sandbox view, and you'll find Test accounts. You can add sandbox test accounts here.

Frontend Example - React (Next.js App Router)

'use client';
import { PaymentReturnType } from '@/const/cart/payment-image';
import { PaymentOptions } from '@/const/payment/payment-option';
import {
    useBuildPaymentInfoMutation,
    useCaptureMutation,
    useLazyGetConfigQuery,
    useProcessMutation,
} from '@/redux/api/test-payment-apiSlice';
import { IGeneralPaymentInfo, IGeneralPaymentResult, IGernalPaymentParams } from '@/typing/cart';
import { ApiResponse } from '@/typing/common';
import { useSearchParams } from 'next/navigation';
import { useEffect, useRef } from 'react';
import { v4 as uuidv4 } from 'uuid';

export default function TestAmaonPay() {
    const gernalPaymentParams: IGernalPaymentParams = {
        paymentTypeCode: PaymentOptions[PaymentOptions.Paypal],
        orderNo: uuidv4(),
    } as IGernalPaymentParams;

    const [GetConfig, result] = useLazyGetConfigQuery(); // rtk query 取得sdk 初始化的參數

    const [Process] = useProcessMutation(); // rtk query 用來呼叫建立建立授權訂單的api

    const [Capture] = useCaptureMutation(); // rtk query 用來呼叫提取信用及完成訂單的api
    const [BuildPaymentInfoMutation] = useBuildPaymentInfoMutation(); // rtk query 用來取得Amazon Pay 付款資訊

    const amazonCheckoutSessionId = useRef('');
    const amazonHasRedirectedBack = useRef(false);

    const searchParams = useSearchParams();

    const loadAmazonPay = () => {
        const script = document.createElement('script');

        script.src = 'https://static-eu.payments-amazon.com/checkout.js';
        script.async = true;
        script.onload = () => {
            // debugger;
            if (window.amazon && window.amazon.Pay) {
                GetConfig(gernalPaymentParams.paymentTypeCode)
                    .unwrap()
                    .then((data: any) => {
                        if (!data) {
                            return;
                        }
                        window.amazon.Pay.renderButton('#amazonpaybutton', {
                            merchantId: data.merchantId,
                            sandbox: data.isSandbox === 'True', // dev environment
                            ledgerCurrency: data.ledgerCurrency, // Amazon Pay account ledger currency
                            checkoutLanguage: 'en_GB', // render language
                            productType: 'PayAndShip', // checkout type
                            placement: 'Cart', // button placement
                            buttonColor: 'Gold',
                            createCheckoutSessionConfig: {
                                payloadJSON: data.payloadJSON,
                                signature: data.signature,
                                publicKeyId: data.publicKeyId,
                            },
                        });
                    });
            }
        };
        document.body.appendChild(script);
    };

    const getPaymentInfoFromToken = (token: string) => {
        const params: IGernalPaymentParams = { ...gernalPaymentParams, paymentGatewaySessionID: token };

        BuildPaymentInfoMutation(params)
            .unwrap()
            .then((res: ApiResponse<IGeneralPaymentInfo>) => {
                document.getElementById('paymentInfo')!.innerText = JSON.stringify(res.data);
            });
    };

    useEffect(() => {
        // Dynamically load the Amazon Pay script
        loadAmazonPay();
        const session = searchParams?.get('amazonCheckoutSessionId') || '';

        const isComplete = searchParams?.get('isComplete') || '';

        if (session) {
            amazonCheckoutSessionId.current = session;
            amazonHasRedirectedBack.current = true;

            if (!isComplete) {
                getPaymentInfoFromToken(session);
            } else {
                const params: IGernalPaymentParams = {
                    ...gernalPaymentParams,
                    paymentGatewaySessionID: session,
                };

                Capture(params)
                    .unwrap()
                    .then((res: ApiResponse<IGeneralPaymentResult>) => {
                        if (res.isSuccess) {
                            alert('Payment success');
                        }
                    });
            }
        }
    }, []); // Empty dependency array means this effect runs once on mount

    const clickHandler = () => {
        const params: IGernalPaymentParams = {
            ...gernalPaymentParams,
            paymentGatewaySessionID: amazonCheckoutSessionId.current,
        };

        Process(params)
            .unwrap()
            .then((res: ApiResponse<IGeneralPaymentResult>) => {
                // debugger;
                if (res.isSuccess && res.data.paymentReturnType === PaymentReturnType.RedirectUrl) {
                    location.href = res.data.paymentReturnValue;
                }
            });
    };

    return (
        <div>
            <h1>test amazon pay</h1>
            <div id="amazonpaybutton"></div>
            <div id="paymentInfo"></div>

            <button
                className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
                onClick={clickHandler}
            >
                Checkout
            </button>
        </div>
    );
}

Backend Example - C#

First, install the Amazon.Pay.API.SDK NuGet package.

Define the backend configuration - Appsetting.json

"Payment": {
    "PaymentOptions": [
 {
   "PaymentName": "AmazonPay",
   "ClientId": "Your clientId", // live and sandbox are the same
   "Secret": "Your clientId secret", // live and sandbox are the same
   "IsSandbox": true,
   "StoreID": "",
   "StoreName": "Your storename",
   "MerchantID": "Your MerchantID", // from amazon 
   "PublicKeyID": "Upload to amazon backoffice PublicKeyID", // 將公鑰上傳amazon pay 產上的ID
   "PrivateKey": "Your private key", // 透過電腦自己產的公鑰,base64 加密起來就不用額外檔案
   "EndPoint": "https://api.paypal.com"
  }
 ]
 }

Initialize the Amazon SDK's WebStoreClient. Since it's used frequently later on, I've extracted it into its own function.

 private WebStoreClient InitiateClient()
 {
    var payConfiguration = new ApiConfiguration
    (
        region: Region.Europe,
        environment: GeneralPaymentConfig.IsSandbox ? Amazon.Pay.API.Types.Environment.Sandbox : Amazon.Pay.API.Types.Environment.Live,
        publicKeyId: GeneralPaymentConfig.PublicKeyID,
        privateKey: System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(GeneralPaymentConfig.PrivateKey))
    );

    var client = new WebStoreClient(payConfiguration);

    return client;
 }

Fetch the required parameters for initializing the Amazon button from the backend.

 public async Task<Result<Dictionary<string, string>>> GetConfig(string returnUrl = "")
 {
    var req = HttpContext.Current.Request;
    var result = new Result<Dictionary<string, string>> { IsSuccess = false, Message = "" };
    try
    {
       if (string.IsNullOrWhiteSpace(returnUrl))
       {
          returnUrl = "/en/test-payment/";
       }

       string ChangePaymentReferID = string.Empty;
       var isChangePayment = !string.IsNullOrEmpty(ChangePaymentReferID);  // for change payment

       var client = InitiateClient();
       var request = new CreateCheckoutSessionRequest(
           checkoutReviewReturnUrl: AppSettingsConstVars.FrontendUrl + returnUrl + (isChangePayment ? $"/payments?id={ChangePaymentReferID}" : ""),
           storeId: GeneralPaymentConfig.StoreID
       );
       request.PaymentDetails.CanHandlePendingAuthorization = false;
       request.DeliverySpecifications.AddressRestrictions.Type = RestrictionType.Allowed;
       request.DeliverySpecifications.AddressRestrictions.AddCountryRestriction("DE");

       //generate the button signature
       var signature = client.GenerateButtonSignature(request);
       var payload = request.ToJson();

       result.Data.Add("signature", signature);
       result.Data.Add("payloadJSON", payload);
       result.Data.Add("publicKeyId", GeneralPaymentConfig.PublicKeyID);
       result.Data.Add("merchantId", GeneralPaymentConfig.MerchantID);
       result.Data.Add("isSandbox", GeneralPaymentConfig.IsSandbox.ToString()); // need test 
       result.Data.Add("ledgerCurrency", HardCodeKey.BasedCurrency);

       result.Success();
    }
    catch (Exception ex)
    {
       NLogUtil.WriteSEQLog($"[Paypal][GetConfig]Error:{ex.Message},StackTrace:{ex.StackTrace}", NLog.LogLevel.Error);
    }

    return result;
 }

Next, implement fetching the shipping address.

 public async Task<Result<GeneralPaymentInfo>> GetPaymentInfoAsync(GernalPaymentParameter paymentParameter)
 {
    Result<GeneralPaymentInfo> result = new Result<GeneralPaymentInfo> { IsSuccess = false, Message = "" };

    try
    {
       var client = InitiateClient();
       var getInfo = client.GetCheckoutSession(paymentParameter.PaymentGatewaySessionID);

       if (getInfo == null)
       {
          throw new Exception($"[AmazonPayService][GetPaymentInfoAsync]:{getInfo.RawResponse}");
       }

       result.Data.Address = ConvertToGeneralAddress(getInfo.ShippingAddress, getInfo.Buyer.Email);

       result.Success();
    }
    catch (Exception ex)
    {
       NLogUtil.WriteSEQLog($"[Paypal][GetPaymentInfoAsync]Error:{ex.Message},StackTrace:{ex.StackTrace}", NLog.LogLevel.Error);
    }

    return result;
 }
 
 private GeneralAddress ConvertToGeneralAddress(Address amazonShipAddress, string Mail)
{
   GeneralAddress generalAddress = new GeneralAddress();
   generalAddress.Line1 = amazonShipAddress.AddressLine1;
   generalAddress.Line2 = (string.IsNullOrEmpty(amazonShipAddress.AddressLine2) && string.IsNullOrEmpty(amazonShipAddress.AddressLine3)) ? "" : string.Join(",", amazonShipAddress.AddressLine2, amazonShipAddress.AddressLine3);
   generalAddress.FirstName = GetMaxLengthStr(ParseName(amazonShipAddress.Name).Item1, 80);
   generalAddress.LastName = GetMaxLengthStr(ParseName(amazonShipAddress.Name).Item2, 80);
   generalAddress.CountryCode = amazonShipAddress.CountryCode;
   generalAddress.PostalCode = amazonShipAddress.PostalCode;
   generalAddress.City = amazonShipAddress.City;
   generalAddress.StateProvinceCode = amazonShipAddress.StateOrRegion;
   generalAddress.Phone = amazonShipAddress.PhoneNumber;
   return generalAddress;
}

private string GetMaxLengthStr(string Text, int Max)
{
   if (string.IsNullOrWhiteSpace(Text))
   {
      return "";
   }

   return Text.Substring(0, Math.Min(Max, Text.Length));
}

private Tuple<string, string> ParseName(string Name)
{
   if (string.IsNullOrWhiteSpace(Name))
   {
      return Tuple.Create("", "");
   }

   var names = Name.Split(' ');

   if (names.Length == 1)
   {
      return Tuple.Create(Name, "");
   }

   var last = names.Last();

   return Tuple.Create(Name.Replace(last, "").Trim(), last);
} 

Get the Amazon Transaction Page URL (PurchaseGetAmazonPayRedirectUrl)

 public async Task<Result<GeneralPaymentResult>> ProcessAsync(GernalPaymentParameter paymentParameter, string returnUrl)
 {
    Result<GeneralPaymentResult> result = new Result<GeneralPaymentResult> { IsSuccess = false, Message = "Please select another credit card or change payment method and try again." };

    try
    {
       var client = InitiateClient();
       var request = new UpdateCheckoutSessionRequest();

       if (string.IsNullOrWhiteSpace(returnUrl))
       {
          returnUrl = "/en/test-payment/?isComplete=true";
       }

       request.WebCheckoutDetails.CheckoutResultReturnUrl = AppSettingsConstVars.FrontendUrl + returnUrl;

       var currencyCode = (Currency)Enum.Parse(typeof(Currency), HardCodeKey.BasedCurrency);

       request.PaymentDetails.ChargeAmount.Amount = HardCodeKey.Payment.TestAmount;
       request.PaymentDetails.ChargeAmount.CurrencyCode = currencyCode;
       request.PaymentDetails.CanHandlePendingAuthorization = false;
       request.PaymentDetails.PaymentIntent = Authorize ? PaymentIntent.Authorize : PaymentIntent.AuthorizeWithCapture;

       NLogUtil.WriteSEQLog($"[AmazonPay][AmazonV2Helper][PurchaseGetAmazonPayRedirectUrl][UpdateCheckoutSession][Authorize]:{Authorize}", NLog.LogLevel.Info);
       request.MerchantMetadata.MerchantReferenceId = paymentParameter.OrderNo;

       request.MerchantMetadata.MerchantStoreName = GeneralPaymentConfig.StoreName;
       request.MerchantMetadata.NoteToBuyer = GetProductDescription();

       var updateResult = client.UpdateCheckoutSession(paymentParameter.PaymentGatewaySessionID, request);

       if (updateResult.Success)
       {
          result.Data.PaymentReturnType = PaymentReturnType.RedirectUrl;
          result.Data.PaymentReturnValue = updateResult.WebCheckoutDetails.AmazonPayRedirectUrl;
          result.Success();

          // TODO Remove this log when stable
          NLogUtil.WriteSEQLog($"[AmazonPayService][ProcessAsync][UpdateCheckoutSession][Success]{JsonConvert.SerializeObject(updateResult)}", NLog.LogLevel.Info);
       }
       else
       {
          NLogUtil.WriteSEQLog($"[AmazonPayService][ProcessAsync][UpdateCheckoutSession][Error]Request:{JsonConvert.SerializeObject(request)},Response:{JsonConvert.SerializeObject(updateResult)}", NLog.LogLevel.Error);
       }
    }
    catch (Exception ex)
    {
       NLogUtil.WriteSEQLog($"[Paypal][ProcessAsync]Error:{ex.Message},StackTrace:{ex.StackTrace}", NLog.LogLevel.Error);
    }

    return result;
 }

Complete the Order and Capture Payment

 public virtual async Task<Result<GeneralPaymentResult>> CaptureAsync(GernalPaymentParameter paymentParameter)
 {
    var result = new Result<GeneralPaymentResult> { IsSuccess = true, Message = "No need implemented" };

    try
    {
       var currencyCode = (Currency)Enum.Parse(typeof(Currency), HardCodeKey.BasedCurrency);
       var client = InitiateClient();
       var request = new CompleteCheckoutSessionRequest(HardCodeKey.Payment.TestAmount, currencyCode);
       CheckoutSessionResponse complete = client.CompleteCheckoutSession(paymentParameter.PaymentGatewaySessionID, request);

       if (complete.Success)
       {
          var paymentTransition = new GeneralPaymentTransition();
          paymentTransition.Authcode = complete.ChargeId;
          paymentTransition.TransactionID = complete.ChargeId;
          paymentTransition.Amount = HardCodeKey.Payment.TestAmount.ToString(); // todo
          paymentParameter.PaymentTypeCode = GeneralPaymentConfig.PaymentName;
          result.Data.PaymentTransition = paymentTransition;
          NLogUtil.WriteSEQLog($"[Paypal][CaptureAsync][Complete][Success]complete:{JsonConvert.SerializeObject(complete)}", NLog.LogLevel.Info);
          result.Success();
       }
       else
       {
          NLogUtil.WriteSEQLog($"[Paypal][CaptureAsync][Complete][Fail]complete:{JsonConvert.SerializeObject(complete)}", NLog.LogLevel.Error);
       }            
    }
    catch (Exception ex)
    {
       NLogUtil.WriteSEQLog($"[Paypal][CaptureAsync]Error:{ex.Message},StackTrace:{ex.StackTrace}", NLog.LogLevel.Error);
    }

    return result;
 }

In Conclusion

The Amazon Pay integration method feels quite dated, with all the page redirects. Plus, features in the admin panel often break. Having integrated it twice now, I can say it's not a very smooth process.

Author

Mark Ku

擁有 10+ 年經驗的資深軟體工程師,現為 AI 應用 Builder,專注於大型平台架構與簡化複雜系統設計,從電商系統到訂閱與收費平台,結合 AI Agent、AI 整合與自動化開發,打造高效率且可持續演進的產品技術基礎。Read More

Found this useful?

The author's free tools, daily podcasts and newsletter are all here.

Mark Ku · This article is licensed under CC BY 4.0. Credit the author and link back to the original when reusing it.

Comments

Subscribe to Newsletter

Subscribe to get new posts delivered instantly — never miss a tech share.

By submitting, you agree to receive emails. You can anytime.

Popular Posts

View all
Mark Ku
··602

Oracle Cloud Always Free Tier: Linux Host and Static IP for a $0 Cloud Solution

Oracle Cloud Always Free Tier: Linux Host and Static IP for a $0 Cloud Solution
Mark Ku
··490

Say Goodbye to Postman's Fee Trap! A Hands-on Guide to Bruno, the Open-Source Git-Native API Testing Powerhouse.

Say Goodbye to Postman's Fee Trap! A Hands-on Guide to Bruno, the Open-Source Git-Native API Testing Powerhouse.
Mark Ku
··333

A Free, Open-Source, Notion-like Knowledge Base — A Complete Guide to Deploying and Backing Up Outline Wiki

A Free, Open-Source, Notion-like Knowledge Base — A Complete Guide to Deploying and Backing Up Outline Wiki
Mark Ku
··264

Training Your Own AI Voice: Hardware Requirements, Open-Source Model Comparison, and LoRA Fine-Tuning

Training Your Own AI Voice: Hardware Requirements, Open-Source Model Comparison, and LoRA Fine-Tuning
Mark Ku
··221

Building an Efficient API Management Platform: Deploying Kong Gateway from Scratch - Part 1

Building an Efficient API Management Platform: Deploying Kong Gateway from Scratch - Part 1
Mark Ku
··215

Setting Up Samba on Ubuntu to Share Folders with Windows 11

Setting Up Samba on Ubuntu to Share Folders with Windows 11