dawson/amazon-ecs
A Laravel package for Amazon ECS (Product Advertising API) that provides a fluent wrapper for searching products, fetching item details, and working with Amazon catalog data. Configure your credentials and associate tag, then query Amazon without handling request signing yourself.
Install the wrapper and publish its config:
composer require dawson/amazon-ecs:^2.2.1
php artisan vendor:publish --provider="Dawson\AmazonEcs\AmazonEcsServiceProvider"
Pin to ^2.2.1 or later — that release restores SearchIndex handling (#23), and earlier 2.x tags silently break the search path described below.
The first place to look is config/amazon-ecs.php and the package's src/AmazonEcs.php — the public surface is small (search, item lookup, browse-node lookup, similarity lookup), so reading the class takes minutes. Wire credentials through .env:
AMAZON_ECS_ACCESS_KEY=AKIA...
AMAZON_ECS_SECRET_KEY=...
AMAZON_ECS_ASSOCIATE_TAG=youraffiliate-20
AMAZON_ECS_COUNTRY=com
Then use the facade for the smallest viable use case — looking up a single known ASIN to enrich a product page:
use Dawson\AmazonEcs\Facades\AmazonEcs;
$result = AmazonEcs::lookup('B00EXAMPLE', ['ResponseGroup' => 'Large']);
$title = (string) $result->Items->Item->ItemAttributes->Title;
If that call succeeds, the package is wired correctly. If it returns a signature error instead, stop and read the PA API 5.0 note in Gotchas — that is the single largest risk with this package today.
For broader use, the next step is AmazonEcs::search($keywords, ['SearchIndex' => 'Electronics']) to seed a catalog, or feed the results of a browse-node lookup into a category browser. Verify on 2.2.1 that the SearchIndex you pass actually shows up in the outgoing request — that was the exact regression #23 fixed.
Inject the service, not the facade. The facade is fine in a controller, but for queue jobs and domain services resolve Dawson\AmazonEcs\AmazonEcs from the container. This makes the class swappable for a fake in tests without fighting the facade root.
Wrap it in one repository class. Don't scatter AmazonEcs:: calls across the app. A single AmazonProductRepository that returns plain arrays or DTOs is the cleanest boundary — it hides XML, handles retries, and gives you one place to migrate to PA-API 5.0 later.
Cache aggressively, keyed by ASIN. Product data moves slowly. A 24-hour cached lookup is almost always fine:
$product = Cache::remember("amazon:asin:{$asin}", now()->addDay(), fn () => $this->fetch($asin));
Use a short negative cache (5–15 min) for empty responses so you don't hammer Amazon on a dead ASIN.
Queue bulk operations with a throttle. Amazon limits PA-API to roughly one request per second per associate tag. A job with public $backoff = [2, 5, 10]; and a rate limiter handles this cleanly:
RateLimiter::for('amazon-ecs', fn () => Limit::perSecond(1));
Normalize the XML immediately. Convert SimpleXMLElement into arrays before persisting. json_decode(json_encode($node), true) is the pragmatic route for small trees, but for anything nested it's brittle — prefer explicit field extraction (Title, ASIN, DetailPageURL, LargeImage->URL, ListPrice->Amount). Never store raw SimpleXML in the database or the cache.
Use ResponseGroup to control payload size. Small for search result lists, Medium or Large only for detail pages. Offers is what you want when you actually care about price and availability; it's often empty for third-party listings, so design your UI to tolerate missing prices.
Search is index-driven — always pass SearchIndex. Since 2.2.1 the argument is forwarded again, but a search without it still returns unfiltered or InvalidParameterValue results depending on the endpoint. Centralize the allowed indexes in config rather than hardcoding them at call sites.
Multi-region. The config is global, but the underlying client accepts a config array in its constructor. For a marketplace switcher, instantiate additional clients with country overridden rather than rebinding the singleton.
This targets the legacy Product Advertising API (4.0), not PA-API 5.0. Amazon moved to PA-API 5.0 with different endpoints, SigV4-only signing, and a new approval process. Credentials provisioned after the transition will not work against this package's endpoints. This is the make-or-break issue for the whole library — verify against live credentials before committing. If it fails, treat dawson/amazon-ecs as a learning reference and use paapi5/paapi5-php-sdk or a hand-rolled SigV4 signer behind the same repository interface.
SearchIndex was broken before 2.2.1. Issue #23 was fixed in 2.2.1; on earlier tags the parameter is dropped or mangled, which looks like an Amazon-side rejection ("no results for an obviously valid query") rather than a library bug. If a colleague reports that search "just returns everything," check composer.lock first. There is no other behavioural change in 2.2.1 — it is a single targeted fix, so nothing else in your integration needs revisiting.
Access is not self-service. Even with valid keys, PA-API access requires an active Associates account with qualifying sales (historically three in 180 days). New accounts frequently get InvalidClientTokenId or AccessDenied even when the code is perfect. Log the raw XML error body before deciding the code is wrong.
Errors often arrive as HTTP 200 with an <Errors> node. Always check:
if (isset($result->Errors)) {
Log::warning('Amazon ECS error', [(string) $result->Errors->Error->Code]);
}
RequestThrottled and TooManyRequests are the two you'll see most; both are retryable with backoff, unlike InvalidParameterValue.
Clock skew breaks signatures. If the host's clock drifts more than a few minutes you get SignatureDoesNotMatch with no other hint. ntpdate/chronyd on the box is worth checking before you touch the signing code.
Associate tag is mandatory for Search, BrowseNodeLookup, and SimilarityLookup — but not ItemLookup. Missing tags surface as cryptic AWS.InvalidParameterValue responses. If you keep one env var unset for local dev, expect only lookups to work.
Config caching. This package reads its config from config('amazon-ecs.*'); using env() directly inside your own code after php artisan config:cache will return null. Route everything through config.
Testing. Http::fake() won't intercept the underlying Guzzle client reliably. Instead bind a fake subclass — or better, inject a fake AmazonProductRepository into feature tests and unit-test only the XML-normalization against recorded fixture files. Capture a search fixture and a lookup fixture; the search response shape from 2.2.1 is worth asserting against explicitly so a future regression is caught by your suite rather than by production.
Staleness is a real cost. Releases are rare (2.2.1 is a lone bugfix after a long dormancy) and the package has essentially no downstream dependents. Pin the version, wrap it tightly, and plan for a swap. The repository-class boundary described above is the main mitigation — a replacement PA-API 5.0 client should be a one-file change.
Localization. Response fields (ListPrice->CurrencyCode, image sizes) vary by locale. Store the country alongside each fetched record so a later re-fetch knows which marketplace it came from.
How can I help you explore Laravel packages today?