DEV Community

Lijo George
Lijo George

Posted on

Amazon shut down PA-API, so I built an affiliate plugin that doesn't need it

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.

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.

The API is gone, and the new one has a sales gate

Amazon's Product Advertising API 5.0 is deprecated. Calling it now gets you a 403 and a message telling you to move to the Creators API, which uses OAuth client credentials instead of the old AWS-style keys.

Access to the Creators API depends on sales. According to this write-up of Amazon's migration notice, 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.

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.

Manual mode

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.

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

Where a price would normally go, the button just says "Check price on Amazon".

Turning Amazon's rules into code

Every row here started as a sentence in the Associates Program Policies.

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

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 $899 and leave under $1,000 alone:

$re = '/(?<range>\b(?:under|below|less than|over|above|from|up to|around|max|budget)\s*)?'
    . '(?:[$£€¥₹]\s?\d[\d,.]*|\b\d[\d,.]*\s?(?:USD|GBP|EUR|dollars|bucks)\b)/i';
Enter fullscreen mode Exit fullscreen mode

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.

Counting clicks without a redirect

The normal way to count affiliate clicks is to link to something like /go/product, 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.

So the link stays untouched and a few lines of JavaScript send a beacon in the background when someone clicks it:

document.addEventListener('click', function (ev) {
  var a = ev.target.closest && ev.target.closest('a[data-afb-p]');
  if (!a) return;
  var data = new FormData();
  data.append('p', a.dataset.afbP);          // product id
  data.append('e', a.dataset.afbE || '');    // which element: table-button, grid-image...
  data.append('post', String(afbClicks.post));
  navigator.sendBeacon(afbClicks.url, data);
}, true);
Enter fullscreen mode Exit fullscreen mode

sendBeacon exists for this case. The browser delivers the request even while it's leaving the page, and the click itself isn't slowed down.

On the server, a REST route checks the product exists and bumps a daily counter:

INSERT INTO wp_afb_clicks (day, post_id, product_id, element, clicks)
VALUES (%s, %d, %d, %s, 1)
ON DUPLICATE KEY UPDATE clicks = clicks + 1;
Enter fullscreen mode Exit fullscreen mode

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.

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 HeadlessChrome in its user agent and the filter was quietly throwing every one of them away.

What's next

I didn't build geotargeting. Amazon's OneLink already sends international visitors to their local store and credits your local tracking IDs, so there was no reason to write my own.

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.

The plugin runs on LaptopOwners, 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.

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.

Sources

Top comments (0)