Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Paapi5 Php Sdk Laravel Package

thewirecutter/paapi5-php-sdk

A PHP SDK for Amazon's Product Advertising API 5.0, offering generated client classes for product search, item lookup, and browse nodes with request signing and typed responses. Useful for Laravel/PHP apps integrating Amazon Associates product data.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require thewirecutter/paapi5-php-sdk

The fork keeps Amazon's generated namespace layout, so all classes live under Amazon\ProductAdvertisingAPI\v1\.... Confirm the installed version's namespace if you have trouble autoloading — forks occasionally reshuffle the PSR-4 map.

What you need before writing a line of code

  • Access Key + Secret Key from the Associates Central → Tools → Product Advertising API page.
  • Your Partner Tag (associate tag), e.g. yourtag-20.
  • The host + region pair for your marketplace. These two values are tightly coupled and must both be correct:
Marketplace Host Region
US webservices.amazon.com us-east-1
CA webservices.amazon.ca us-east-1
UK webservices.amazon.co.uk eu-west-1
DE/FR/IT/ES webservices.amazon.{de,fr,it,es} eu-west-1
JP webservices.amazon.co.jp us-west-2
AU webservices.amazon.com.au us-west-2
IN webservices.amazon.in eu-west-1

First working request

use Amazon\ProductAdvertisingAPI\v1\Configuration;
use Amazon\ProductAdvertisingAPI\v1\Api\DefaultApi;
use Amazon\ProductAdvertisingAPI\v1\com\amazon\paapi5\v1\SearchItemsRequest;
use Amazon\ProductAdvertisingAPI\v1\com\amazon\paapi5\v1\SearchIndex;
use Amazon\ProductAdvertisingAPI\v1\com\amazon\paapi5\v1\SearchItemsResource;

$config = new Configuration();
$config->setAccessKey(config('paapi.access_key'));
$config->setSecretKey(config('paapi.secret_key'));
$config->setPartnerTag(config('paapi.partner_tag'));
$config->setRegion('us-east-1');
$config->setHost('webservices.amazon.com');

$api = new DefaultApi(new \GuzzleHttp\Client(['timeout' => 10]), $config);

$request = (new SearchItemsRequest())
    ->setPartnerTag(config('paapi.partner_tag'))
    ->setKeywords('standing desk')
    ->setSearchIndex(SearchIndex::ALL)
    ->setItemCount(10)
    ->setResources([
        SearchItemsResource::ITEM_INFOTITLE,
        SearchItemsResource::IMAGESPRIMARYMEDIUM,
        SearchItemsResource::OFFERSLISTINGSPRICE,
    ]);

$response = $api->searchItems($request);

First use case to build: a ProductLookup service that takes an ASIN and returns title / image / price as a plain array. Don't call the SDK from controllers — wrap it immediately so you can swap caching, retries, and error handling later.

Order of exploration: Configuration (credentials + host) → DefaultApi (the only class you call) → the *Request classes (immutable-ish fluent setters) → the *Response classes (heavily nested, mostly null until you request the resource).

Implementation Patterns

1. One configured API instance per marketplace, built from config

The Configuration object is mutable and cheap — build a factory rather than a singleton so multi-marketplace sites (US + UK) can coexist:

final class PaapiFactory
{
    public static function make(string $marketplace): DefaultApi
    {
        $m = config("paapi.markets.{$marketplace}");
        $config = new Configuration();
        $config->setAccessKey($m['access_key']);
        $config->setSecretKey($m['secret_key']);
        $config->setPartnerTag($m['partner_tag']);
        $config->setRegion($m['region']);
        $config->setHost($m['host']);
        $config->setDebug(false);

        return new DefaultApi(
            new Client(['timeout' => 8, 'connect_timeout' => 3]),
            $config
        );
    }
}

2. Always enumerate setResources() explicitly

PA-API 5.0 returns nothing you didn't ask for. This is the single biggest source of "the SDK is broken" tickets — the response object deserializes fine but every property is null.

Group resources into a constant per use case so call sites stay readable:

const LISTING_RESOURCES = [
    SearchItemsResource::ITEM_INFOTITLE,
    SearchItemsResource::ITEM_INFOFEATURES,
    SearchItemsResource::IMAGESPRIMARYLARGE,
    SearchItemsResource::OFFERSLISTINGSPRICE,
    SearchItemsResource::OFFERSLISTINGSMERCHANTINFO,
    SearchItemsResource::BROWSENODEINFOBROWSENODES,
];

3. Chunk GetItems — hard limit of 10 ASINs

$results = collect($asins)
    ->chunk(10)
    ->flatMap(function ($chunk) use ($api) {
        $req = (new GetItemsRequest())
            ->setPartnerTag($this->partnerTag)
            ->setItemIds($chunk->values()->all())
            ->setResources(self::LISTING_RESOURCES)
            ->setCondition('Any');
        try {
            return $api->getItems($req)->getItemsResult()->getItems() ?? [];
        } catch (ApiException $e) {
            report($e);
            return [];
        }
    });

4. Normalize to your own DTOs immediately

The SDK response tree is five or six levels deep (getItemsResult()->getItems()[0]->getItemInfo()->getTitle()->getDisplayValue()). Map once into a Product value object so the rest of the app never imports an Amazon\... class. This also lets you write tests without the SDK.

