<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lijo George</title>
    <description>The latest articles on DEV Community by Lijo George (@lijogrg444).</description>
    <link>https://dev.to/lijogrg444</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F210166%2Ffbfaf34b-fee1-44df-b6eb-13200cc4f262.png</url>
      <title>DEV Community: Lijo George</title>
      <link>https://dev.to/lijogrg444</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lijogrg444"/>
    <language>en</language>
    <item>
      <title>Amazon shut down PA-API, so I built an affiliate plugin that doesn't need it</title>
      <dc:creator>Lijo George</dc:creator>
      <pubDate>Sun, 25 Oct 2026 16:21:09 +0000</pubDate>
      <link>https://dev.to/lijogrg444/amazon-shut-down-pa-api-so-i-built-an-affiliate-plugin-that-doesnt-need-it-54b</link>
      <guid>https://dev.to/lijogrg444/amazon-shut-down-pa-api-so-i-built-an-affiliate-plugin-that-doesnt-need-it-54b</guid>
      <description>&lt;p&gt;I'm building a laptop review site and wanted the product boxes and comparison tables you get from plugins like AAWP. Then I found out the API those plugins run on has been switched off, and the replacement won't let a new site in until it's already making sales. So I built a WordPress plugin that works with no Amazon API at all.&lt;/p&gt;

&lt;p&gt;Most of the interesting decisions came from Amazon's rules rather than from WordPress, which is why this post spends more time on policy than you'd expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API is gone, and the new one has a sales gate
&lt;/h2&gt;

&lt;p&gt;Amazon's Product Advertising API 5.0 is &lt;a href="https://affiliate-program.amazon.com/creatorsapi/docs/en-us/paapiv5-deprecation" rel="noopener noreferrer"&gt;deprecated&lt;/a&gt;. Calling it now gets you a 403 and a message telling you to &lt;a href="https://affiliate-program.amazon.com/creatorsapi/docs/en-us/migrating-to-creatorsapi-from-paapi" rel="noopener noreferrer"&gt;move to the Creators API&lt;/a&gt;, which uses OAuth client credentials instead of the old AWS-style keys.&lt;/p&gt;

&lt;p&gt;Access to the Creators API depends on sales. According to &lt;a href="https://dev.to/th3nate/amazon-pa-api-v5-is-shutting-down-april-30-2026-here-is-what-changes-at-the-auth-layer-22ek"&gt;this write-up of Amazon's migration notice&lt;/a&gt;, you need final-accepted Associates status and at least 10 qualifying referred sales in the last 30 days. A site launching this month has none, so it has no API.&lt;/p&gt;

&lt;p&gt;That hurts more than it sounds, because without the API Amazon doesn't let you show prices, availability, customer reviews or star ratings. Most affiliate plugins are built around exactly those.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual mode
&lt;/h2&gt;

&lt;p&gt;Products live in a custom post type. Each one has an ASIN, my own photo, highlights and pros/cons I write myself, my own score out of 5, and spec fields like CPU, RAM, display and battery. From that one record the plugin renders a product box, a comparison table that turns into stacked cards on phones, a grid of cards for "top 3" sections, a numbered ranking list, and single fields or text links anywhere in a post.&lt;/p&gt;

&lt;p&gt;Everything is available as shortcodes and as two Gutenberg blocks. The blocks render on the server with &lt;code&gt;ServerSideRender&lt;/code&gt;, and the editor script is plain &lt;code&gt;wp.element.createElement&lt;/code&gt;, so there's no build step to maintain.&lt;/p&gt;

&lt;p&gt;Where a price would normally go, the button just says "Check price on Amazon".&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning Amazon's rules into code
&lt;/h2&gt;

