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

Apaapi Laravel Package

jakiboy/apaapi

APAAPI is a PHP wrapper for Amazon Product Advertising API v5, offering fluent methods for product searches, item lookups, browse nodes, and response handling. It simplifies credentials, request signing, and endpoint configuration for Laravel and standalone apps.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install the library plus a PSR-18 stack. apaapi is HTTP-client agnostic; it speaks PSR-18/PSR-17, so you must bring your own implementation. Guzzle is the path of least resistance in Laravel (it's usually already installed):

composer require jakiboy/apaapi guzzlehttp/guzzle

Without a PSR-18 client and PSR-17 request/stream factories bound, the first call fails with a "no HTTP client" style exception — that is the #1 first-run stumble.

2. Get credentials. You need an Amazon Associates/PA-API account with access_key, secret_key, and an approved partner_tag. Note the two auth paths the package advertises: classic PA-API 5.0 (SigV4) and the newer OAuth 2.0 / credential-less Creators API flow. Pick one deliberately — they are not interchangeable and the credential shape differs.

3. Put credentials in config, not code. Add to config/services.php:

'amazon' => [
    'access_key'  => env('AMAZON_ACCESS_KEY'),
    'secret_key'  => env('AMAZON_SECRET_KEY'),
    'partner_tag' => env('AMAZON_PARTNER_TAG'),
    'locale'      => env('AMAZON_LOCALE', 'US'),
],

4. First use case: keyword search returning titles + prices.

use Apaapi\Config;
use Apaapi\Client;
use Apaapi\Query;
use Apaapi\Operations\SearchItems;

$config = Config::create()
    ->setCredentials([
        'access_key'  => config('services.amazon.access_key'),
        'secret_key'  => config('services.amazon.secret_key'),
        'partner_tag' => config('services.amazon.partner_tag'),
    ])
    ->setLocalization(config('services.amazon.locale'))
    ->setHttpClient(new \GuzzleHttp\Client())
    ->setRequestFactory(new \GuzzleHttp\Psr7\HttpFactory())
    ->setStreamFactory(new \GuzzleHttp\Psr7\HttpFactory());

$operation = new SearchItems();
$query = (new Query())
    ->setKeywords('laravel book')
    ->setItemCount(10)
    ->setResources(['ItemInfo.Title', 'Images.Primary.Medium', 'Offers.Listings.Price']);
$operation->setQuery($query);

$response = Client::create($config)->execute($operation);
$data = json_decode((string) $response->getBody(), true);

Where to look first: the src/ tree — Config, Client, Operations/* (one class per PA-API operation), Query, Localization, Response, Exception. Then examples/ in the repo and the README's usage section. Operation classes are the map of everything the API can do; read the constructor/setters of Query to see which parameters are supported before hand-rolling arrays.

Note on naming: this library is small and its v2 API surface has churned. Treat the method names above as the shape of the API and confirm exact signatures against the README/tag you install — wrap it in your own service class (below) so a bump doesn't leak into 40 call sites.

Implementation Patterns

Bind once in a service provider. Never construct Config in a controller. A singleton is correct for the config object, but see the localization gotcha below.

// app/Providers/AmazonServiceProvider.php
public function register(): void
{
    $this->app->singleton(Client::class, function ($app) {
        $http = new \GuzzleHttp\Client(['timeout' => 10, 'connect_timeout' => 5]);
        $factory = new \GuzzleHttp\Psr7\HttpFactory();

        return Client::create(
            Config::create()
                ->setCredentials(config('services.amazon.credentials'))
                ->setLocalization(config('services.amazon.locale'))
                ->setHttpClient($http)
                ->setRequestFactory($factory)
                ->setStreamFactory($factory)
                ->setLogger($app['log'])
        );
    });
}

Wrap it in one application-level service. This is the single most valuable pattern. Your app should depend on AmazonCatalog::search() / ->items(), not on Apaapi\*:

class AmazonCatalog
{
    public function __construct(private Client $client) {}

    public function search(string $keywords, array $resources = self::DEFAULT): array
    {
        $op = new SearchItems();
        $op->setQuery((new Query())->setKeywords($keywords)->setItemCount(10)->setResources($resources));

        return $this->decode($this->client->execute($op));
    }
}

Benefits: cache keys, retries, logging, and response normalization all live in one place, and you can fake AmazonCatalog in feature tests without touching HTTP at all.

Batch with GetItems, don't loop. GetItems accepts up to 10 ASINs per request. A product-listing page with 40 ASINs = 4 calls, not 40. Combined with the TPS ceiling (below), this is the difference between a page load and a timeout.

Request the minimum Resources you need. The default response is thin; asking for ItemInfo.* and Offers.* on ten items produces hundreds of KB and slows every call. Declare a small constant set per screen (listing, detail, price-check) and reuse it.

Multi-marketplace: clone, don't mutate. If you support .fr and .de in one app, build a fresh Config/Client per locale rather than calling setLocalization() on the shared singleton. A mutated singleton silently returns French prices on a German page — and only under some request orderings, which makes it brutal to reproduce.

Cache aggressively, but lawfully. PA-API requires you to refresh pricing data on a schedule (roughly 24h max for price/availability) and forbids indefinite archiving of most offer data. A pragmatic layer:

return Cache::remember("amazon:item:{$asin}:{$locale}", now()->addHours(12), fn () => ...);

Store the fetch timestamp alongside the payload so you can render "price as of …", which the Associates operating agreement expects.

Respect the throughput limit in the queue, not the request. PA-API grants ~1 request/second with a small burst. A ThrottleRequests-style Redis lock around each job, or RateLimited job middleware with releaseAfter, keeps you off 429s when a bulk import fans out.

public function middleware(): array
{
    return [(new RateLimited('amazon-api'))->releaseAfter(1)];
}

Normalize the response into DTOs immediately. PA-API JSON is deeply nested (Items[0].ItemInfo.Title.DisplayValue) and shape-shifts when fields are missing. Map to AmazonItem value objects (asin, title, imageUrl, price, currency, url) at the boundary; every view and JSON resource downstream becomes trivial and testable.

Testing: Http::fake() is useless here — this isn't the Laravel HTTP client. Fake your AmazonCatalog wrapper for feature tests, and for unit tests inject a stub PSR-18 ClientInterface that returns canned JSON. Keep one recorded real response per operation as a fixture; that's your regression net.

Gotchas and Tips

  • It is not a Laravel package. No auto-discovery, no config/apaapi.php, no facade. Everything above is yours to write. Budget for it.
  • Missing PSR-17 factories. Installing only guzzlehttp/guzzle is usually enough (it ships PSR-7), but if you use Symfony HttpClient or a bare PSR-18 adapter you also need nyholm/psr7 or similar, and you must pass the same object as both request and stream factory.
  • Partner tag ↔ locale mismatch produces a 400 with an unhelpful message. The tag, the hostname, and the SigV4 region are a matched set (e.g. webservices.amazon.fr / eu-west-1). Let the library's Localization drive the endpoint; don't hand-set hosts.
  • Signature failures are almost always the clock. SigV4 rejects skewed timestamps. If calls work locally and 403 in a container, check NTP in that container before rewriting auth code.
  • Errors arrive as JSON in the body with an HTTP 4xx/5xx. getBody() doesn't throw on Errors — you must inspect $data['Errors'] ?? []. 429 means throttle; 400 InvalidParameterValue usually means a bad ASIN format or a resource name that doesn't exist for that operation.
  • The body is a stream. (string) $response->getBody() or you'll get a resource handle in your logs. Don't call getContents() twice on the same response.
  • ItemCount is capped at 10 and GetItems at 10 ASINs; anything above is silently clamped or rejected. Paginate with ItemPage rather than inflating counts.
  • Don't log the Authorization header. The library can attach a PSR-3 logger — if you enable debug logging, scrub headers. Also remember php artisan config:cache freezes env() calls: config must be read via config(), never env() inside the provider closure.
  • Credential-less/OAuth mode has a token lifecycle. If you're on the Creators API path, tokens expire and must be refreshed; cache the token in the cache store with an expiry margin, and never assume the first call of the day will succeed without a refresh.
  • ASIN vs ISBN vs UPC. SearchItems accepts Keywords broadly but GetItems is ASIN-only. Validate/normalize before dispatching to avoid burning your TPS on guaranteed 400s.
  • Extension points worth knowing: the injectable PSR-18 client (swap in a retrying/decorating client — e.g. one that retries 429/5xx with backoff — without touching the library), the PSR-3 logger, and the Response object exposing status, headers, and the built URI/request body — invaluable when Amazon rejects a request and you need to reproduce it with curl.
  • Compliance is a feature, not paperwork. Affiliate disclosure, rel="nofollow sponsored", image usage rules, and price-freshness requirements are all things a reviewer will check. Encode them in your Blade component/DTO so no developer can forget.
  • Version pinning. With 54 stars and a fast-moving 2.x, pin a minor version and read the changelog before composer update. Your service wrapper is what makes that upgrade a one-file change.
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.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels