---
title: "Amazon Pay Integration Notes"
description: "A detailed guide to integrating Amazon Pay, covering key generation, frontend React button integration, backend C# SDK initialization, retrieving payment information, and implementing order authorization and capture."
canonical_url: "https://blog.markkulab.net/en/post/integrate-amazonpay"
author: "Mark Ku"
author_url: "https://blog.markkulab.net/en/author/mark-ku"
site: "Mark Ku's Tech Notes"
date_published: "2024-03-08 01:01:35 +0800"
category: "Payment"
tags: ["amazon pay", "payment", "checkout", "react", "csharp", "integration", "ecommerce"]
language: "en"
license: "CC BY 4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
attribution: "when reusing or quoting, credit the author and link back to the original"
---

# Amazon Pay Integration Notes

## 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.

## Related Documentation
*   [Integration Documentation](https://developer.amazon.com/docs/amazon-pay-checkout/get-set-up-for-integration.html)
*   [How to generate public/private keys](https://sellercentral.amazon.de/external-payments/amazon-pay/integration-central/lwa?ref=py_clientid_confcard_sboxhome_GB)

## Integration Flowchart
Amazon Pay Checkout is actually quite similar to PayPal Express Checkout.
![Amazonpay integration flow](https://blog.markkulab.net/content/markku/posts/integrate-amazonpay/images/amazonpay-integration-flow.png)

## First, Obtain the Necessary Amazon Pay Keys
### Next, go to the seller central > select Sandbox View > [Integration central](https://sellercentral.amazon.de/gp/pyop/seller/integrationcentral?ref=py_intcentr_confcard_sboxhome_GB)

![Find Integration central](https://blog.markkulab.net/content/markku/posts/integrate-amazonpay/images/find-integration-central.png)

### 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](https://blog.markkulab.net/content/markku/posts/integrate-amazonpay/images/integration-central-1.png)
If you don't have the "Self-developed" dropdown option, you can select "Woo Commerce" instead.
![Integration central - 2](https://blog.markkulab.net/content/markku/posts/integrate-amazonpay/images/integration-central-2.png)

### Following the official [Doc](https://developer.amazon.com/docs/amazon-pay-api-v2/manually-generating-key-pairs.html#generating-key-pair), 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](https://blog.markkulab.net/content/markku/posts/integrate-amazonpay/images/upload-public-key.png)
### On this screen, you'll find most of the keys you need.
![All key in here](https://blog.markkulab.net/content/markku/posts/integrate-amazonpay/images/keys.png)

### However, you'll need to find the Store ID and Client Secret on a different [page](https://sellercentral.amazon.de/external-payments/amazon-pay/integration-central/lwa?ref=py_clientid_confcard_sboxhome_GB).
![store id and secret](https://blog.markkulab.net/content/markku/posts/integrate-amazonpay/images/store-id-and-secret.png)

## 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](https://sellercentral.amazon.de/external-payments/sandbox/home). 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.

---

## About this article and its author

Originally published on [Mark Ku's Tech Notes](https://blog.markkulab.net/en/post/integrate-amazonpay)

License: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) — when reusing or quoting, credit the author and link back to the original

### About the author

**[Mark Ku](https://blog.markkulab.net/en/author/mark-ku)** — Software Solution Provider

- 10+ years senior software engineer, now an AI Builder
- Focused on large-platform architecture — North-American e-commerce, AI SaaS subscription billing
- Combining AI Agents and automation to build evolvable product foundations

### Free tools built by the author

All of these are free to use:

- [Free PDF Sign Tool](https://blog.markkulab.net/en/tools/pdf-sign): Online PDF sign tool — draw, type, or upload a signature, then drag, resize, and download. Everything runs in your browser; nothing is uploaded.
- [VS Code Refactory](https://blog.markkulab.net/en/tools/refactory): Refactory is a VS Code refactoring extension: 34 actions plus a 37-rule code-smell inspection layer with a Code Health dashboard, across 18 languages, backed by 534 tests. It learns your repo's conventions: where interfaces live, where DI is registered, whether 'use client' belongs. It ranks files by git churn × complexity so you know what to fix first, and hands any smell to the Claude Code already on your machine. Free to use, and your source never leaves your computer.
- [DB-Kit Database Manager](https://blog.markkulab.net/en/tools/db-kit): DB-Kit is a lightweight, cross-platform database manager built with Tauri + Rust + React. Manage MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, SQLite, MongoDB, Redis, Kafka, Elasticsearch and RabbitMQ from one consistent interface: passwords encrypted in the OS keychain, SSH tunnels, full CRUD, a visual query builder, stacked multi-statement result sets, cross-connection data transfer and compare/sync, Excel / CSV import & export, visualized execution plans, ER diagrams, scheduled backups, SQL stress testing with p50–p99 latency percentiles, a 15-rule SQL review engine, Kafka message browsing with monitoring & alerts, a bilingual UI (Traditional Chinese / English), a built-in AI assistant (natural-language SQL, AI review and tuning advice) and the dbk CLI. Free and open source (MIT), with installers for Windows, macOS and Linux.
- [VS Code Super Mermaid](https://blog.markkulab.net/en/tools/super-mermaid): Super Mermaid is a VS Code extension for beautiful Mermaid diagrams out of the box: auto-colored live preview, mouse pan & zoom, high-res PNG / SVG export, 21 templates and multiple themes. Free and open source (MIT).
- [React Super Mermaid](https://blog.markkulab.net/en/tools/react-super-mermaid): react-super-mermaid is an open-source React component library: render beautiful Mermaid diagrams with a single <MermaidViewer>, with built-in colorful / sketch themes, pan & zoom, in-diagram search, and high-res SVG / PNG export. Lightweight, SSR-safe, fully typed. Free and open source (MIT).
- [Jira / Confluence Super Mermaid](https://blog.markkulab.net/en/tools/jira-super-mermaid): An Atlassian Forge app: write Mermaid syntax directly inside a Jira issue or a Confluence page and get flowcharts, sequence diagrams, state machines and Gantt charts. 11 diagram types, SVG / PNG export, light and dark themes, full CJK support. Runs on Atlassian: your diagrams live in your own site and the app calls no third-party service. Free, coming soon to the Atlassian Marketplace.
- [Mermaid Live Preview](https://blog.markkulab.net/en/tools/mermaid-preview): Write Mermaid in your browser, see it render instantly, and share the whole diagram as a single link. No sign-up, nothing uploaded to a server, and mermaid.live share links work as-is.
- [React Intl Phone Number](https://blog.markkulab.net/en/tools/react-intl-phone-number): react-intl-phone-number is an open-source React component: framework-agnostic and antd-free, with E.164 in/out, a searchable flag / country-code dropdown, configurable validation levels (strict / mobile-strict / loose), themeable CSS, and i18n — phone logic powered by google-libphonenumber. Lightweight and fully typed. Free and open source (MIT).
- [Uptime Kuma Cluster](https://blog.markkulab.net/en/tools/uptime-kuma-cluster): Turn single-node Uptime Kuma into a highly available cluster: OpenResty + Lua smart load balancing, shared MariaDB state, health checks and automatic failover, plus cluster-management REST APIs. One Docker Compose command to start. Free and open source (MIT).
- [Special Education](https://blog.markkulab.net/en/education): Learning materials crafted for special education students

### Daily podcasts

- [Mark's Tech Insights — Daily AI News](https://blog.markkulab.net/en/category/tech-news): Daily curated AI and tech trends. Catch the latest developments via audio summaries — covering AI applications, software architecture, DevOps, and engineering practice. — RSS: https://blog.markkulab.net/feed.xml
- [AI股市蝦聊](https://blog.markkulab.net/en/category/ai-stock-chat): Every trading day, an AI-analyzed take on the Taiwan stock market, delivered as a two-host conversation covering the session and the next-day outlook. — RSS: https://blog.markkulab.net/ai-stock-chat/feed.xml
- [開源好物週報](https://blog.markkulab.net/en/category/open-source-weekly): A weekly two-host pick of free open-source tools surfaced from real Hacker News, GitHub, and Reddit buzz — what pain they solve and the fastest way to get started. — RSS: https://blog.markkulab.net/open-source-weekly/feed.xml

### Newsletter

[Subscribe to the newsletter](https://blog.markkulab.net/en/subscribe) — Be the first to know about new posts. No spam, unsubscribe anytime.