&lt;p&gt;Every row here started as a sentence in the &lt;a href="https://affiliate-program.amazon.com/help/operating/policies" rel="noopener noreferrer"&gt;Associates Program Policies&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule (Associates Program Policies)&lt;/th&gt;
&lt;th&gt;What the plugin does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Links must be accessed directly from your site; no cloaking or hiding that a link goes to Amazon&lt;/td&gt;
&lt;td&gt;Every &lt;code&gt;href&lt;/code&gt; is the direct &lt;code&gt;amazon.com/dp/ASIN/?tag=&lt;/code&gt; URL. The custom URL field refuses bit.ly-style shorteners and &lt;code&gt;/go/&lt;/code&gt; redirects on your own domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prices and availability only via the API&lt;/td&gt;
&lt;td&gt;No price field at all. Saving a product warns if a price shows up in any text field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reviews and star ratings only via the API&lt;/td&gt;
&lt;td&gt;Only my own score is shown, and it's always labelled "Our rating"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product images from the API can't be stored&lt;/td&gt;
&lt;td&gt;Product images are my own uploads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disclose the affiliate relationship&lt;/td&gt;
&lt;td&gt;A disclosure line goes at the top of any post that contains an affiliate link&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No sub-tags that identify individual visitors&lt;/td&gt;
&lt;td&gt;No per-visitor tracking of any kind&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The price check was trickier than it looks, because half the keywords on a laptop site are things like "best laptops under $1,000". It has to catch &lt;code&gt;$899&lt;/code&gt; and leave &lt;code&gt;under $1,000&lt;/code&gt; alone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$re&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/(?&amp;lt;range&amp;gt;\b(?:under|below|less than|over|above|from|up to|around|max|budget)\s*)?'&lt;/span&gt;
    &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'(?:[$£€¥₹]\s?\d[\d,.]*|\b\d[\d,.]*\s?(?:USD|GBP|EUR|dollars|bucks)\b)/i'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A match with no range word in front of it counts as a price. The editor gets a warning rather than a hard block, since a regex like this will guess wrong now and then.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counting clicks without a redirect
&lt;/h2&gt;

&lt;p&gt;The normal way to count affiliate clicks is to link to something like &lt;code&gt;/go/product&lt;/code&gt;, log the hit and 302 the visitor to Amazon. The policies rule that out, because the link has to go to Amazon directly and can't hide where it leads.&lt;/p&gt;

&lt;p&gt;So the link stays untouched and a few lines of JavaScript send a beacon in the background when someone clicks it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;closest&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a[data-afb-p]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;afbP&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// product id&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;e&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;afbE&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// which element: table-button, grid-image...&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;afbClicks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendBeacon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;afbClicks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sendBeacon&lt;/code&gt; exists for this case. The browser delivers the request even while it's leaving the page, and the click itself isn't slowed down.&lt;/p&gt;

&lt;p&gt;On the server, a REST route checks the product exists and bumps a daily counter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;wp_afb_clicks&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clicks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;DUPLICATE&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;clicks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clicks&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The table has no cookies, IP addresses or user agents in it, only the day, post, product, element and a count. That's enough to see which posts and which buttons get clicked, and there's nothing personal in it to protect.&lt;/p&gt;

&lt;p&gt;Bots get filtered out by user agent. The first automated test I ran recorded zero clicks, which confused me until I remembered that headless Chrome sends &lt;code&gt;HeadlessChrome&lt;/code&gt; in its user agent and the filter was quietly throwing every one of them away.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;I didn't build geotargeting. Amazon's &lt;a href="https://affiliate-program.amazon.com/help/node/topic/GAGAL4V362SH7EC5" rel="noopener noreferrer"&gt;OneLink&lt;/a&gt; already sends international visitors to their local store and credits your local tracking IDs, so there was no reason to write my own.&lt;/p&gt;

&lt;p&gt;The next piece is a Creators API mode for when the site qualifies. The policies allow non-image content to be cached for up to 24 hours, so that sets how often it has to refresh.&lt;/p&gt;

&lt;p&gt;The plugin runs on &lt;a href="https://laptopowners.com" rel="noopener noreferrer"&gt;LaptopOwners&lt;/a&gt;, the laptop review site I'm building. If you've already moved a project onto the Creators API, I'm curious what the rate limits turned out to be like in practice.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was written with help from AI as english is not my first language. I checked the facts and code against my own plugin.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://affiliate-program.amazon.com/creatorsapi/docs/en-us/paapiv5-deprecation" rel="noopener noreferrer"&gt;PA-API 5 deprecation notice (Amazon)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://affiliate-program.amazon.com/creatorsapi/docs/en-us/migrating-to-creatorsapi-from-paapi" rel="noopener noreferrer"&gt;Migrating to Creators API from PA-API (Amazon)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/th3nate/amazon-pa-api-v5-is-shutting-down-april-30-2026-here-is-what-changes-at-the-auth-layer-22ek"&gt;Amazon PA-API v5 deprecates April 30, 2026 and retires May 15 (DEV)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://affiliate-program.amazon.com/help/operating/policies" rel="noopener noreferrer"&gt;Associates Program Policies (Amazon)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://affiliate-program.amazon.com/help/node/topic/GAGAL4V362SH7EC5" rel="noopener noreferrer"&gt;OneLink (Amazon Associates Help)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
