infyomlabs/laravel-upcitemdb
Laravel wrapper for the UPCitemdb API that lets you look up product details by UPC or barcode. Fetch titles, descriptions, pricing, images, and specs, then bring barcode-driven product data into your PHP applications through a simple, fluent interface.
Install
composer require infyomlabs/laravel-upcitemdb
The package targets Laravel 9 and ships a service provider, so package discovery wires it up automatically. On older Laravel you would register Infyomlabs\LaravelUpcItemDb\Providers\UPCItemDBServiceProvider and the facade alias manually in config/app.php, but you should be on a supported framework anyway.
Publish and configure
php artisan vendor:publish --provider="Infyomlabs\LaravelUpcItemDb\Providers\UPCItemDBServiceProvider"
The published config/upcitemdb.php is the first file to read after composer show infyomlabs/laravel-upcitemdb. Look at three things:
/prod/trial/lookup, unauthenticated, IP rate-limited) and a paid endpoint (/prod/lookup, requires user_key). Confirm which one the package defaults to and how to switch it.UPCITEMDB_USER_KEY / UPCITEMDB_KEY_TYPE). Trial users generally leave these blank.First call
use Infyomlabs\LaravelUpcItemDb\Facades\UPCItemDB;
$result = UPCItemDB::lookup('012345678901');
if (($result['code'] ?? null) === 'OK' && ! empty($result['items'])) {
$item = $result['items'][0];
// $item['title'], $item['brand'], $item['ean'], $item['images'][0] ?? null
}
Confirm the exact facade accessor and method name by opening the facade class under vendor/infyomlabs/laravel-upcitemdb/src/. The wrapper is thin enough that src/ is a five-minute read, and reading it saves more time than reading docs that may not exist.
First use case. A "scan a barcode, show the product" flow is the canonical entry point: one controller method, one lookup, one view. Do not start with bulk imports — the trial rate limits will bite.
Wrap it behind your own interface. Non-negotiable for a 1.0-era package on an EOL framework.
interface BarcodeLookup
{
public function find(string $code): ?ProductData;
public function findMany(array $codes): array;
}
final class UpcItemDbLookup implements BarcodeLookup
{
public function find(string $code): ?ProductData
{
$res = UPCItemDB::lookup($code);
return $this->hydrate($res['items'][0] ?? null);
}
}
Bind it in a service provider. Every test, controller, and job talks to BarcodeLookup, never the facade. This is the single cheapest insurance policy against the package being abandoned or needing a fork for Laravel 11/12.
Cache aggressively. Barcode-to-product mappings are effectively immutable. Cache hits for 7–30 days, misses for hours not days. Cache only definitive answers (code: OK with items, or a clean NOT_FOUND) — never cache 429s or 5xx responses.
return Cache::remember("upc:{$code}", now()->addDays(7), fn () => $this->lookup->find($code));
Batch lookups. The UPCitemdb API accepts comma-separated upc= values and supports offset and matchMode query parameters. If the facade only exposes a single-value lookup(), either pass through the underlying HTTP client or fork the package — the trial endpoint's request-per-hour limit makes batching the difference between viable and not. Note the response shape does not change: you still get one items array.
Persist to a model. Keep a products table keyed by UPC/EAN and refresh on a schedule rather than hitting the API on every request.
Product::updateOrCreate(
['upc' => $item['upc']],
['title' => $item['title'], 'brand' => $item['brand'], 'payload' => $item, 'fetched_at' => now()]
);
Push bulk imports to a queue job. A CSV of 5,000 barcodes is a queued job with ->onQueue('enrichment') and a RateLimiter token bucket in front of it — not a controller call.
Test by faking your wrapper, not the package. If the underlying client uses Laravel's Http facade, Http::fake(['*/lookup*' => Http::response(['code' => 'OK', 'items' => [...]])]) works. If it uses Guzzle directly (likely for a 2022 package), you cannot Http::fake() it. Mock BarcodeLookup in feature tests, and write one integration test against the real HTTP layer in a dedicated suite.
Log raw responses on error. A one-line Log::debug('upcitemdb', ['code' => $code, 'response' => $res]) in the wrapper pays for itself the first time a barcode "does not exist" for a reason you did not anticipate.
Trial endpoint is IP-rate-limited, not key-limited. As of writing, the free tier is roughly 100 requests/day and 6/hour. Behind a NAT or load balancer all your workers share the same bucket, which is why local tests can pass and production suddenly 429s. Read the code field: LIMIT_EXCEEDED is a first-class error value, not an HTTP status.
Response shape is nested. You get { code, total, offset, items: [...] }, not the item. total === 0 with code === 'OK' is the not-found case in some response variants; code === 'NOT_FOUND' in others. Treat both as "missing" and validate defensively — code is a string in the current API but has been inconsistent historically.
Do not conflate transport errors with "not found". Wrap calls in a try/catch around GuzzleHttp\Exception\* or Illuminate\Http\Client\ConnectionException. A network blip that gets cached as "product missing" is a bug you will not find for months.
Normalize identifiers before sending. UPC-A is 12 digits, EAN-13 is 13, ISBN-10 must be converted to ISBN-13. Leading zeros matter — do not ltrim them. Store the normalized form, not the raw scan.
Set explicit timeouts. If the wrapper does not expose one in config, subclass the client or construct it with a Guzzle client that has connect_timeout and timeout set. Default Guzzle timeouts are effectively unbounded and will pin a PHP-FPM worker.
config:cache freezes env values. Your deployment must export UPCITEMDB_* before php artisan config:cache runs, or the production container will silently use the trial endpoint.
Octane. Verify the package's client is constructed per-request or is genuinely stateless. In-process caches or mutable state on the resolved singleton will leak between requests under Octane and Swoole.
Facade alias collision. If another package or your own app already aliases UPCItemDB, discovery will throw at boot. Check config/app.php aliases.
Laravel version ceiling. Last release is 2022 and the v1.0.0 note is "Laravel 9 support added". On Laravel 10+ you will likely need --ignore-platform-reqs or a fork. Because you wrapped it behind BarcodeLookup, a fork is a one-file change — because you did not, it is a refactor.
Extension points to know. Config file (base URL, credentials, timeouts), the service provider (swap the bound client in register()), and the facade accessor. If neither the config nor the provider exposes what you need, do not monkey-patch — replace the binding with your own implementation of the underlying client and keep the wrapper interface stable.
Pin ^1.0 and commit composer.lock. For a package this size that is a one-maintainer project, the lockfile is the real version contract.
How can I help you explore Laravel packages today?