private function mapItem($item): ?Product
{
    $title = $item->getItemInfo()?->getTitle()?->getDisplayValue();
    if (!$title) return null; // partially-accessible item

    return new Product(
        asin: $item->getASIN(),
        title: $title,
        price: $item->getOffers()?->getListings()[0]?->getPrice()?->getAmount(),
        imageUrl: $item->getImages()?->getPrimary()?->getLarge()?->getURL(),
    );
}

5. Cache aggressively — your quota is the real bottleneck

PA-API gives you roughly 1 request/second and a daily cap tied to your affiliate revenue. Every SearchItems call should go through a cache keyed on marketplace + operation + normalized request payload:

$key = 'paapi:'.md5($marketplace.serialize($request->__toString() ?: $request));

return Cache::remember($key, now()->addHours(6), fn () => $api->searchItems($request));

Cache the response object, not just the mapped array — then changing your mapping layer doesn't invalidate the cache. Serialize with json_encode($response) and json_decode(..., true) into arrays, or store the raw toArray() output; regenerating typed objects from arrays isn't worth it for read paths.

6. Retry decorator for 429 / 503

The SDK throws ApiException for both throttling and genuine errors. Distinguish by status code and back off with jitter — never retry blindly in a tight loop, you'll dig the hole deeper.

private function withRetry(callable $fn, int $attempts = 3)
{
    foreach (range(1, $attempts) as $i) {
        try {
            return $fn();
        } catch (ApiException $e) {
            $code = $e->getCode();
            if (!in_array($code, [429, 503]) || $i === $attempts) throw $e;
            usleep((2 ** $i) * 250_000 + random_int(0, 100_000));
        }
    }
}

7. Push bulk work to the queue

Any "sync 5,000 ASINs nightly" job must be queued with a Redis::throttle('paapi')->allow(1)->every(1) guard or a dedicated single-worker queue. Doing it inside a web request will time out and burn quota.

8. Handle Errors payloads that arrive alongside results

GetItems can succeed (200) while individual ASINs are listed in ItemsResult->Errors (e.g. ItemNotAccessible, InvalidParameterValue). Check both the top-level result and the per-item error list; don't assume a non-throwing call means every ASIN resolved.

9. Testing

Stub DefaultApi in the container or inject it behind your own interface. The generated classes are final-ish and constructor-heavy, so mocking the SDK directly is painful — mocking your ProductRepository is trivial.

Gotchas and Tips

  • Region/host mismatch produces signature or AccessDenied errors, not a helpful message. Double-check the table above. us-east-1 is correct for CA and BR despite them being separate marketplaces — the region is an AWS region, not a country.
  • Empty arrays everywhere is usually a missing setResources() call, not an auth problem. Verify with a single known-good ASIN and an explicit ItemInfo.Title before debugging credentials.
  • setPartnerTag() must be called on the request too, not just on Configuration. Recent versions of the generated request classes default it from config in some code paths and not others — set it explicitly every time. Mismatched tags surface as InvalidPartnerTag / AssociateValidationFailure.
  • ApiException::getResponseBody() is where the real error lives. The exception message is generic ("[400] Client error"). Log the decoded body — it contains __type, Message, and a RequestId that Amazon support will ask for.
catch (ApiException $e) {
    Log::warning('paapi', [
        'status'  => $e->getCode(),
        'body'    => json_decode($e->getResponseBody(), true),
        'request' => $request,
    ]);
}
  • TotalResultCount and ItemCount are lies. They're estimates and are frequently far larger than what you can actually page to. PA-API caps SearchItems at 10 items per page and 10 pages of pagination (page 1–10), so a "full catalog scrape" is impossible by design.
  • Pagination uses setItemPage() (1-indexed), not tokens. If you need more than 100 results you must vary the query (keywords, BrowseNodeId, price ranges) and dedupe.
  • NoResults is a 200 with an empty SearchResult, while NoResults on GetItems for a single ASIN returns a per-item error. Handle both shapes.
  • Credentials must never touch the repo. Put them in config/services.php fed from env, and add them to your .env.example as blanks. Also add a startup check that fails loudly in production if any of the four values is empty — silent misconfig is hard to notice because the SDK will happily sign with a blank key and return a 401.
  • Don't enable SDK debug output in production. Configuration::setDebug(true) can dump headers, including your Authorization header. If you need request tracing, log only the RequestId from the response or exception.
  • Guzzle timeout is your friend. The SDK does not set one. Without it a hung connection blocks a whole queue worker. Set timeout and connect_timeout when constructing the client.
  • Response objects are large. Caching a 10-item SearchItems response as JSON is often 50–200 KB. If you cache to Redis, compress (gzencode) or cache only the mapped arrays; if you cache to the filesystem, be aware of inode pressure on high-cardinality query sets.
  • The SDK is generated code — don't patch it. If you hit a bug (Guzzle major-version incompatibility is the classic one), pin the fork's version, or wrap the call and post-process. Subclassing generated request classes breaks on upgrade.
  • ItemIds must be ASINs, not URLs or SKUs, and are case-sensitive — uppercase them before calling or you'll get InvalidParameterValue for valid products.
  • BrowseNodeInfo lookups are a separate rate-limited resource. GetBrowseNodes counts against the same quota as SearchItems; cache browse node trees for days, not hours — they barely change.
  • Extension point worth knowing: the only real seam is your own service class. Build PaapiClient (raw SDK calls, retries, logging), ProductRepository (mapping + caching), and controllers on top. That's the layer that lets you add a second marketplace, swap in Rainforest/Keepa as a fallback, or migrate off PA-API without touching business logic.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package